feat(dorm,activity): 补齐宿舍通知与家校活动的发布/回执/台账能力 + 两个裸意图直达表单
build / build-push (push) Successful in 51s

两个模块与「校园公告」同构(发布 → 阅读情况 → 回执闭环),原先只接了「读列表 + 我提交」
一层,价值最高的 发布/盯回执/催办/统计 全空。本轮补齐。

宿舍通知(9 → 35 个工具)
- 发布闭环:dorm.publish(立即发布、默认发给全部宿舍)/ dorm.editNotice /
  dorm.sendList(改指 /api/notice/SendList,我发布的)/ dorm.readDetail / dorm.statistics;
  字典:dorm.noticeTypeDic(type=noticeType)/ dorm.deptDic(type=dept)/ dorm.formTemplates。
- 回执闭环:dorm.receipts / dorm.receiptDetail / dorm.reviewReceipt / dorm.remind /
  dorm.expedite / dorm.remindStudent / dorm.markRead / dorm.pull / dorm.top;
  家长侧补 dorm.initializeRed(一键已读)。
- 宿舍特有的床位台账(模块自带、此前完全没接):dorm.pendingStudents → dorm.assign
  → dorm.removeStudent / dorm.transfer / dorm.setMonitor,以及楼栋房间增改
  (dorm.addBuilding / dorm.editBuilding / dorm.addRoom / dorm.editRoom)。

家校活动(7 → 22 个工具)
- 发布闭环:activity.publish / activity.edit / activity.sent / activity.readDetail /
  activity.statistics;字典:activity.deptDic(type=1)/ activity.categoryDic(type=2)。
- 报名闭环:activity.signups / activity.signupDetail / activity.review /
  activity.remind / activity.expedite / activity.pull / activity.top;
  家长侧补 activity.initializeRed。

裸意图直达(IntentShortcut)
- 新增 DORM_PUBLISH(仅教职工)与 ACTIVITY_PUBLISH(仅教职工),学生落回模型如实说明。
- ★ 修跨模块歧义:「发通知 / 发布通知」不带限定时校园公告与宿舍通知都成立,
  已从公告短语集移除,改由模型按上下文选;只认带「公告/校园」或「宿舍」「活动」限定的说法。

角色(RolePolicy)
- 发布/编辑/阅读情况/回执处理/催办/统计/字典/床位台账 → STAFF_ONLY;
  家长侧(通知箱、回执提交、活动广场、报名、一键已读)保持不限。

表单下拉复用请假的 optionCall + valueKeys + dependsOn 通道(模块 /api/* 只认浏览器会话,
服务端必然 302)。发布表单的接收对象固定为「全部」(receiveType/receiveList 用 fix 写死),
避免表单里出现难用的房间/班级 id 输入框。

测试:新增 DormActivityPublishIntentTest(10);IntentDirectFormTest +4(教师得表单且不过大模型、
学生落回模型);NoticeIntentFormTest 增加「发通知不再被公告劫持」;
RolePermissionTest 补新工具的可见性断言。全量 86/86 通过。

README:4.2 能力划分更新 + 新增「宿舍通知/家校活动补全了哪些能力」;5.1.0 意图对照表补三行。
This commit is contained in:
lijing
2026-10-12 19:49:52 +08:00
parent d2c6c185b5
commit b6a8a63b0e
10 changed files with 918 additions and 31 deletions
@@ -16,7 +16,8 @@ import java.util.Set;
*
* <p>名单:
* <ul>
* <li>{@link #STAFF_ONLY} —— 教职工侧(教师 + 管理员):请假登记/审批/统计、公告发布审批、宿管、专业发展;</li>
* <li>{@link #STAFF_ONLY} —— 教职工侧(教师 + 管理员):请假登记/审批/统计、公告发布审批、
* 宿舍通知发布/回执处理/床位台账、家校活动发布/报名处理、专业发展;</li>
* <li>{@link #STUDENT_ONLY} —— 学生侧:请假申请、交作业、销假;</li>
* <li>{@link #STUDENT_OR_PARENT} —— 家校活动报名:家长与学生都可发起;</li>
* <li>{@link #SHARED} —— 学生与教职工都可能发起的:撤销请假。</li>
@@ -35,10 +36,23 @@ public final class RolePolicy {
// 公告:发布与审批
"notice.sent", "notice.deptDics", "notice.publish",
"notice.approvalList", "notice.approve", "notice.reject",
// 宿舍通知:楼栋/房间/住宿生管理侧
// 宿舍通知:发布 / 编辑 / 阅读情况 / 回执处理 / 催办 / 统计
"dorm.publish", "dorm.editNotice", "dorm.sendList",
"dorm.readDetail", "dorm.receipts", "dorm.receiptDetail", "dorm.reviewReceipt",
"dorm.remind", "dorm.expedite", "dorm.remindStudent", "dorm.markRead",
"dorm.pull", "dorm.top", "dorm.statistics",
// 宿舍通知:发布表单的字典与单据模板
"dorm.deptDic", "dorm.noticeTypeDic", "dorm.formTemplates",
// 宿舍通知:楼栋 / 房间 / 床位台账(宿管日常)
"dorm.buildings", "dorm.rooms", "dorm.roomStudents", "dorm.dormTree",
// 家校活动:发布时选班级
"activity.classTree"
"dorm.pendingStudents", "dorm.assign", "dorm.removeStudent", "dorm.transfer",
"dorm.setMonitor", "dorm.addBuilding", "dorm.editBuilding", "dorm.addRoom", "dorm.editRoom",
// 家校活动:发布 / 编辑 / 阅读情况 / 报名处理 / 催办 / 统计
"activity.publish", "activity.edit", "activity.sent",
"activity.readDetail", "activity.signups", "activity.signupDetail", "activity.review",
"activity.remind", "activity.expedite", "activity.pull", "activity.top", "activity.statistics",
// 家校活动:发布表单的字典与选班级
"activity.deptDic", "activity.categoryDic", "activity.classTree"
));
/** 学生专属 */
@@ -1013,6 +1013,13 @@ public class AgentOrchestrator {
.append("缺哪项就照常调用工具让系统弹表单,不要自己编造发文部门,也不要用文字反复追问。")
.append("该工具只对教职工下发(学生身份看不到),学生要求发布公告时如实说明身份不够。\n");
sb.append("11. ★ 「发布宿舍通知」与「发布校园公告」是**两个不同模块**,不要混用:")
.append("宿舍通知发给**家长**、可要求家长回执(回执方式:无需/确认/填报),发布时要填通知类型与发文部门;")
.append("校园公告是全校范围的公告。用户说「发通知」而没指明模块时,先问清楚是校园公告还是宿舍通知。\n");
sb.append("12. ★ 「发布家校活动」要拿到标题、说明、活动分类、主办部门、活动地点与起止时间;")
.append("活动默认要求家长报名。缺项就照常调用工具让系统弹表单,分类与主办部门必须来自字典,不要编造。\n");
return sb.toString();
}
@@ -11,7 +11,8 @@ import java.util.Set;
/**
* 「裸意图」直达:把只有意图、没有任何细节的一句话(典型:<b>「我要请假」</b>、
* <b>「发布校园公告」</b>)确定性地映射到对应表单,<b>不经过大模型决策</b>。
* <b>「发布校园公告」</b>、<b>「发布宿舍通知」</b>、<b>「发布活动」</b>)
* 确定性地映射到对应表单,<b>不经过大模型决策</b>。
*
* <h3>为什么需要它</h3>
* 同一句「我要请假」,模型可能给出三种都不合预期的反应:
@@ -122,6 +123,29 @@ public class IntentShortcut {
"好的,下面是**发布校园公告**表单:填好公告标题和正文,选好发文部门,"
+ "点「确认提交」我就直接发布(立即生效,全站可见)。");
/**
* 发布宿舍通知意图:<b>仅教职工</b>(宿管 / 管理员)。
*
* <p>与公告不同,宿舍通知强调「给家长发、要回执」,所以表单里带「回执方式」;
* 接收对象默认全部宿舍(房间级定向请到模块页面里勾选)。
*/
private static final Intent DORM_PUBLISH = new Intent("dorm.publish",
null,
"dorm.publish",
"发布宿舍通知需要教师或管理员(宿管)身份;你可以先看看「我的宿舍通知箱」,或换个身份账号再来发布。",
"好的,下面是**发布宿舍通知**表单:填好标题和正文,选好通知类型与发文部门,"
+ "点「确认提交」我就发给全部宿舍的家长(要家长回执的话,在「回执方式」里选一种)。");
/**
* 发布家校活动意图:<b>仅教职工</b>。活动需要地点与起止时间,故表单字段比公告多。
*/
private static final Intent ACTIVITY_PUBLISH = new Intent("activity.publish",
null,
"activity.publish",
"发布家校活动需要教师或管理员身份;你可以先看看「活动广场」,或换个身份账号再来发布。",
"好的,下面是**发布家校活动**表单:填好活动标题、说明、分类、地点和起止时间,"
+ "点「确认提交」我就发给全部班级的家长(需要家长报名的话,报名方式保持默认即可)。");
/**
* 请假的标准说法全集(已剥掉语气词与标点后的形态)。
*/
@@ -137,16 +161,33 @@ public class IntentShortcut {
* (「公告」「通知」单独一句,用户多半是想看看有什么),误判成"发布"会很突兀。
* 只认带「发/发布」动词的说法,与「读公告」明确区分开。
*
* <p>⚠️ 现在收「发通知」是安全的:本平台里 {@code campusnotice} 就是「校园公告(校园通知)」,
* 而宿舍通知目前**没有**发布工具({@code dormnotice} 只有家长侧回执)。等将来接上
* 宿舍通知发布后,「发通知」会变成跨模块歧义(猜错模块比不直达更糟)——
* 届时要么把本组里的「通知」说法摘掉只留「公告」,要么交给模型按上下文选。
* <p>★ 2026-10 起「发通知 / 发布通知」这类**不带模块限定**的说法已从本组移除:
* 宿舍通知也接上了发布工具({@code dorm.publish}),同一句「发通知」会变成跨模块歧义
* (猜错模块比不直达更糟)。现在只认带「公告」或「校园」限定的说法,其余交给模型按上下文选。
*/
private static final Set<String> NOTICE_PUBLISH_BARE = new HashSet<>(Arrays.asList(
"发布公告", "发公告", "发个公告", "发布校园公告", "校园公告发布",
"发布一条公告", "发一条公告", "发布一篇公告", "发一篇公告",
"发布通知", "发通知", "发个通知", "发布校园通知", "校园通知发布",
"发布一条通知", "发一条通知"
"发布校园通知", "校园通知发布"
));
/**
* 发布宿舍通知的标准说法全集 —— 必须带「宿舍」限定(否则与校园公告混淆)。
*/
private static final Set<String> DORM_PUBLISH_BARE = new HashSet<>(Arrays.asList(
"发布宿舍通知", "发宿舍通知", "发个宿舍通知", "发一条宿舍通知", "发布一条宿舍通知",
"宿舍通知发布", "发布宿舍公告", "发宿舍公告", "宿舍公告发布", "给宿舍发通知"
));
/**
* 发布家校活动的标准说法全集。
*
* <p>⚠️ 不收「报名活动」—— 那是家长侧的另一件事({@code activity.signup}),不属发布意图。
*/
private static final Set<String> ACTIVITY_PUBLISH_BARE = new HashSet<>(Arrays.asList(
"发布活动", "发活动", "发个活动", "发起活动", "活动发布",
"发布一个活动", "发布一条活动", "发一条活动",
"发布家校活动", "发家校活动", "发布校园活动", "发校园活动", "发布亲子活动"
));
/**
@@ -172,6 +213,8 @@ public class IntentShortcut {
static {
RULES.add(new Rule(LEAVE_BARE, LEAVE));
RULES.add(new Rule(NOTICE_PUBLISH_BARE, NOTICE_PUBLISH));
RULES.add(new Rule(DORM_PUBLISH_BARE, DORM_PUBLISH));
RULES.add(new Rule(ACTIVITY_PUBLISH_BARE, ACTIVITY_PUBLISH));
}
/**
@@ -7,20 +7,36 @@ import java.util.ArrayList;
import java.util.List;
/**
* 家校活动模块工具 —— 后端 FamilyActivity,前缀 /api/activity、/api/base。
* 家校活动模块工具 —— 后端 FamilyActivity,前缀 /api/activity、/api/dict、/api/base。
*
* <p>核心闭环:活动广场 → 家长报名 → 学校确认。
* <p>核心闭环:<b>学校发活动 → 家长报名 → 学校确认报名 / 盯报名情况</b>。
* 与「宿舍通知」同构(发布 → 阅读情况 → 回执闭环),只是回执换成了「报名」。
*/
public final class ActivityTools {
public static final String MODULE = "familyactivity";
/** 主办部门字典的 type 值(/api/dict/List?type=1) */
private static final Object DICT_TYPE_DEPT = 1;
/** 活动分类字典的 type 值(/api/dict/List?type=2) */
private static final Object DICT_TYPE_CATEGORY = 2;
private ActivityTools() {
}
public static List<ToolDef> defs() {
List<ToolDef> list = new ArrayList<>();
addParentSide(list);
addPublishLoop(list);
addSignupLoop(list);
addDictionaries(list);
return list;
}
/* ==================== 家长 / 学生侧 ==================== */
private static void addParentSide(List<ToolDef> list) {
list.add(ToolDef.of("activity.plaza", MODULE, "活动广场",
"当前家长/学生视角的活动广场(含我的报名状态与分桶计数)。用户问“最近有什么活动”“我能报什么”时用它。",
"GET", "/api/activity/MyActivities")
@@ -72,11 +88,185 @@ public final class ActivityTools {
.label("活动"))
.write("/par/mine"));
list.add(ToolDef.of("activity.initializeRed", MODULE, "一键已读全部活动",
"把家长端收到的活动一次性全部标记为已读。",
"POST_FORM", "/api/activity/InitializeRed")
.write("/par/activities"));
}
/* ==================== 发布闭环(教职工) ==================== */
private static void addPublishLoop(List<ToolDef> list) {
list.add(ToolDef.of("activity.publish", MODULE, "发布家校活动",
"发布一条家校活动(可要求家长报名),默认发给**全部班级**的家长。"
+ "调用前必须确认:标题 title、正文 content、活动分类 category、主办部门 deptId、"
+ "活动地点 venue、开始/结束时间 startTime/endTime;"
+ "缺失就直接调用本工具,系统会弹出表单让用户补齐,不要用文字反复追问;"
+ "活动分类与主办部门必须来自字典,不要编造。默认立即发布;如需先存草稿,把 draft 置 true。",
"POST_JSON", "/api/activity/Create")
.p(ToolParam.required("title", "string", "活动标题")
.label("活动标题").eg("秋季亲子运动会"),
ToolParam.required("content", "string", "活动说明(纯文本)")
.label("活动说明").as("textarea").eg("为增进家校互动,学校拟举办秋季亲子运动会……"),
ToolParam.required("category", "string", "活动分类,来自 activity.categoryDic 的 k 字段")
.label("活动分类")
.optionsFrom("activity.categoryDic").optionsKeys("k", "v"),
ToolParam.required("deptId", "string", "主办部门 id,来自 activity.deptDic 的 k 字段")
.label("主办部门")
.optionsFrom("activity.deptDic").optionsKeys("k", "v"),
ToolParam.required("venue", "string", "活动地点")
.label("活动地点").eg("学校操场"),
ToolParam.required("startTime", "string", "活动开始时间 yyyy-MM-dd HH:mm:ss")
.label("开始时间").eg("2026-10-20 09:00:00"),
ToolParam.required("endTime", "string", "活动结束时间 yyyy-MM-dd HH:mm:ss")
.label("结束时间").eg("2026-10-20 11:30:00"),
ToolParam.required("receiptMode", "string", "报名方式:signup 需要家长报名 / none 无需报名")
.label("报名方式").dft("signup")
.options(new String[]{"signup", "none"}, new String[]{"需要家长报名", "无需报名"}),
ToolParam.optional("signupDeadline", "string", "报名截止时间 yyyy-MM-dd HH:mm:ss(留空=不限)")
.label("报名截止时间").eg("2026-10-18 18:00:00"),
ToolParam.optional("quota", "integer", "名额上限(0 或留空 = 不限)")
.label("名额上限").dft("0"),
ToolParam.optional("allowAccompany", "integer", "是否允许家长随行")
.label("家长随行").dft("1")
.options(new String[]{"1", "0"}, new String[]{"允许", "不允许"}),
ToolParam.optional("cost", "number", "活动费用(元,留空=免费)")
.label("活动费用"),
ToolParam.optional("smsFlag", "integer", "是否同时发短信提醒家长")
.label("同时发短信").dft("0")
.options(new String[]{"0", "1"}, new String[]{"否", "是"}),
ToolParam.optional("topmost", "integer", "是否置顶")
.label("置顶").dft("0")
.options(new String[]{"0", "1"}, new String[]{"否", "是"}),
ToolParam.optional("draft", "string", "是否存草稿:true 只保存不发布")
.label("存草稿").dft("false")
.options(new String[]{"false", "true"}, new String[]{"立即发布", "存草稿"}))
// 接收对象:默认全部班级(班级级定向请到模块页面里勾选)
.fix("receiveType", "class")
.fix("receiveList", "ALL")
.write("/list/new"));
list.add(ToolDef.of("activity.edit", MODULE, "编辑家校活动",
"修改一条已发布(或草稿)的活动。调用前须确认 sourceId(要改的那条活动 id)。",
"POST_JSON", "/api/activity/Edit")
.p(ToolParam.required("sourceId", "integer", "要编辑的活动 id")
.label("活动"),
ToolParam.required("title", "string", "活动标题")
.label("活动标题"),
ToolParam.required("content", "string", "活动说明")
.label("活动说明").as("textarea"))
.fix("receiveType", "class")
.fix("receiveList", "ALL")
.write("/list"));
list.add(ToolDef.of("activity.sent", MODULE, "我发布的活动",
"查询我发布的家校活动列表(可按关键词筛选)。用户问“我发过哪些活动”“那个活动发了没”时用它。",
"GET", "/api/activity/SendList")
.p(ToolParam.optional("pageIndex", "integer", "页码").dft("1"),
ToolParam.optional("pageSize", "integer", "每页条数").dft("10"),
ToolParam.optional("scope", "string", "传 user 只看自己发布的"),
ToolParam.optional("keyword", "string", "标题关键词"))
.readAt("/list"));
list.add(ToolDef.of("activity.readDetail", MODULE, "活动阅读情况",
"查询某个活动的阅读情况(已读 / 未读分组,按年级·班级)。",
"GET", "/api/activity/ReadDetail")
.p(ToolParam.required("serid", "integer", "活动 id"))
.readAt("/signups"));
list.add(ToolDef.of("activity.pull", MODULE, "撤回活动",
"撤回一条已发布的活动(撤回后家长端不可见)。",
"POST_FORM", "/api/activity/UserPull")
.p(ToolParam.required("serid", "integer", "活动 id")
.label("活动"))
.write("/list"));
list.add(ToolDef.of("activity.top", MODULE, "置顶 / 取消置顶活动",
"把一个活动置顶或取消置顶。",
"POST_FORM", "/api/activity/SetTopmost")
.p(ToolParam.required("serid", "integer", "活动 id")
.label("活动"),
ToolParam.optional("topmost", "integer", "1 置顶(默认) / 0 取消置顶")
.label("操作").dft("1")
.options(new String[]{"1", "0"}, new String[]{"置顶", "取消置顶"}))
.write("/list"));
list.add(ToolDef.of("activity.expedite", MODULE, "催办未读(活动)",
"对一个活动的未读人员统一催办(短信 / 站内提醒)。",
"POST_FORM", "/api/activity/Expedite")
.p(ToolParam.required("serid", "integer", "活动 id")
.label("活动"))
.write("/signups"));
list.add(ToolDef.of("activity.statistics", MODULE, "活动统计",
"查询活动的发布 / 阅读 / 报名统计(可按时间段)。",
"GET", "/api/activity/Statis")
.p(ToolParam.optional("startTime", "string", "开始时间 yyyy-MM-dd").eg("2026-10-01"),
ToolParam.optional("endTime", "string", "结束时间 yyyy-MM-dd").eg("2026-10-31"))
.readAt("/stats"));
}
/* ==================== 报名闭环(教职工) ==================== */
private static void addSignupLoop(List<ToolDef> list) {
list.add(ToolDef.of("activity.signups", MODULE, "活动报名名单",
"查询某个活动的报名 / 回执名单(按班级分组 + 汇总)。"
+ "用户问“还有谁没报名”“报名情况怎么样”时用它。",
"GET", "/api/activity/Signups")
.p(ToolParam.required("serid", "integer", "活动 id")
.label("活动"),
ToolParam.optional("signupStatus", "string",
"筛选:all 全部 / signed 已报名 / confirmed 已确认 / canceled 已取消 / none 未报名")
.dft("all"))
.readAt("/signups"));
list.add(ToolDef.of("activity.signupDetail", MODULE, "单条报名详情",
"查看某条报名的明细(家长填写的参加人数 / 联系人 / 备注)。",
"GET", "/api/activity/SignupDetail")
.p(ToolParam.required("signupId", "integer", "报名 id"))
.readAt("/signups"));
list.add(ToolDef.of("activity.review", MODULE, "处理活动报名",
"对一条报名执行确认 / 取消 / 候补操作。",
"POST_JSON", "/api/activity/Review")
.p(ToolParam.required("signupId", "integer", "报名 id")
.label("报名"),
ToolParam.required("action", "string", "处理动作:confirm 通过 / cancel 取消 / waitlist 转候补")
.label("处理动作")
.options(new String[]{"confirm", "cancel", "waitlist"},
new String[]{"通过", "取消", "转候补"}))
.write("/signups"));
list.add(ToolDef.of("activity.remind", MODULE, "催报名(活动)",
"向未报名的家长发送报名提醒。",
"POST_FORM", "/api/activity/Remind")
.p(ToolParam.required("serid", "integer", "活动 id")
.label("活动"))
.write("/signups"));
}
/* ==================== 字典(发布表单下拉的来源) ==================== */
private static void addDictionaries(List<ToolDef> list) {
list.add(ToolDef.of("activity.deptDic", MODULE, "主办部门字典",
"查询本校可选的主办部门(发布活动时填 deptId 用)。"
+ "仅在用户明确问「有哪些主办部门」时使用;"
+ "发布活动时**不要**先调它 —— 表单的「主办部门」下拉会自动带出可选值。",
"GET", "/api/dict/List")
.fix("type", DICT_TYPE_DEPT)
.readAt("/list/new"));
list.add(ToolDef.of("activity.categoryDic", MODULE, "活动分类字典",
"查询本校可选的活动分类(发布活动时填 category 用)。"
+ "仅在用户明确问「有哪些活动分类」时使用;"
+ "发布活动时**不要**先调它 —— 表单的「活动分类」下拉会自动带出可选值。",
"GET", "/api/dict/List")
.fix("type", DICT_TYPE_CATEGORY)
.readAt("/list/new"));
list.add(ToolDef.of("activity.classTree", MODULE, "年级班级树",
"年级 → 班级 树(发布活动时选接收班级用)。",
"GET", "/api/base/GetClassTree")
.readAt("/list/new"));
return list;
}
}
@@ -7,20 +7,44 @@ import java.util.ArrayList;
import java.util.List;
/**
* 宿舍通知模块工具 —— 后端 DormNotice,前缀 /api/notice、/api/dorm、/api/base。
* 宿舍通知模块工具 —— 后端 DormNotice,前缀 /api/notice、/api/dorm、/api/dict、/api/base。
*
* <p>家校闭环:宿管发通知 → 家长查看 → 家长回执/填报。
* <p>两条主线:
* <ul>
* <li><b>家校闭环</b>:宿管发通知 → 家长查看 → 家长回执/填报 → 宿管盯回执、催办。</li>
* <li><b>床位台账</b>:待安排池 → 安排入住 → 退宿 → 调宿 → 设宿舍长(宿管每天的活)。</li>
* </ul>
*
* <p>注意:本模块与「校园公告」一样,服务端直调用户态接口会拿到 {@code code=302}(只认浏览器 CAS 会话),
* 凡是需要「取字典给下拉」的字段都用 {@code optionsFrom + optionsKeys} 声明,由前端带用户登录态代取
* (见 AgentOrchestrator#buildForm)。
*/
public final class DormNoticeTools {
public static final String MODULE = "dormnotice";
/** 通知类型字典的 type 值(/api/dict/List?type=noticeType) */
private static final String DICT_TYPE_NOTICE = "noticeType";
/** 发文部门字典的 type 值(/api/dict/List?type=dept) */
private static final String DICT_TYPE_DEPT = "dept";
private DormNoticeTools() {
}
public static List<ToolDef> defs() {
List<ToolDef> list = new ArrayList<>();
addParentSide(list);
addPublishLoop(list);
addReceiptLoop(list);
addDictionaries(list);
addDormLedger(list);
return list;
}
/* ==================== 家长 / 学生侧 ==================== */
private static void addParentSide(List<ToolDef> list) {
list.add(ToolDef.of("dorm.myNotices", MODULE, "我的宿舍通知箱",
"当前家长/学生视角的宿舍通知箱(含我的回执状态与各分桶计数)。用户问“宿舍有什么通知”“我要回执”时用它。",
"GET", "/api/notice/MyNotices")
@@ -35,14 +59,6 @@ public final class DormNoticeTools {
.p(ToolParam.required("serid", "integer", "通知 id"))
.readAt("/par/notices"));
list.add(ToolDef.of("dorm.sendList", MODULE, "宿舍通知发布列表",
"查询已发布的宿舍通知(家长端收件箱 / 管理端全部)。",
"GET", "/api/notice/List")
.p(ToolParam.optional("pageIndex", "integer", "页码").dft("1"),
ToolParam.optional("pageSize", "integer", "每页条数").dft("10"),
ToolParam.optional("keyword", "string", "标题关键词"))
.readAt("/list"));
list.add(ToolDef.of("dorm.submitReceipt", MODULE, "提交宿舍通知回执",
"家长提交某条宿舍通知的回执 / 填报。调用前须确认 serid;如该通知需填报,还需填 fillData 内容。",
"POST_JSON", "/api/notice/SubmitReceipt")
@@ -67,6 +83,198 @@ public final class DormNoticeTools {
.label("宿舍通知"))
.write("/par/mine"));
list.add(ToolDef.of("dorm.initializeRed", MODULE, "一键已读全部宿舍通知",
"把我收到的宿舍通知一次性全部标记为已读。",
"POST_FORM", "/api/notice/InitializeRed")
.write("/par/notices"));
}
/* ==================== 发布闭环(教职工) ==================== */
private static void addPublishLoop(List<ToolDef> list) {
list.add(ToolDef.of("dorm.publish", MODULE, "发布宿舍通知",
"发布一条宿舍通知(可要求家长回执 / 填报),默认发给**全部宿舍**的家长。"
+ "调用前必须确认:标题 title、正文 content、通知类型 noticeType、发文部门 deptId;"
+ "缺失就直接调用本工具,系统会弹出表单让用户补齐,不要用文字反复追问;"
+ "通知类型与发文部门必须来自字典,不要编造。默认立即发布;如需先存草稿,把 draft 置 true。",
"POST_JSON", "/api/notice/Create")
.p(ToolParam.required("title", "string", "通知标题")
.label("通知标题").eg("关于宿舍熄灯时间调整的通知"),
ToolParam.required("content", "string", "通知正文(纯文本)")
.label("通知正文").as("textarea").eg("各位家长:自本周起,宿舍熄灯时间调整为 22:30……"),
ToolParam.required("noticeType", "string", "通知类型,来自 dorm.noticeTypeDic 的 k 字段")
.label("通知类型")
.optionsFrom("dorm.noticeTypeDic").optionsKeys("k", "v"),
ToolParam.required("deptId", "string", "发文部门 id,来自 dorm.deptDic 的 k 字段")
.label("发文部门")
.optionsFrom("dorm.deptDic").optionsKeys("k", "v"),
ToolParam.required("receiptMode", "string", "回执方式:none 无需回执 / confirm 确认回执 / fill 填报回执")
.label("回执方式").dft("none")
.options(new String[]{"none", "confirm", "fill"},
new String[]{"无需回执", "需要确认回执", "需要填报回执"}),
ToolParam.optional("deadlineTime", "string", "回执截止时间 yyyy-MM-dd HH:mm:ss(留空=不限)")
.label("回执截止时间").eg("2026-10-20 18:00:00"),
ToolParam.optional("needSupervisor", "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("topmost", "integer", "是否置顶")
.label("置顶").dft("0")
.options(new String[]{"0", "1"}, new String[]{"否", "是"}),
ToolParam.optional("draft", "string", "是否存草稿:true 只保存不发布")
.label("存草稿").dft("false")
.options(new String[]{"false", "true"}, new String[]{"立即发布", "存草稿"}))
// 接收对象:默认全部宿舍(房间级定向请到模块页面里勾选)
.fix("receiveType", "room")
.fix("receiveList", "ALL")
.write("/list/new"));
list.add(ToolDef.of("dorm.editNotice", MODULE, "编辑宿舍通知",
"修改一条已发布(或草稿)的宿舍通知。调用前须确认 sourceId(要改的那条通知 id)。",
"POST_JSON", "/api/notice/Edit")
.p(ToolParam.required("sourceId", "integer", "要编辑的通知 id")
.label("通知"),
ToolParam.required("title", "string", "通知标题")
.label("通知标题"),
ToolParam.required("content", "string", "通知正文")
.label("通知正文").as("textarea"))
.fix("receiveType", "room")
.fix("receiveList", "ALL")
.write("/list"));
list.add(ToolDef.of("dorm.sendList", MODULE, "我发布的宿舍通知",
"查询我发布的宿舍通知列表(可按关键词 / 状态筛选)。用户问“我发过哪些宿舍通知”“那条通知发了没”时用它。",
"GET", "/api/notice/SendList")
.p(ToolParam.optional("pageIndex", "integer", "页码").dft("1"),
ToolParam.optional("pageSize", "integer", "每页条数").dft("10"),
ToolParam.optional("scope", "string", "传 user 只看自己发布的"),
ToolParam.optional("keyword", "string", "标题关键词"))
.readAt("/list"));
list.add(ToolDef.of("dorm.readDetail", MODULE, "宿舍通知阅读情况",
"查询某条宿舍通知的阅读情况(已读 / 未读分组,按楼栋·房间)。",
"GET", "/api/notice/ReadDetail")
.p(ToolParam.required("serid", "integer", "通知 id"))
.readAt("/receipts"));
list.add(ToolDef.of("dorm.pull", MODULE, "撤回宿舍通知",
"撤回一条已发布的宿舍通知(撤回后家长端不可见)。",
"POST_FORM", "/api/notice/UserPull")
.p(ToolParam.required("serid", "integer", "通知 id")
.label("宿舍通知"))
.write("/list"));
list.add(ToolDef.of("dorm.top", MODULE, "置顶 / 取消置顶宿舍通知",
"把一条宿舍通知置顶或取消置顶。",
"POST_FORM", "/api/notice/SetTopmost")
.p(ToolParam.required("serid", "integer", "通知 id")
.label("宿舍通知"),
ToolParam.optional("topmost", "integer", "1 置顶(默认) / 0 取消置顶")
.label("操作").dft("1")
.options(new String[]{"1", "0"}, new String[]{"置顶", "取消置顶"}))
.write("/list"));
list.add(ToolDef.of("dorm.expedite", MODULE, "催办未读(宿舍通知)",
"对一条宿舍通知的未读人员统一催办(短信 / 站内提醒)。",
"POST_FORM", "/api/notice/Expedite")
.p(ToolParam.required("serid", "integer", "通知 id")
.label("宿舍通知"))
.write("/receipts"));
list.add(ToolDef.of("dorm.remindStudent", MODULE, "向某学生家长催办(宿舍通知)",
"针对某条宿舍通知,单独向某个学生的家长发送提醒。",
"POST_FORM", "/api/notice/RemindStudent")
.p(ToolParam.required("serid", "integer", "通知 id")
.label("宿舍通知"),
ToolParam.required("studentId", "integer", "学生 id")
.label("学生"))
.write("/receipts"));
list.add(ToolDef.of("dorm.markRead", MODULE, "代标记某学生已读(宿舍通知)",
"管理员代为把某条宿舍通知标记为某学生已读。",
"POST_FORM", "/api/notice/MarkRead")
.p(ToolParam.required("serid", "integer", "通知 id")
.label("宿舍通知"),
ToolParam.required("studentId", "integer", "学生 id")
.label("学生"))
.write("/receipts"));
list.add(ToolDef.of("dorm.statistics", MODULE, "宿舍通知统计",
"查询宿舍通知的发送 / 阅读 / 回执统计(可按时间段)。",
"GET", "/api/notice/Statis")
.p(ToolParam.optional("startTime", "string", "开始时间 yyyy-MM-dd").eg("2026-10-01"),
ToolParam.optional("endTime", "string", "结束时间 yyyy-MM-dd").eg("2026-10-31"))
.readAt("/stats"));
}
/* ==================== 回执闭环(教职工) ==================== */
private static void addReceiptLoop(List<ToolDef> list) {
list.add(ToolDef.of("dorm.receipts", MODULE, "宿舍通知回执名单",
"查询某条宿舍通知的回执 / 填报名单(按楼栋·楼层分组 + 汇总)。"
+ "用户问“还有谁没回执”“回执情况怎么样”时用它。",
"GET", "/api/notice/Receipts")
.p(ToolParam.required("serid", "integer", "通知 id")
.label("宿舍通知"),
ToolParam.optional("status", "string", "筛选:all 全部 / confirmed 已确认 / filled 已填报 / canceled 已取消 / none 未回执")
.dft("all"))
.readAt("/receipts"));
list.add(ToolDef.of("dorm.receiptDetail", MODULE, "单条回执详情",
"查看某条回执的明细(家长填写的确认 / 填报内容)。",
"GET", "/api/notice/ReceiptDetail")
.p(ToolParam.required("receiptId", "integer", "回执 id"))
.readAt("/receipts"));
list.add(ToolDef.of("dorm.reviewReceipt", MODULE, "处理宿舍通知回执",
"对一条回执执行确认 / 取消 / 重置操作。",
"POST_JSON", "/api/notice/Review")
.p(ToolParam.required("receiptId", "integer", "回执 id")
.label("回执"),
ToolParam.required("action", "string", "处理动作:confirm 通过确认 / cancel 取消 / reset 重置为待处理")
.label("处理动作")
.options(new String[]{"confirm", "cancel", "reset"},
new String[]{"通过确认", "取消", "重置为待处理"}))
.write("/receipts"));
list.add(ToolDef.of("dorm.remind", MODULE, "催回执(宿舍通知)",
"向未回执的家长发送回执提醒。",
"POST_FORM", "/api/notice/Remind")
.p(ToolParam.required("serid", "integer", "通知 id")
.label("宿舍通知"))
.write("/receipts"));
}
/* ==================== 字典 / 模板(发布表单下拉的来源) ==================== */
private static void addDictionaries(List<ToolDef> list) {
list.add(ToolDef.of("dorm.noticeTypeDic", MODULE, "宿舍通知类型字典",
"查询本校可选的宿舍通知类型(发布宿舍通知时填 noticeType 用)。"
+ "仅在用户明确问「有哪些通知类型」时使用;"
+ "发布宿舍通知时**不要**先调它 —— 表单的「通知类型」下拉会自动带出可选值。",
"GET", "/api/dict/List")
.fix("type", DICT_TYPE_NOTICE)
.readAt("/list/new"));
list.add(ToolDef.of("dorm.deptDic", MODULE, "宿舍通知发文部门字典",
"查询本校可选的发文部门(发布宿舍通知时填 deptId 用)。"
+ "仅在用户明确问「有哪些发文部门」时使用;"
+ "发布宿舍通知时**不要**先调它 —— 表单的「发文部门」下拉会自动带出可选值。",
"GET", "/api/dict/List")
.fix("type", DICT_TYPE_DEPT)
.readAt("/list/new"));
list.add(ToolDef.of("dorm.formTemplates", MODULE, "回执单据模板",
"查询「需填报」类宿舍通知可用的单据模板(模板字段由后端预置,发布人只选类型)。",
"GET", "/api/notice/FormTemplates")
.readAt("/list/new"));
}
/* ==================== 宿舍床位台账(宿管日常) ==================== */
private static void addDormLedger(List<ToolDef> list) {
list.add(ToolDef.of("dorm.buildings", MODULE, "宿舍楼栋列表",
"查询宿舍楼栋列表(含房间数/床位数/入住数)。",
"GET", "/api/dorm/BuildingList")
@@ -86,10 +294,104 @@ public final class DormNoticeTools {
.readAt("/setting"));
list.add(ToolDef.of("dorm.dormTree", MODULE, "宿舍楼栋房间树",
"楼栋 → 房间 的完整树(发布宿舍通知时选接收房间用)。",
"楼栋 → 房间 的完整树(发布宿舍通知、选择房间时用)。",
"GET", "/api/base/GetDormTree")
.readAt("/list/new"));
return list;
list.add(ToolDef.of("dorm.pendingStudents", MODULE, "待安排床位学生池",
"查询尚未安排床位的学生名单(「安排入住」的数据源)。",
"GET", "/api/dorm/PendingStudents")
.p(ToolParam.optional("keyword", "string", "姓名关键词"),
ToolParam.optional("gender", "string", "性别筛选"))
.readAt("/setting"));
list.add(ToolDef.of("dorm.assign", MODULE, "安排入住(分配床位)",
"把待安排的学生调入某个房间(按空床位顺序自动落位)。"
+ "调用前须确认 roomId 与学生 id(可多个,逗号分隔)。",
"POST_FORM", "/api/dorm/AssignStudent")
.p(ToolParam.required("roomId", "integer", "目标房间 id")
.label("房间"),
ToolParam.required("studentIds", "string", "学生 id,多个用英文逗号分隔")
.label("学生"))
.write("/setting"));
list.add(ToolDef.of("dorm.removeStudent", MODULE, "退宿(移出房间)",
"把某学生移出所在房间,退回待安排池。",
"POST_FORM", "/api/dorm/RemoveStudent")
.p(ToolParam.required("roomId", "integer", "房间 id")
.label("房间"),
ToolParam.required("studentId", "integer", "学生 id")
.label("学生"))
.write("/setting"));
list.add(ToolDef.of("dorm.transfer", MODULE, "调宿(换房间)",
"把房间内某学生转到另一间房。",
"POST_FORM", "/api/dorm/TransferStudent")
.p(ToolParam.required("studentId", "integer", "学生 id")
.label("学生"),
ToolParam.required("fromRoomId", "integer", "原房间 id")
.label("原房间"),
ToolParam.required("toRoomId", "integer", "目标房间 id")
.label("目标房间"))
.write("/setting"));
list.add(ToolDef.of("dorm.setMonitor", MODULE, "设置 / 取消宿舍长",
"把某房间内的一名学生设为宿舍长;studentId 传 0 表示取消宿舍长。",
"POST_FORM", "/api/dorm/SetMonitor")
.p(ToolParam.required("roomId", "integer", "房间 id")
.label("房间"),
ToolParam.required("studentId", "integer", "学生 id(传 0 取消宿舍长)")
.label("学生"))
.write("/setting"));
list.add(ToolDef.of("dorm.addBuilding", MODULE, "新增宿舍楼栋",
"新增一栋宿舍楼。",
"POST_FORM", "/api/dorm/AddBuilding")
.p(ToolParam.required("name", "string", "楼栋名称")
.label("楼栋名称").eg("1 号楼"),
ToolParam.optional("floors", "integer", "楼层数")
.label("楼层数"),
ToolParam.optional("remark", "string", "备注")
.label("备注").as("textarea"))
.write("/setting"));
list.add(ToolDef.of("dorm.editBuilding", MODULE, "编辑宿舍楼栋",
"修改宿舍楼栋信息(名称 / 楼层数 / 备注)。",
"POST_FORM", "/api/dorm/EditBuilding")
.p(ToolParam.required("buildingId", "integer", "楼栋 id")
.label("楼栋"),
ToolParam.required("name", "string", "楼栋名称")
.label("楼栋名称"),
ToolParam.optional("floors", "integer", "楼层数")
.label("楼层数"),
ToolParam.optional("remark", "string", "备注")
.label("备注").as("textarea"))
.write("/setting"));
list.add(ToolDef.of("dorm.addRoom", MODULE, "新增宿舍房间",
"在某楼栋下新增房间(可按 roomCount 批量生成连续房间号)。",
"POST_FORM", "/api/dorm/AddRoom")
.p(ToolParam.required("buildingId", "integer", "所属楼栋 id")
.label("楼栋"),
ToolParam.required("roomNo", "string", "房间号")
.label("房间号").eg("301"),
ToolParam.optional("bedCount", "integer", "床位数")
.label("床位数").dft("6"),
ToolParam.optional("roomCount", "integer", "批量生成房间数量(留空=只建一间)")
.label("批量数量"))
.write("/setting"));
list.add(ToolDef.of("dorm.editRoom", MODULE, "编辑宿舍房间",
"修改房间信息(房间号 / 床位数 / 宿管)。",
"POST_FORM", "/api/dorm/EditRoom")
.p(ToolParam.required("roomId", "integer", "房间 id")
.label("房间"),
ToolParam.optional("roomNo", "string", "房间号")
.label("房间号"),
ToolParam.optional("bedCount", "integer", "床位数")
.label("床位数"),
ToolParam.optional("supervisor", "string", "宿管姓名")
.label("宿管"))
.write("/setting"));
}
}