Files
profession-front/docs/宿舍通知模块-功能与接口说明-v1.md
T

22 KiB
Raw Blame History

宿舍通知模块 · 功能与接口说明 v1

模板来源:D:\项目管理\FamilyActivity(家校活动信息发布) 本模块:D:\项目管理\DormNotice(家校宿舍通知) 技术栈一致:Vue3 + Vite + view-ui-plus + alova + vite-plugin-mock,npm run dev:mock 纯前端可跑。


一、为什么不能直接复用「活动」模块

活动与宿舍通知同为「发布 → 家长接收 → 回执」的家校闭环,但收件人粒度和回执形态两处根本差异,决定了它是独立模块而不是活动的换皮:

维度 活动信息发布 宿舍通知
收件人粒度 班级(receiveType=class,receiveList 为班级 id 串) 宿舍房间(receiveType=room,receiveList 为房间 id 串)
应收人数口径 班级学生数 覆盖房间的实际入住床位数
回执形态 单一:报名(填参加人数 + 随行 + 费用) 三档:仅通知 / 需确认(打开即确认) / 需填报(统一回执单:公共区固定 + 专属区随单据类型)
回执内容 固定字段(joinCount、guardianName、remark) 动态字段:由发布方声明(留宿起止、探视时段、搬迁日期…)
紧急程度 无 普通 / 重要 / 紧急(家长端置顶标红)
宿管协同 无 needSupervisor:需宿管到场配合执行
覆盖统计 按班 按楼栋覆盖房间数 / 床位数

关键业务点: 武校是寄宿制,同一班级的学生分住不同楼栋房间。若沿用「按班级下发」,一条「1号楼 3 层明日停水」会推送给全部家长(大量无关触达),而真正受影响的房间只占少数。因此本模块把发布对象下沉到房间一级,应收人数取房间实际入住床位,回执率才具备管理意义。


二、功能地图(7 个页面)

发布端(教师 / 宿管 / 德育处)

# 页面 路由 modId 说明
1 我发布的 /list 2 本人发布的通知列表,含草稿;行内可催办、置顶、撤回、删除、复用再发
2 全部通知 /noticelist 3 全校通知(去掉 scope=user),用于跨部门查询与统计核对
3 发布通知 /list/new 22(parentId=2) 新建 / 编辑(?serid=)/ 复用(?fid=)三态同一组件
4 通知详情 /list/detail、/noticelist/detail 23 / 24 管理视角(type=0 不标读)与查看视角(type=1 标读)
5 回执情况 /receipts 25 按「楼栋·房间」分组的回执/填报名单,支持代确认、取消、退回、催办、导出 CSV
6 通知统计 /stats 4 总量/回执率/覆盖人次、类型分布、月度趋势、楼栋覆盖、发布人分布
7 宿舍设置 /setting 21 通知类型字典 + 宿舍楼栋房间台账 + 自定义组(宿管/生活老师)

家长端(家长 / 学生账号)

# 页面 路由 modId 说明
8 宿舍通知箱 /par/notices 31 左侧分桶列表(全部/待回执/已回执/已结束)+ 右侧通知详情与回执操作
9 我的回执 /par/mine 32 同一组件,meta.initTab=done 默认落「已回执」

端分离机制(沿用模板)

App.vue 按路由前缀判定视角(/par → parent,其余 → teacher),先调 POST /api/base/SetRole 切 mock 当前用户,再拉 GetUserInfo / GetUserMod。这样同一份代码在家长端与发布端各拿一份身份与菜单,不需要两套工程。


三、发布通知页(模块的复杂度中心)

3.1 表单字段

字段 控件 必填 说明
通知标题 Input(≤50 字) ✅ 家长端列表主标题
通知类型 Select ✅ 取自「宿舍设置 → 通知类型」字典(停用项不下发)
发布部门 Select ✅ 校办 / 德育处 / 生活管理科 / 教务处 / 后勤保障部 / 校医务室 / 武术训练部 / 安全管理办公室
紧急程度 Radio — 普通 / 重要 / 紧急(家长端置顶标红)
生效时间 DatePicker(datetime) — 留空 = 立即生效;用于「明早起执行新作息」提前下发
回执截止 DatePicker(datetime) — 留空 = 不设截止;早于生效时间会被前端拦下
回执方式 Radio — none / confirm / fill,见 3.3
回执单据 Select(单据类型) fill 时 ✅ 只选类型,字段由系统预置模板带出,不可增删改;下方为家长端表单只读预览
接收宿舍 楼栋→楼层→房间勾选弹窗 ✅ 或勾「全部宿舍」(receiveList=ALL)
宿管配合 Switch — 需宿管到场配合执行时开启
置顶展示 Switch — 家长端列表置顶并标「顶」
短信提醒 Switch — 需学校已开通短信
附件 AttachmentUpload — 图片/文档,走 /api/base/FileUpload
通知内容 wangEditor 富文本 — 图片走 /api/file/UploadImage

3.2 接收宿舍选择器(本模块独有)

弹窗左侧楼栋列表(名称 / 房间数 / 入住 N/M),右侧按楼层分组的房间勾选网格,每间显示 房号(已住/总床位);支持「本层全选」「本楼全选」「清空」。底部实时汇总 N 间 · M 名住宿生。 表单区回显为可读摘要:6 间宿舍 · 42 名住宿生|1号楼 305、1号楼 306、2号楼 …。

校验要点:receiveList 挂 type: "array", min: 1,「全部宿舍」勾选时清空手工选择并以 ALL 提交,保证校验能通过(模板里「全部班级」的同一套处理方式)。

3.3 三档回执方式(家校闭环的核心)

值 家长端行为 适用通知类型
none 仅通知 阅读即知悉,不产生回执记录,只统计已读 作息调整、停水停电、卫生检查、违规通报、假期安排
confirm 需确认 打开通知详情即自动确认,无需点击;仅在自动确认失败时给手动兜底按钮 安全排查、夜间查寝、宿舍调整(知悉类)
fill 需填报 家长按统一回执单提交(公共区固定 + 专属区随单据类型),截止前可修改,也可取消 留宿申请、家长探视、物品清退、宿舍调整(需回执搬迁日期)

为什么 confirm 要「打开即确认」:让家长为「已阅」多点一次按钮,实际结果是没人点、管理端拿不到真实触达数据。打开即确认把「已读」升级为「已确认知悉」,且顺手省掉一次点击。实现上在 open() 内触发一次 autoConfirmIfNeeded(),提交后只刷新左侧列表与分桶计数、不重新拉详情,避免自我循环。

3.4 统一回执单(公共区 + 专属区)

「需填报」不是自由表单,而是一张统一回执单,分两个区块:

区块 内容 是否随单据变化
公共区 学生(花名册带出) / 家长姓名 / 联系电话 / 回执结论(同意 / 不同意 / 需沟通) ❌ 所有单据完全一致
专属区 单据登记项(留宿起止、到访时段、搬迁日期等) ✅ 随单据类型 formType

四类预置单据:

单据 专属区字段 适用
stay 留宿申请单 留宿开始日期* / 留宿结束日期* / 留宿人数*(人) / 留宿事由*(家远不便往返、赛前集训、伤病康复观察、学业补差、其他) 周末 / 假期留宿申请
visit 探视预约单 到访日期* / 到访时段*(上午 / 下午 / 晚间) / 到访人数*(人) 家长到校探视预约
adjust 调宿申请单 可配合搬迁日期* / 情况说明 床位重排、宿舍调换确认
general 通用回执单 无(仅公共区) 只需家长给结论的场景

发布人只能选单据类型,不能增删字段:字段清单由后端按 formType 生成(buildFormFields()),发布页表单只上传 formType,不传字段定义。发布页下方为只读预览(公共区 chips + 专属区 chips),单据类型由 GET /api/notice/FormTemplates 下发(结构见 ReceiptFormTemplate)。

为什么不做自由表单:自由拼字段会让每条通知的字段名、字段数、表头各不相同 —— 回执落成 fillData 的 JSON 后无法跨通知合并统计,导出的 CSV 表头也对不上。而本模块的回执只有 confirmed / filled / canceled 三态、没有审批下游,字段结构化的意义只剩「汇总」;既然只为汇总,口径就必须固定。

字段模型: FillField = { key, label, type, required, placeholder?, options?, unit?, group },其中 group ∈ common|specific,type ∈ text|number|date|datetime|select|textarea。 家长端按 group 分两段渲染(先公共区、后专属区),按 type 渲染控件,select 回显时把 value 反查为 label。 提交时按后端口径统一转成字符串 Map(Map<String,String>),避免数字/日期序列化口径不一致。

3.5 通知流程状态

draft 草稿 → published 已发布 → closed 已截止 → finished 已结束。 草稿不产生回执记录、家长端不可见;撤回(UserPull)把状态置为 closed 并标 isPulled=1,家长端不再展示,但已提交的回执记录保留以便追溯。


四、回执情况页

  • 顶部:通知选择(仅列出 receiptMode != none 且非草稿的本部门通知)+ 状态过滤(全部/已确认/已填报/已取消/未回执)+ 催办 + 导出。
  • 汇总条:应收(住宿生)/ 已回执 / 已确认 / 已填报 / 已取消 / 未回执 / 回执率 / 覆盖宿舍数 / 覆盖床位数。
  • 名单:按 楼栋名 · 房间号 分组,组内每名学生一行 —— 学生 / 床位 / 家长 / 电话 / 关系 / 回执结论(公共区,独立成 Tag 列)/ 单据登记项(专属区字段展开)/ 状态 / 回执时间 / 备注。
  • 行操作:
    • 回执详情 —— 弹窗展示家长信息 + 按「公共区 / 专属区」两段展开的填报内容 + 已读时间;
    • 代为确认 —— 家长电话口头确认等系统外已告知的情形,管理员代录(需填报类会同时写入结论,避免产生「已填报但内容为空」的空单);
    • 取消/撤回 —— 家长申请不参与(不留宿、不探视)时置为已取消并留痕;
    • 退回未回执 —— 置回 none,家长端可重新提交。
  • 导出 CSV:列顺序固定为 房间/床位/学生/班级/单据类型/家长/关系/电话 + 公共区(回执结论) + 专属区字段 + 状态/时间/备注,带 BOM 供 Excel 直接打开;同类单据导出的列完全相同,可纵向合并。

五、宿舍设置页(3 个 Tab)

  1. 通知类型 —— TypeSetBox 卡片 + vuedraggable 拖动排序;启停状态影响发布下拉,删除不影响历史通知。
  2. 宿舍楼栋房间 —— 左侧楼栋台账(房间数、入住率、宿管),右侧该楼房间表格(房号 / 楼层 / 入住 N/M 床(满)/ 宿舍长 / 宿管)。
    • 新增楼栋按「楼层数 × 每层房间数 × 每间床位数」批量铺房间,新楼栋按空楼创建(usedBeds=0)——真实流程是先建房间、再由床位安排流程写入住宿生;
    • 新增房间从该层已有房号往后追加,避免重号;
    • 改床位数收紧时,装不下的学生退回待安排池(不再凭空裁掉);
    • 删除房间/楼栋均有保护:房间内仍有学生、楼栋下仍有房间时后端拒绝并给出可读原因。
  3. 自定义组 —— 宿管 / 生活老师联系组,成员从基础平台教师账号中选取(UserCheckModal)。

5.1 房间人员(住宿生)与宿舍长

「人员怎么进房间」在本模块只有一条链路,全部收在房间行的「住宿生/床位」入口里:

动作 效果 约束
安排入住 从「待安排」名单(学籍在册未分配 / 退宿退回 / 转学插班)勾选学生调入本房 不得超过空床位数;按空床位顺序落位;性别与楼栋校验(女生楼不收男生,反之亦然)
调整宿舍 房内学生转到其他房间(可跨楼栋) 目标房需有空床位,满房选项置灰;原房为宿舍长时,身份顺延给同寝第一位
退宿 学生移出房间并退回待安排池(来源标注「退宿待重新安排」) 退宿后床位号自动顺延补齐,不留空洞床位

关键口径:usedBeds 恒等于花名册长度(单一数据源)。早期实现是反过来的 —— 按 usedBeds 反向凭空生成学生,导致新建房间/调大床位会「自动变出不存在的人」。现已收口:房间人数只能由床位安排接口驱动。

宿舍长(isMonitor)显式落在花名册数据上,不再靠「数组第一位」隐式约定:

  • 房间表格「宿舍长」列显示当前宿舍长,未指定显示灰字「未指定」;
  • 每间房至多 1 人,在住宿生名单里点「设为宿舍长 / 取消宿舍长」改派(SetMonitor,取消传 studentId=0);
  • 宿舍长退宿或调出时,身份自动顺延给同寝第一位学生。

待安排学生池(PendingStudents)是安排入住的数据源:初始 46 人(姓名按性别分池,避免「周雨萱(男)」这类错配),配合「来源」列区分新生未分配 / 退宿待重新安排 / 楼栋调整待安置 / 转学插班待安排。


六、接口清单

统一响应 { code, msg, data },code==0 时 alova 拦截器解包为 data。GET 走 query,POST 分两种(postJSON 对齐 @RequestBody,postForm 对齐 @RequestParam)。

基础 / 身份

方法 路径 说明
GET /api/base/GetUserInfo 当前登录用户(教师 / 家长)
GET /api/base/GetUserMod 已授权菜单(modId + page)
POST /api/base/SetRole 端分离切视角(as=teacher/parent)
GET /api/base/GetSchoolAll 教师组织树(联系组选人)
GET /api/base/GetDormTree 楼栋→楼层→房间树(叶子带 bedCount / usedBeds)

宿舍台账

GET /api/dorm/BuildingList、GET /api/dorm/RoomList?buildingId=&keyword=、GET /api/dorm/RoomStudents?roomId=(含 isMonitor)、 POST /api/dorm/AddBuilding|EditBuilding|DelBuilding、POST /api/dorm/AddRoom|EditRoom|DelRoom

床位安排 / 宿舍长

方法 路径 说明
GET /api/dorm/PendingStudents?keyword=&gender= 待安排学生池(安排入住的数据源)
POST /api/dorm/AssignStudent 安排入住(roomId + studentIds 逗号串),先整体校验性别/空床位再落位,避免半截数据
POST /api/dorm/RemoveStudent 退宿(roomId + studentId),学生退回待安排池,床位号顺延
POST /api/dorm/TransferStudent 调整宿舍(studentId + fromRoomId + toRoomId),返回 { wasMonitor, bedNo }
POST /api/dorm/SetMonitor 设 / 取消宿舍长(roomId + studentId,取消传 0)

通知

方法 路径 说明
GET /api/notice/SendList 我发布的(scope=user)/ 全部通知;支持 keyword、noticeType、level、buildingId、noticeStatus、时间区间、orderByTime
GET /api/notice/List 家长端收件箱(仅已发布、非未来定时、面向本生房间),额外返回 unreadCount
GET /api/notice/Detail 详情(type=1 标读并同步标记该房间学生的已读;type=0 管理视角不标读)
GET /api/notice/GetDetail 编辑回显(不标读)
GET /api/notice/FormTemplates 单据模板列表(统一回执单选型:公共区 + 各类单据的专属区字段,发布人只能选不能改)
GET /api/notice/GetNextPrev 上一篇 / 下一篇
GET /api/notice/ReadDetail 阅读情况(按楼栋分组,叶子为房间名 + 人数)
POST /api/notice/Create 新建(JSON body,返回新 id)
POST /api/notice/Edit 编辑(JSON body,含 sourceId)
POST /api/notice/Delete 删除(JSON 数组 id,级联删回执)
POST /api/notice/UserPull 撤回
POST /api/notice/SetTopmost 置顶 / 取消置顶
POST /api/notice/Expedite 催办未读
POST /api/notice/ReceiveRooms 追加接收房间(补发)
POST /api/notice/InitializeRed 一键全部已读

回执 / 填报

方法 路径 说明
GET /api/notice/Receipts?serid=&status= 回执名单 + 汇总(按 楼栋 · 房间 分组),含 formType / formTypeDesc / formFields
GET /api/notice/ReceiptDetail?receiptId= 单条回执详情(含 fillData、formType 与 formFields)
POST /api/notice/Review 处理回执(JSON):action=confirm/cancel/reset;需填报类代确认时会写入公共区结论
POST /api/notice/Remind 向未回执家长发送提醒,返回 { reminded }
GET /api/notice/ExportReceipts 导出 CSV 文本(固定列序:固定列 → 公共区 → 专属区 → 回执信息)
GET /api/notice/MyNotices 家长端通知箱(含 buckets: {todo, done, finished, all})
GET /api/notice/MyNotice?serid= 家长端某通知详情(通知信息 + 我的回执 + formType + formFields),打开即标读
POST /api/notice/SubmitReceipt 提交回执 / 填报(JSON:serid, fillData, guardianName, guardianPhone, relation, remark;fillData 为字段键 → 值)
POST /api/notice/CancelReceipt 取消回执

统计 / 字典 / 联系组 / 文件

GET /api/notice/Statis、GET /api/dict/List?type=dept|noticeType、GET /api/dict/Sync、 GET /api/dict/ManageList、POST /api/dict/Add|Update|Delete|Sort、 GET /api/contacts/GetGroup|GetContactsTea|GetGroupTree、POST /api/contacts/AddGroup|EditGroup|DelGroup|SetContactsTea、 POST /api/base/FileUpload、POST /api/file/UploadImage、GET /api/file/Preview|Down、GET /api/notice/AllFileDown


七、mock 数据说明

src/assets/mock/dormapi.ts(约 2200 行)覆盖上表全部接口,要点:

  • 确定性伪随机:seededRand(sin(seed)),同一房间/通知的入住学生、回执率刷新不跳变,适合演示与截图。
  • 楼栋与房间:4 栋楼(1/2 号男生 6+5 层×8 间×8 床,3 号女生 4 层×6 间×6 床,4 号武术特长班 3 层×6 间×4 床)。 roomId = buildingId*10000 + floor*100 + index(如 10305 = 1 号楼 3 层 05 房),房间号即 305。
  • 住宿生花名册:1 床 1 生,含姓名(武校风格,男/女名字分池)、班级(文化班 + 套路表演班/散打竞技班/拳击班混合)、床位号、家长姓名/关系/电话、是否宿舍长 isMonitor(默认首位,可改派)。
  • 待安排学生池:46 人(studentId 段 900001+,与房内学生 roomId*100+i 不冲突),带来源标注;退宿 / 床位缩减的学生回流到此池。
  • 床位一致性:usedBeds 由花名册长度派生,rebuildRoomIndex() 不再按床位数反向生成学生。
  • 通知数据集:覆盖 13 类通知 × 4 种流程状态 × 3 档回执方式 × 3 种紧急程度,含草稿、已撤回、定时未到、已截止等边界样本。
  • 回执生成:按通知状态给不同回执率(已结束 ~95%、已发布 ~60%、已截止 ~85%),已结束类回执时间落在截止日前;填报内容按 formFields 逐字段造合理值(留宿起止、探视时段、搬迁日期等),并派生 progressOf() 汇总(应收/已确认/已填报/已取消/未回执/回执率/房间数/床位数)。
  • 家长端演示身份:ME_ROOM = "10305"(1 号楼 305 室第一位学生),家长端只看该房间命中的通知;?serid= 打开即标读。
  • 写操作真实生效:Create/Edit/Delete/Review/SubmitReceipt/CancelReceipt/ReceiveRooms 以及宿舍台账的 Add/Edit/Del、床位安排的 Assign/Remove/Transfer/SetMonitor 都会改内存数据并同步重建回执,页面刷新后行为一致(重启 dev server 复位)。

八、沿用模板的既有约定(踩坑记录)

这些是模板项目已经踩过并写进注释的坑,本模块原样继承:

  1. <FormItem required> 必须配 prop —— 否则字段名被解析成 undefined,model 取到 undefined,家长填了值也照样报错、表单永远提交不了。
  2. 数字字段规则必须显式写 type: "number" —— async-validator 只在规则「除掉 message 后只剩 required」时走 required 校验器;一旦带 trigger,会回退到 type: "string",InputNumber 给出的数字被判类型不符,错误文案还被自定义 message 掩盖,极具误导性。
  3. 表单模型是稳定的 reactive 对象,不能写成 computed(() => ({...})) —— 每次依赖变化生成新对象,而 FormItem 校验读的是 Form.model,props 要等父组件重渲染才更新,同步 change 校验拿到的还是旧对象(「选了却红字不消失」)。
  4. rules 写成普通常量/computed 要分清场景 —— Form 内有 watch: { rules () { this.validate() } },引用一变就全量重校验。
  5. 存草稿只校验标题,并用 clearFieldState() 清掉其它字段的残留红字。
  6. postForm 同时把参数拼进 URL query —— 后端 @RequestParam 收得到,且 vite-plugin-mock 不解析 urlencoded body,靠 query 才能拿参。
  7. 面包屑/菜单高亮靠 meta.parentId,发布通知/详情不在菜单树里也能正确高亮父菜单「我发布的」。
  8. menuRoute.ts 做模块配置兜底解析(平台模块 path 格式不可控),NAME_ALIASES 已按宿舍口径改写,并兼容旧 /notice、/dorm、/activity 前缀。

九、后续接入后端的改造点

  1. vite.config.ts 的 /api 代理 target 换成本模块后端上下文(现仍指向模板的 familyactivity)。
  2. src/assets/config/menu.ts 是本地菜单兜底,接入后端 GetUserMod 后以接口返回为准(modId 契约见路由表)。
  3. src/assets/mock/index.ts 是 vite build 的 prod mock 入口,正式构建不启用 mock(localEnabled: mode === "mock")。
  4. GetDormTree 已按 ztree 结构返回,若后续要用 ztree 选房间(而非当前的自绘楼栋+楼层勾选),可直接接入 src/components/MyTree*.vue。