Files
CampusnoticeFront/执行计划任务方案.md
lijing a48c5f6343
build / build (push) Successful in 13m15s
fix: 修复日期筛选与表单参数双写、下线范围预估,并改造列表三列样式
- v29 删除前端对 notice/ScopePreview(推送范围「已选学员数」预估)的请求:
  api 方法、ScopePreviewVo 类型、dev:mock 桩、ClassScopeInput/PushScopePanel 两个组件的预估 UI 一并移除。
- v30 修复发文部门/联系组名称重复且带逗号(如「校园部,校园部」并逐次翻倍):
  postForm 原先把参数同时拼进 URL query 与 urlencoded body,Servlet 会合并两处取值、
  Spring @RequestParam String 再以「,」拼接。改为只发 body,仅在 dev:mock 下附带 query
  (vite-plugin-mock 不解析 urlencoded body),不影响线上。
- 修复时间筛选失效:DatePicker 的 Date 实例被 alova 直接 encodeURIComponent 成浏览器本地化
  日期串(Fri Aug 21 2026 ... GMT+0800),后端按 yyyy-MM-dd 解析不了。列表侧提交前归一化为
  短日期,alova-core 另加一层 Date → 本地 'yyyy-MM-dd HH:mm:ss' 的兜底归一化。
- 列表「类型 / 状态 / 阅读情况」三列改醒目样式(口径不变,仅样式):
  类型改胶囊(重要=实心红、普通=浅灰弱化);状态改彩色圆点胶囊,已发布绿/定时中蓝/
  待审批橙/已驳回红/草稿灰,已撤回、置顶改实心小徽标;阅读情况改渐变进度条 + 已读/应读·百分比,
  读满转绿。同步微调类型(80→90)、阅读情况(110→180)列宽。
- 同步更新《执行计划任务方案.md》v29 / v30 记录。
2026-09-22 09:46:33 +08:00

180 KiB
Raw Permalink Blame History

通知公告前端改造成「校园通知」执行计划任务方案(v2 · 2026-09-13 检查修订)

工程:前端 E:\YHXT\view\CampusAnnouncement(Vue3 + iview + alova + vite);后端 E:\YHXT\java\CampusNotice 参照物:http://localhost:8101/api-test.html(校园通知测试台,6 视图信息架构) 决策基线:对齐 api-test 能力 + 全站「通知」术语;保留 iview 左侧菜单框架;接口契约以后端 Controller 实际定义为准。

本版用途:v1 方案(信息架构/术语/API 扩展/页面清单)已大面积落地。本轮对已改造部分做全面错误检查——进度盘点、错误清单 E1–E8、修复计划与回归清单;修复结果回填 §七。


一、改造进度盘点(对照 v1 差距清单)

v1 待办项 状态 说明
术语全站切换 ✅ 完成 views/components 零残留;layout 菜单名映射表内旧名是映射源,应保留 → v10 已删除该映射表,栏目名以接口 name 为准
菜单契约风险(GetUserMod 数据不可控) ⚠️ 定论已被 v10 推翻 原方案:layout.vue MENU_NAME_MAP 展示层改名 + utils/menuRoute.ts 解析器(path 精确→模块名别名→外链→modId 兜底),配错显式提示不静默跳默认页。现方案见 v10:改名层移除,仅保留 menuRoute 解析器(仍按原始 name/modId 兜底)
草稿箱独立页 ✅ 完成 DraftList.vue(搜索/续编/提交审批弹窗/删除);菜单由 layout 注入(id 3100,挂「已发管理」后)
API 扩展 SaveDraft/DraftList/Submit/ScopeTree/ScopePreview/ReadReceipt/Approve+opinion ✅ 完成 notice.ts 已封装,类型对齐后端
写通知:三态动作区/回执勾选/推送范围树+预估/多步审批流 ✅ 基本完成 PushScopePanel + ApprovalFlowEditor(审批人池数据源、会签/或签、上移下移);教师接收名单缺失 → E3
已发管理:状态/类型/部门/日期筛选、审批进度、定时中标注、驳回重提、阅读回执弹窗 ✅ 完成 NoticeList.vue
审批中心:四 Tab/待审角标/多步进度 ✅ 完成 layout 60s 轮询 ApprovalCount 角标;审批意见列死字段 → E6
我的通知:未读已读分段/类型/搜索/置顶重要徽标/回执提示 ✅ 完成 NoticeAccept.vue
详情:审批轨迹+每步流程进度+四态徽标 ✅ 完成 NoticeDetails.vue;三处字段后端不返回 → E4/E5
审核设置(审批人池 CRUD) ⚠️ 有错误 页面完成,列表接口路径写错 → E1
旧路径兼容/重定向 ✅ 完成 routes.ts 别名段保留

编译级验证:npm run build 通过;vue-tsc --noEmit 与本机 Node/TS 版本不兼容无法运行(工具链问题,非代码错误),类型契约以下一轮人工比对为准。


二、错误检查方法

  1. 编译层:vite build;
  2. 契约层:逐接口比对 NoticeController/DictController/BaseApiController/FileController 的 @Mapping 与 @RequestParam/@RequestBody 参数名 vs 前端 api/module/notice.ts 的 URL/方法/参数;
  3. 数据层:比对 NoticeVo/NoticeDetailVo/SubmitRequest/FlowStepItem 实际返回字段 vs 页面渲染引用与 types/notice.ts 声明(前端类型里存在而后端不返回的字段=死引用);
  4. 交互层:对照 api-test.html 六视图的表单字段与操作语义,找缺失能力。

三、检出错误清单

# 位置 现象 根因 修复
E1 notice.ts approvers() 审核设置页审批人列表加载失败 调 dict/Approvers,后端为 dict/Approver(单数) 前端路径改 /Approver
E2 NewNotice.vue buildBody 「存为草稿」丢失已配置审批流 → 草稿箱「提交审批-沿用草稿已存流程」必失败 flow 仅提交审批动作携带,draft 分支不带(后端 SaveDraft 支持存流程定义) draft 分支同样携带 flow
E3 NewNotice.vue 无「教师接收名单」:不能只通知教师;ScopePreview 教师数恒 0;发布被强制要求选学员范围 参照 api-test cReceivers 面板未实现(后端 receiveList 支持逗号串/JSON/ALL) 新增表单项;buildBody 带 receiveList;校验改「范围或名单二选一」;联动 PushScopePanel 预估;编辑回显(ALL 或 JSON 数组型)
E4 NoticeDetails.vue 驳回/通过 Alert 审批人/时间/意见显示空 用了后端不存在的 approvalBy/approvalTime/approvalRemark 改从 approvalRecords 取末条 action=3/2(operatorName/actionTime/opinion)
E5 NoticeDetails.vue 接收班级标签 恒显「全部班级」 用了不存在的 receiveText 按 pushScopes 名称拼接;ALL→全体教师;旧链路兜底;标签改「接收范围」
E6 ApprovalList.vue 审批意见列 恒显「—」 NoticeVo 无 approvalRemark(意见仅详情轨迹返回) 删除该列;types 同步清理死字段
E7 后端 DictController 「发送短信提醒」勾选永不出现(404 被 catch 静默) 前端约定 dict/Sync;后端 service 有 getSchModuleIsSms() 但控制器漏暴露 后端补 @GetMapping("Sync")(3 行;需重启生效;现实现固定 false 属平台占位,UI 行为不变)
E8 ClassCheckInput.vue/GradeClassSel.vue 无运行影响 死代码,引用后端不存在的 base/GetClassTree 仅登记,后续清理删除

已知限制(不列入本轮修复):旧链路班级通知编辑回显依赖的 detail.receiveType 后端 Detail VO 未返回,旧数据续编范围不回填;如需支持由后端补字段。


四、修复执行顺序

  1. 前端小修:E1(1 行)→ E2(1 行)→ E6(删列)+ types 死字段清理;
  2. 前端中修:E4/E5(NoticeDetails computed)→ E3(NewNotice 教师名单 + 校验 + 回显 + 预估联动);
  3. 后端补口:E7(DictController.Sync)+ mvn compile 验证(JAVA_HOME=D:/Program Files/Java/jdk-1.8,mvn=D:/Program Files/Java/maven/apache-maven-3.6.1/bin/mvn.cmd);
  4. 回归:npm run build 重跑 + §五清单自检,结果回填 §七。

五、回归验证清单

  • E1:审核设置页列表加载/新增/停用/删除正常
  • E2:写通知→配两步审批→存为草稿→草稿箱提交审批(不重选人)→ 已发管理进度「第1/2步…」
  • E3:只填教师名单不选范围可发布;范围+名单预估合计正确;ALL 全员;草稿续编回显
  • E4:已驳回单详情意见/人/时间正确;通过单显示最后通过人
  • E5:范围单「接收范围:xx、xx」;教师单「全体教师」;旧单「全部班级」
  • E6:审批中心四 Tab 正常、无死列
  • E7:重启后 GET /api/dict/Sync code=0
  • 旧链路回归:收件箱/一键已读/撤回/置顶/转发/附件/打印 不受影响
  • npm run build 通过;主链路冒烟(Create direct/approve、Submit、多步 Approve、Reject、Delete)

六、风险与边界

  • E7 需 IDEA 重启服务生效(服务跑在 Debug 模式,脚本不代重启),本轮仅保证编译通过;
  • E3 回显保守:仅 ALL 或 JSON 数组型 receiveList 回填,防旧班级 id 串误显;
  • 全部修复不动鉴权/布局/alova 基建;types 仅删后端证实不返回的字段。

七、修复完成记录(实施时回填)

项 状态 备注
E1 ✅ 2026-09-13 notice.ts approvers() → dict/Approver
E2 ✅ 2026-09-13 NewNotice.vue draft 分支 buildBody 携带 flow
E3 ✅ 2026-09-13 教师名单表单项 + receiveList 入 body + 「范围/名单二选一」校验 + 预估联动 + 编辑回显(ALL 或 JSON 数组型)
E4 ✅ 2026-09-13 NoticeDetails.vue 驳回/通过 Alert 改取 approvalRecords 末条记录
E5 ✅ 2026-09-13 「接收范围」标签按 pushScopes/ReceiveList 计算(receiveLabel)
E6 ✅ 2026-09-13 审批中心删除审批意见死列;types 清 approvalRemark/approvalBy/approvalTime/receiveText
E7 ✅ 2026-09-13 DictController 补 GET Sync,mvn compile 通过;需 IDEA 重启服务生效(当前 service 固定返回 false,勾选仍隐藏属平台占位预期)
E8 📝 登记 ClassCheckInput/GradeClassSel 死代码,后续清理删除
构建回归 ✅ 2026-09-13 npm run build 通过(仅 chunk 体积警告,非错误);死字段引用清零

v3(2026-09-14)· 写通知业务逻辑迁移三项

需求(参照 api-test 写通知视图):

  1. 推送范围(学员)接口更换,并显示学员选中人数;
  2. 去除教师接收名单(撤销 v2-E3 在写通知中的名单输入,仅移除发布表单入口,旧数据详情展示不动);
  3. 选择审批人接口更换:由「审批人池(dict/ApproverPool 账号路径)」改为「教师树(base/getTeaTree 教师ID路径)」。

一、契约核对(后端实况,均已确认支持)

能力 api-test 用法 后端契约 前端现状(待改)
学员范围树 GET base/getStuTree?toUser=false,节点 {id,name,type(1组/2叶子),children} 自底向上归类 scopeType(叶=4 班级→父=3 年级→再上=2 学部→根强制=1 全校) BaseApiController.getStuTree(SchTreeNodeDto 树) PushScopePanel 走 notice/ScopeTree(后端已拼好树,但需求要求统一换 getStuTree 口径)
选中学员数 GET notice/ScopePreview?scopes=… → studentCount NoticeController.scopePreview(scopes, receiveList?),receiveList 可空 已有预估行「学员/教师/合计」,含教师定向口径
审批人选人树 GET base/getTeaTree?toUser=true,叶子 id=teaId BaseApiController.getTeaTree ApprovalFlowEditor 走 dict/ApproverPool + UserCheckModal 账号
流程提交 每步 {name,vote,approverTeaIds:[…]} FlowStepItem.approverTeaIds 与 approvers 二选一;resolveRequestFlow 同时服务于 Create.flow 与 Submit.steps;服务端 bindApprovers 按校教师名单绑定姓名 前端每步传 approvers:[账号]
记忆带入 notice/ApproverSuggest → teaIds;notice/ApproverResolve?ids= → {id,name} 姓名解析 两接口均存在 Suggest 结果靠池内 teaId 映射账号;Resolve 未封装
编辑回显 详情 approvalFlow[].approverTeaIds/approverNames/vote ApprovalFlowVo 四字段齐备 回显走 approvers 账号列表

二、修改计划

  1. API 层 api/module/notice.ts:baseApi 增 getStuTree/getTeaTree;noticeApi 删 scopeTree()、增 approverResolve(ids);approvalSettingApi 删 approverPool()(仅编辑器使用;审核设置管理页走 approvers() 不受影响);scopePreview 保留(receiveList 变可选、不再传)。
  2. 类型 types/notice.ts:新增 SchTreeNode(getStuTree/getTeaTree 节点);NoticeRequest.flow 项改 {name?,vote,approverTeaIds};ScopeTreeNode 注释改为「面板视图节点(由 getStuTree 归类而来)」。
  3. PushScopePanel.vue:数据源换 base/getStuTree?toUser=false + toScopeNode 归类(与 api-test 同算法,toUser=false 无学生叶子、防御性跳过 type≠1);删 receiveList prop 及预估联动;预估改为「已选学员 N」(N=ScopePreview.studentCount)。
  4. NewNotice.vue:删「教师名单」表单项、form.receiveList、buildBody receiveList、编辑回显 ReceiveList 块;校验恢复「必须选择推送范围」;PushScopePanel 去 prop;审批流回显改 approverTeaIds/approverNames;编辑态 Submit steps 改传 approverTeaIds。
  5. ApprovalFlowEditor.vue(重写选人):内联教师树选择面板(搜索过滤+命中自动展开、组节点不可勾、教师叶子可多选、同教师跨组出现勾选态同步、完成选择回填当前步);步骤模型 {vote, teaIds, names};勾选启用时/手动按钮带入 ApproverSuggest 记忆(经 ApproverResolve 解析姓名,未命中降级 ID);对外 v-model flow=有效步 {vote, approverTeaIds}。
  6. DraftList.vue:提交审批 steps 改传 approverTeaIds;「沿用草稿已存流程」逻辑不变(不勾选=steps 缺省)。
  7. 回归:npm run build;对照 api-test 人工走读契约。

已知限制:旧「账号路径」草稿/驳回单(approvalFlow 仅有 approvers 无 teaIds)在新编辑器中不回显,保存后流程按新 selections 覆盖(api-test 同口径);如需兼容由后端补 teaId 反查。

三、回归验证清单(v3)

  • 写通知:范围树来自 getStuTree,勾选全校/学段/年级/班级出 chips,「已选学员 N」随勾选变化
  • 写通知:无「教师名单」输入;不选范围直接发布/提交审批被拦截
  • 写通知:勾选审批→教师树选人(搜索/多选/多组同步);带入上次审批人经 Resolve 显示姓名
  • Create/Edit/Submit 请求体 flow/steps 为 {name,vote,approverTeaIds};提交后审批中心可正常审批
  • 编辑回显:教师树审批流+姓名正确;草稿箱提交审批(重选/沿用)正常
  • 旧数据:详情「接收范围/全体教师」标签不受影响;审核设置页 CRUD 不受影响
  • npm run build 通过

四、实施记录(v3 回填)

项 状态 备注
1 API 层 ✅ 2026-09-14 baseApi.getStuTree/getTeaTree;noticeApi 删 scopeTree()、增 approverResolve(ids);删 approverPool();scopePreview 保留(receiveList 缺省空)
2 类型层 ✅ 2026-09-14 新增 SchTreeNode;NoticeRequest.flow 与 SubmitRequest.steps 改 {name?,vote,approverTeaIds};receiveList 字段标注「发布表单已下线」
3 PushScopePanel ✅ 2026-09-14 数据源 base/getStuTree?toUser=false + toScopeNode 自底向上归类(同 api-test);删 receiveList prop;预估区改「已选学员 N」(ScopePreview.studentCount,防抖 350ms,无勾选不显示)
4 教师名单去除 ✅ 2026-09-14 NewNotice 表单项/form.receiveList/buildBody 组装/编辑回显(ALL·JSON 串回填)全删;校验恢复「请选择推送范围!」;详情 receiveLabel 与旧数据展示未动
5 ApprovalFlowEditor ✅ 2026-09-14 重写为教师树路径:Modal 内联树(搜索命中过滤+自动展开、组不可勾、叶子多选、同教师跨组按 teaId 同步、完成选择回填当前步);步骤模型 {vote,teaIds};带入上次=ApproverSuggest→ApproverResolve(未命中降级 ID);对外 flow={name,vote,approverTeaIds}
6 NewNotice 审批流 ✅ 2026-09-14 回显改 approvalFlow[].approverTeaIds/approverNames;编辑态 Submit steps 传 approverTeaIds;Create/Edit flow 体同步
7 DraftList ✅ 2026-09-14 提交审批弹窗 steps 传 approverTeaIds;不勾选=steps 缺省沿用草稿已存流程(不变)
构建回归 ✅ 2026-09-14 npm run build 通过(40.2s,仅 chunk 体积警告);残留引用清零(approverPool/scopeTree/form.receiveList 无引用);ApproverResolveVo{id,name,photo}/ApproverSuggestVo{teaIds} 字段人工比对一致

待人工冒烟(需登录环境):§三 v3 清单逐条过;后端 notice/ScopeTree 接口本轮起前端不再调用(保留未删,其他端或仍用)。


v4(2026-09-14)·「审批列表菜单消失」排查登记(平台侧问题,前端零改动)

一、现象与结论

  • 现象:左侧菜单无「审批中心」「审核设置」入口,其余菜单正常。
  • 结论:非代码问题。基础平台用户中心对该校全员不再返回模块 id=5(审批列表)与 id=6(审核设置),GetUserMod 透传后前端菜单随之消失。

二、排查证据链

# 层面 核查结果
1 前端路由/菜单 /approval(modId 5)、/approval-setting(modId 6) 路由、menu.ts 兜底、MENU_NAME_MAP(审批列表→审批中心)、图标角标逻辑全部完好;v3 改造未触碰菜单/layout
2 后端 CampusNotice BaseApiController.GetUserMod 仅透传 YschBaseOpenApi.getUserModule(userId, schId),无过滤
3 远程用户中心实况 直查 base.yunhaixingtong.com/userbase /oapi/user/modules(复刻平台签名,脚本 .scratch/oapi-call.mjs):云海中学(schId=3) 全部已知账号(lijing=10008、王虎=10007、陈星1=10006、测试教师2=10045、ceshiteacher=125287)均只返回 1/2/3/21/4 五个模块,5/6 全员缺失
4 时间线线索 平台 2026-09-10 新建「校园公告」应用(appKey 100006),疑似其后有人调整过应用/模块/角色配置导致丢失

三、修复方案(平台侧操作,前端不用发版)

  1. 基础平台管理后台 → 应用管理 → 「通知公告」应用 → 模块管理:确认两模块存在且启用——审批列表(path /approval,前端显示「审批中心」)、审核设置(path /approval-setting);
  2. 角色权限中为对应角色(老师/管理员)重新勾选这两个模块;
  3. 恢复授权后菜单即时回来(前端已有名称映射+路径解析兜底)。

备选:平台侧无法及时修改时,可仿「草稿箱」由 layout 前端注入这两个菜单项临时兜底(偏离“菜单以平台授权为准”契约,需产品确认后实施)。

四、验证方式

恢复后执行 node .scratch/oapi-call.mjs "/oapi/user/modules" "userId=10008&schId=3",确认返回列表包含 id=5、id=6;或直接刷新前端页面观察左侧菜单。

五、实施记录

项 状态 备注
排查登记 ✅ 2026-09-14 本节;前端/后端零代码改动
平台侧重新授权 ⏳ 待平台管理员操作 模块 5/6 挂回应用并授权角色
复验 ⏳ 待平台操作后 按 §四 验证

v5(2026-09-14)· 发公告「推送范围」改为「接收班级」二选一(全部班级 / 指定班级)

需求(参照截图,旧链路 new_notice 样式):发公告表单的范围选择由「全校/学段/年级/班级」多级树面板(PushScopePanel)改为:

  • 单选 全部班级 / 指定班级;
  • 指定班级 → 只读输入框(placeholder「选择通知的班级」)+「+」按钮打开选择用户弹窗(左侧年级→班级树勾选、右侧已选列表全选/计数、提交/取消),垃圾桶按钮清空;
  • 表单项 label 由「推送范围」改为「接收班级」。

一、契约核对(后端实况,均以源码确认)

# 结论 依据
1 后端零改动:全部班级 → pushScopes=[{scopeType:1, scopeId:0, scopeName:"全部班级"}];指定班级 → [{scopeType:4, scopeId:classId, scopeName:className}…] PushScopeTypeEnum:SCHOOL(1) 走 getSchStuList(schSerId) 全校(scopeId 不参与解析,api-curl-test.sh 亦用 scopeId=0 验证);CLASS(4) 走 getClaArrStuList 展开
2 弹窗班级树数据源 = base/getStuTree?toUser=false,前端自底向上归出「年级→班级」两层(学校/学段层丢弃) 后端无 base/GetClassTree(v2-E8 登记属实,ClassCheckInput 的旧数据源不可用);BaseApiController.getStuTree 返回组节点树
3 mock 缺 getStuTree(v3 换源后未补),本轮一并补 mock(学校→学段→年级→班级),保持 mock 模式可用 mock/noticeapi.ts 仅有 GetClassTree/GetSchoolAll
4 编辑回显兼容:pushScopes 中 type=2/3(旧多级面板选的学部/年级)在树中展开为其下属班级;type=5/6 等无法映射项原样保留随表单提交,防丢范围 对齐 PushScopePanel.applyScopes「树外项保留」口径
5 旧链路数据(receiveType=class):tlist 班级 id 串 → type=4 项(树加载后解析真实班级名);tlist="all" → 全部班级 NewNotice 既有映射逻辑延续

二、修改计划

  1. 新建 src/components/ClassScopeInput.vue:v-model=PushScopeItem[];radio 全部班级(默认)/指定班级 + 只读输入框 + 加号(开 UserCheckModal,数据源 getStuTree 自归类的年级→班级树)/垃圾桶(清空指定班级);弹窗提交空选=回落全部班级(旧链路口径);指定班级下方保留「已选学员 N 人」预估(ScopePreview,防抖,v3 能力不回退);暴露 setScopes() 供编辑回显(含 type 2/3 展开与名称解析,树未就绪时缓冲)。
  2. NewNotice.vue:PushScopePanel → ClassScopeInput,label 改「接收班级」;回显三分支改走 setScopes(pushScopes / receiveType=class 班级串 / tlist=all→全部班级);发布校验文案改「请选择接收班级!」。
  3. mock/noticeapi.ts:补 /api/base/getStuTree(toUser 参数按组节点树返回,由 gradeClasses 派生)。
  4. PushScopePanel.vue 退役(保留文件不删,文档登记);ClassCheckInput/GradeClassSel 死代码登记维持 v2-E8 口径。
  5. 回归:npm run build + 本节清单人工冒烟。

三、回归验证清单(v5)

  • 新建公告:默认「全部班级」,直接发布成功,详情接收范围显示「全部班级」
  • 切「指定班级」自动开弹窗:年级树勾选级联、右侧全选/计数、搜索班级名可用;提交回填班级名顿号串
  • 指定班级清空后发布被拦(「请选择接收班级!」);弹窗提交空选回落全部班级
  • 编辑回显:v3 单(全校/年级/班级混合)→ 对应 radio 态与班级名正确;旧链路班级串单 → 指定班级 + 真实班级名;tlist=all 单 → 全部班级
  • Create/Edit 请求体 pushScopes 为 scopeType 1/4 组合,后端详情/收件范围/应读人数正确
  • mock 模式(isMock)下弹窗树可加载
  • npm run build 通过;其余表单项(附件/审批/定时/草稿)不受影响

四、实施记录(v5 回填)

项 状态 备注
1 ClassScopeInput ✅ 2026-09-14 新建 components/ClassScopeInput.vue:radio 全部班级(默认)/指定班级 + 只读框 + 加号弹窗/垃圾桶清空;树=base/getStuTree 自归类「年级→班级」;空选提交回落全部班级(旧链路口径);setScopes 含 type 2/3 展开为班级、type 5/6 原样保留(extras)、树未就绪缓冲;「已选学员 N 人」预估保留(ScopePreview)
2 NewNotice 接入 ✅ 2026-09-14 PushScopePanel → ClassScopeInput;label 改「接收班级」;回显三分支(pushScopes 原样 / receiveType=class 班级串→type4+树解析真实班级名 / tlist=all→全部班级);转发空范围默认全部班级;校验文案「请选择接收班级!」
3 mock getStuTree ✅ 2026-09-14 补 /api/base/getStuTree(学校→学段→年级→班级组节点树,由 gradeClasses 派生);已知问题登记:mock 仍缺 base/getTeaTree(v3 审批选人换源后未补),mock 模式下审批流选人不可用,后续补
4 PushScopePanel 退役登记 ✅ 2026-09-14 文件保留未删(无任何 import 引用,不参与构建产物);如需恢复多级树面板可直接复用
构建回归 ✅ 2026-09-14 npm run build 通过(15.68s,仅 chunk 体积警告,与历次一致);PushScopePanel/推送范围 残留引用仅剩注释与退役文件本身

待人工冒烟(需登录环境):§三 v5 清单逐条过(重点:真实 getStuTree 数据下年级归类是否正确、后端 Create/Edit 全部班级与指定班级的应读人数)。


v6(2026-09-14)· 审批人选人弹窗改用「选择用户」穿梭式(UserCheckModal)

需求(参照截图):发送通知「审批 → 选择审批人」当前用的是 v3 重写的单栏内联教师树 Modal(标题「选择审批人(可多选)」)。改为复用 UserCheckModal.vue 的「选择用户」双栏穿梭式弹窗:左侧组织树(带复选框、父子级联)+ 中间箭头 + 右侧已选列表(全选/计数/删除选中)+ 顶部名字搜索 + 提交/取消。审批人数据源不变,仍走教师树 base/getTeaTree?toUser=true(叶子 id=teaId)。多步审批、或签/会签、记忆带入、编辑回显等能力不回退。

一、契约核对

# 结论 依据
1 UserCheckModal 可直接复用:props sources:[{name,loader}](loader 返回 ztree 树)、userids:number[](回显勾选);emits on-submit 返回 [{id,name,group}](id=真实 teaId,经 getSplitId 还原) 组件已用于「接收班级」弹窗,formatTree 无 isParent 时按有无 children 判组/叶子(教师叶子无 children→type 2)
2 单数据源(教师树)→ sources.length===1,Tabs 不渲染,与截图一致(截图无 tab 栏,仅搜索框+双栏) UserCheckModal 模板 v-if="sources.length>1"
3 提交后按步回填:steps[editingStep].teaIds = 返回 ids,姓名写入 teaNames 供 chip 展示;对外 flow 仍 {name,vote,approverTeaIds} 现 collectFlow/emitChange 不变
4 依赖 jQuery zTree + requireImg 已由「接收班级」弹窗全局引入,无需新增依赖 UserCheckInput/ClassScopeInput 已用
5 记忆带入(ApproverSuggest→ApproverResolve)、编辑回显(setFlow+names 兜底)、多步增删/上下移、dropApprover 逐 chip 删除——均与选人弹窗解耦,保持不动 现组件逻辑
6 已知限制延续 v5:mock 缺 base/getTeaTree,mock 模式下审批选人弹窗树加载失败(真实环境正常);本轮不扩范围补该 mock v5 §四登记

二、修改计划(仅 ApprovalFlowEditor.vue)

  1. 引入 UserCheckModal;模板中删除内联 <Modal title="选择审批人(可多选)">…</Modal> 整块,替换为 <UserCheckModal ref="pickerRef" :sources="pickerSources" :userids="editingTeaIds" @on-submit="onPickerSubmit" />。
  2. pickerSources = [{ name: "教师", loader: () => baseApi.getTeaTree(true).send() }];editingStep 由 let 改 ref(0);editingTeaIds = computed(() => steps[editingStep].teaIds)。
  3. openPicker(i):editingStep=i 后 pickerRef.value.show();onPickerSubmit(arr):回填当前步 teaIds、写 teaNames、emitChange()(UserCheckModal 提交后自关)。
  4. 删除仅供内联树使用的死代码:rawTree/rows/rowByKey/treeLoading/treeError/expanded/keyword/pickerOpen、buildRows/ensureTree/hitKeys/visibleRows/toggle/isTeacherChecked/onTeacherCheck/closePicker/renderFromSteps,及 SchTreeNode 若不再引用则删;删除 .tea-tree/.picker-search 等相关 scoped 样式。
  5. 保留:teaNames/nameOf/steps/enabled/addStep/removeStep/move/dropApprover/collectFlow/emitChange/validFlow/validate/setFlow/loadSuggest/bindStepByTeaIds/watch(enabled)/onMounted/defineExpose。
  6. 回归:npm run build。

三、回归验证清单(v6)

  • 勾选审批→点「选择审批人」弹出「选择用户」双栏弹窗(无 tab),左=教师组织树可勾选级联,右=已选列表带全选/计数/删除选中,顶部搜索教师名可定位勾选
  • 提交后当前步生成教师 chips(姓名正确);再次打开回显已勾教师
  • 多步:每步独立选人互不串;上移/下移/删除步、逐 chip ×删除、垃圾桶清空 均正常
  • 「带入上次所选审批人」经 ApproverResolve 显示姓名(池外降级 ID)
  • 编辑回显:教师树审批流+姓名正确还原,弹窗内可再编辑
  • Create/Edit/Submit 请求体 flow/steps 仍为 {name,vote,approverTeaIds}
  • npm run build 通过;接收班级弹窗(同组件另一实例)不受影响

四、实施记录(v6 回填)

项 状态 备注
1 弹窗替换 ✅ 2026-09-14 模板删除内联 <Modal title="选择审批人(可多选)"> 整块,改 <UserCheckModal ref="pickerRef" :sources="pickerSources" :userids="editingTeaIds" @on-submit="onPickerSubmit" />;单数据源「教师」→ 无 tab,与截图一致
2 数据源 ✅ 2026-09-14 pickerSources=[{name:"教师",loader:()=>baseApi.getTeaTree(true).send()}],叶子 id=teaId 由 UserCheckModal getSplitId 还原为真实 teaId
3 按步回填 ✅ 2026-09-14 editingStep 改 ref;editingTeaIds=computed(steps[editingStep].teaIds) 回显;openPicker(i) 设步后 pickerRef.show();onPickerSubmit(arr) 回填 st.teaIds=arr.map(id)、写 teaNames、emitChange()
4 死代码清理 ✅ 2026-09-14 删 rawTree/rows/rowByKey/treeLoading/treeError/expanded/keyword/pickerOpen + buildRows/ensureTree/hitKeys/visibleRows/toggle/isTeacherChecked/onTeacherCheck/closePicker/renderFromSteps + SchTreeNode import + .tea-tree/.picker-search 等 scoped 样式;保留多步/或签会签/记忆带入/回显/校验/defineExpose
构建回归 ✅ 2026-09-14 npm run build 通过(17.51s,仅 chunk 体积警告,与历次一致);残留引用清零(grep 内联树符号无匹配);flow 对外契约 {name,vote,approverTeaIds} 不变
5 单栏收敛 ✅ 2026-09-14 追加需求「不需要双栏,只保留左树」:UserCheckModal 新增 showSelected prop(默认 true,接收班级弹窗不变),false 时隐藏中间箭头 + 右侧已选栏(全选/计数/删除选中),树面板满宽、弹窗宽 420px;勾选状态仍由内部 tempUserList 承载,submit 返回逻辑不变;审批弹窗传 :show-selected="false"。npm run build 通过(17.89s)

待人工冒烟(需登录环境):§三 v6 清单逐条过(重点:真实 getTeaTree 数据下双栏弹窗勾选/搜索/全选/计数、多步互不串、编辑回显再编辑、提交后审批中心可正常审批)。已知限制延续:mock 缺 getTeaTree,mock 模式审批选人弹窗树加载失败(真实环境正常)。


v7(2026-09-14)· 审批中心与审核设置:接口与绑定改造(审批人池升级为教师白名单)

核心问题:v3–v6 将写通知审批人选人切换到教师树路径(base/getTeaTree → teaId),但「审核设置」维护的仍是账号路径的 sys_approver 池(account 主键,teaId 字段存在但从未写入)。两条链路完全脱节——审核设置里配的名单写通知选人不消费,任何本校教师都能被选为审批人,池形同虚设;后端 bindApprovers(teaId 路径)只校验教师名单不查池,bindAccounts(账号路径)查池但前端已不再触发。

改造方向(已确认):池升级为教师白名单——审核设置新增审批人改为「从本校教师中选择」,一次落 account/name/deptId/teaId 四件套;写通知选人时教师树只显示池中 enabled=1 的教师;后端 bindApprovers 送审时增加池校验。审批中心增加「当前审批人」列。


一、现状盘点

1.1 审批中心(views/notice/ApprovalList.vue,路由 /approval,modId 5)

维度 现状
接口 GET /api/notice/ApprovalList(参数 pageIndex/pageSize/status/keyword/scope);POST /api/notice/Approve(form serid/opinion);POST /api/notice/Reject(form serid/remark);GET /api/notice/ApprovalCount(layout 60s 轮询角标)
Tab pending 待我审批(默认)/ approved 已通过 / rejected 已驳回 / all 全部
表格列 序号、标题、类型、来源部门、发布人、提交时间、审批进度(approvalStatusDesc Tag)、操作
操作 查看通知(跳详情 type=0);canApprove && pending → 通过/驳回弹窗;pending 但非当前审批人 → 灰字「等待他人审批」
参数契约 前端传 status(旧名),后端主参数 auditState(新名),Controller L359-360 做别名归一——能跑但不对齐新契约
缺失 无「当前审批人」列(卡在谁那里不可见);无「我提交的」视角(发布人无法跟踪自己的单子)

1.2 审核设置(views/notice/ApprovalSetting.vue,路由 /approval-setting,modId 6)

维度 现状
接口 GET /api/dict/Approver(含停用全量);POST /api/dict/AddApprover(JSON {account,name,deptId});POST /api/dict/UpdateApprover(JSON {id,name?,deptId?,enabled?});POST /api/dict/DelApprover(form id);GET /api/dict/List?type=1(部门下拉)
列表列 姓名、账号、所属部门、状态(启用/停用 Tag)、操作(停用/启用、编辑、删除)
弹窗字段 姓名(必填)、账号(可选,手填)、所属部门(Select 必填)
核心缺陷 ① 无 teaId 绑定入口 → sys_approver.tea_id 永远为空;② 账号手填易错且与用户中心不一致;③ 池无消费者——写通知选人走教师树全量,不查池

1.3 写通知选人链路(components/ApprovalFlowEditor.vue,v6 现状)

维度 现状
数据源 UserCheckModal,pickerSources=[{name:"教师", loader:()=>baseApi.getTeaTree(true).send()}]——全量教师树,不过滤池
提交契约 每步 {name?, vote, approverTeaIds:[teaId]}(教师ID路径)
后端校验 bindApprovers(NoticeService L931-969):teaId 按本校教师名单(classService.getSchTeaList)绑定姓名;approverGroupId>0 时加资格组校验(当前=0 未启用);不查 sys_approver 池
记忆带入 ApproverSuggest→teaIds;ApproverResolve→姓名(未命中降级 ID)

1.4 后端审批人池接口(DictController + DictService)

接口 返回字段 teaId 支持
GET dict/ApproverPool(仅启用) id, name, account, deptName, teaId, isParent ✅ 已返回 teaId(L294)
GET dict/Approver(含停用) id, account, name, deptId, deptName, enabled ❌ 不返回 teaId(L302-316)
POST dict/AddApprover body Map<String,String>{account,name,deptId} ❌ 不接 teaId(L320-337)
POST dict/UpdateApprover body Map<String,Object>{id,name,deptId,enabled} ❌ 不接 teaId(L341-355)
POST dict/DelApprover form id —

1.5 后端送审校验双轨(NoticeService.buildSteps L790-839)

路径 触发条件 校验方法 校验强度
账号路径 FlowStepItem.approvers 非空 bindAccounts(L845-874):查 sys_approver 池 enabled=1,池外/停用报错 ✅ 强(池约束)
教师ID路径 FlowStepItem.approverTeaIds 非空 bindApprovers(L931-969):查本校教师名单 getSchTeaList,不在名单报错;approverGroupId>0 时加资格组 ❌ 弱(不查池,任何本校教师均可)

v3 后前端只走教师ID路径 → 池校验被完全绕过。

1.6 saveFlowDefinition(L765-783)与草稿

saveFlowDefinition(草稿/编辑态存流程定义)也调用 buildSteps(L775)→ 若直接在 bindApprovers 加池校验,草稿保存也会被拦。需区分:草稿暂存不校验池,正式送审(Submit/Create approve)才校验。


二、问题清单

# 层面 问题 影响 优先级
P1 架构 审核设置池(sys_approver)与写通知选人(教师树)完全脱节,池无消费者 审核设置形同虚设,配了也不约束选人 P0
P2 后端 bindApprovers(teaId 路径)不查 sys_approver 池,任何本校教师可被选为审批人 审批人不受管控 P0
P3 后端 DictService.addApprover/updateApprover 不接 teaId;getApproverList 不返回 teaId sys_approver.tea_id 永远为空,池与教师树无法关联 P0
P4 前端 审核设置弹窗账号手填,无「从教师选择」入口 易输错、与用户中心不一致、无法落 teaId P0
P5 前端 ApprovalFlowEditor 选人数据源为全量教师树,不过滤池 可选到池外教师(后端也不拦) P0
P6 前端 notice.ts 的 approverPool() 在 v3 被删除,前端无法拉池 写通知选人无法做池过滤 P1
P7 前端 ApproverVo 类型缺 teaId 字段 审核设置列表/弹窗无法展示和编辑 teaId P1
P8 后端 NoticeVo 无「当前步待签人姓名」字段 审批中心无法显示「当前审批人」列 P1
P9 前端 审批中心 ApprovalQuery.status 未升级为后端新名 auditState 能跑(后端别名兼容)但契约不对齐 P2
P10 Mock mock 定义 /api/dict/Approvers(复数)vs 实际 /api/dict/Approver(单数);缺 getTeaTree/ApproverSuggest/ApproverResolve;审批人无 teaId mock 模式下审核设置 404、审批选人不可用 P2
P11 后端 saveFlowDefinition(草稿)也走 buildSteps→bindApprovers,加池校验会误拦草稿 需区分草稿/送审校验强度 P1

三、修改计划

Phase 1 · 后端:审批人池 teaId 化 + 送审池校验 + 列表待签人字段

工程:E:\YHXT\java\CampusNotice;编译:JAVA_HOME=D:/Program Files/Java/jdk-1.8,mvn=D:/Program Files/Java/maven/apache-maven-3.6.1/bin/mvn.cmd;服务跑 IDEA Debug + devtools(小改热重启,大改需手动重启)。

# 文件 改动
1.1 DictService.java L302-316 getApproverList() 返回 Map 增加 item.put("teaId", approver.getTeaId())
1.2 DictService.java L320-337 addApprover(...) 签名扩为 addApprover(String account, String name, String deptId, Integer teaId);teaId 非空时落库;teaId 非空且 account 为空时,按 teaId 查本校教师名单(classService.getSchTeaList)自动解析 account/name(教师登录账号),解析失败报「所选教师无登录账号,无法作为审批人」
1.3 DictService.java L341-355 updateApprover(...) 签名扩为 updateApprover(Integer id, String name, String deptId, Integer enabled, Integer teaId);teaId 非 null 时更新(含清空传 0→null 语义可选)
1.4 IDictService.java L94/L99 接口签名同步 1.2/1.3
1.5 DictController.java L104-107 addApprover body 改 Map<String,Object>(兼容 teaId 数字),取 teaId = body.get("teaId") 转 Integer 传 service
1.6 DictController.java L112-119 updateApprover body 取 teaId 转 Integer 传 service
1.7 NoticeService.java L931-969 bindApprovers(...) 增加重载 bindApprovers(Integer schId, List<Integer> teaIds, boolean validatePool);validatePool=true 时:拉 sysApproverRepository.findBySchIdAndEnabledOrderByNameAsc(schId,1) 建 teaId Set,teaIds 中不在 Set 的报「审批人不在本校审批人名单或已停用(姓名),请在「审核设置」中维护」;原签名委托 validatePool=false 保持兼容
1.8 NoticeService.java L790 buildSteps(...) 增加 boolean validatePool 参数,透传 bindApprovers;账号路径 bindAccounts 本身已查池不受影响
1.9 NoticeService.java L765 saveFlowDefinition(...) 调 buildSteps(entity, flow, legacyTeaIds, **false**)——草稿/编辑暂存不校验池
1.10 NoticeService.java L490 doSubmitApproval(...) 调 buildSteps(entity, flow, legacyTeaIds, **true**)——正式送审校验池
1.11 NoticeVo.java L58-75 增加字段 private List<String> pendingApproverNames;(当前步待签人姓名;非 pending 态为 null)
1.12 NoticeService.java L1081 fillApprovalFields(...) 装配 pendingApproverNames:定位当前步(stepNo==approvalLevel+1 且 status==ACTIVE),取 approverNames JSON 列表,减去本轮本步已处理人(notice_approval_record round+stepNo 命中者),余下即待签人;复用 toFlowVos 中 pending 计算逻辑(L1204+)
1.13 编译验证 mvn compile -q;devtools 热重启或 IDEA 手动重启

兼容性说明:

  • 存量 sys_approver 记录 teaId 为 null → 池过滤时这些记录不会出现在教师树白名单(teaId null 无法匹配树叶子),需在审核设置中逐条「编辑→选择教师」补绑 teaId;方案提供批量补绑脚本可选(§五)。
  • 存量待审批单(status=2)的 approverTeaIds 快照已落库,Approve/Reject 按快照走不重新校验池(requireStepApprover L672 只比对快照),不受影响。
  • bindAccounts(账号路径)保持不变,api-test.html 测试台账号路径仍可用。

Phase 2 · 前端:审核设置改造(从教师选择 + teaId 绑定)

工程:E:\YHXT\view\CampusAnnouncement。

# 文件 改动
2.1 types/notice.ts ApproverVo(L504-511) 增加 teaId?: number | null
2.2 api/module/notice.ts approvalSettingApi addApprover 入参增 teaId?: number;updateApprover 入参增 teaId?: number;恢复 approverPool(): GET /api/dict/ApproverPool(v3 删除,Phase 3 选人过滤要用),返回类型 ApproverPoolItem{id,name,account,deptName,teaId,isParent}
2.3 ApprovalSetting.vue 弹窗 ① 增加「选择教师」按钮 → 打开 UserCheckModal(数据源 baseApi.getTeaTree(true),单选模式:新增 multiple prop 默认 true,此处传 false,选新自动取消旧);② 选中回调 [{id:teaId,name,group}] → 自动填 form.name、form.teaId,account 由后端按 teaId 解析(前端 account 字段改只读展示,placeholder「选择教师后自动填充」);③ 编辑态回显:row.teaId 非空时按钮文案「重新选择教师」,空时「选择教师」;④ form 增 teaId 字段,saveApprover 请求体带 teaId
2.4 ApprovalSetting.vue 列表 可选增加「教师ID」列(teaId,width 90,排查用;teaId 为空显示「未绑定」灰色 Tag 提示需编辑补绑)
2.5 ApprovalSetting.vue Alert 文案 更新为:「这里维护可被选为审批人的教师名单(白名单)。发布通知选择审批人时,只能从此处启用的教师中选择;停用后不出现在选人列表,已有审批流程不受影响。」
2.6 UserCheckModal.vue 新增 multiple?: boolean prop(默认 true);false 时勾选新叶子自动取消其他已选(单选语义),submit 仍返回数组(长度≤1);接收班级/审批流弹窗不传该 prop 行为不变

Phase 3 · 前端:写通知选人改为池白名单过滤

# 文件 改动
3.1 ApprovalFlowEditor.vue pickerSources loader 改为:并行拉 approvalSettingApi.approverPool() + baseApi.getTeaTree(true);池为空 → Message.warning("请先在「审核设置」中添加审批人") 并返回空树;否则建 allowedTeaIds = Set(pool.filter(p=>p.teaId!=null).map(p=>p.teaId)),递归过滤教师树:叶子 id∈Set 保留,组节点无保留叶子则剪枝
3.2 新增工具函数 filterTreeByTeaIds(tree, idSet) 放 utils/index.ts 或组件内:递归 SchTreeNode[],type===2(叶子)按 id∈idSet 过滤,type===1(组)过滤 children 后空则丢弃;返回新树不改原数据
3.3 记忆带入兼容 ApproverSuggest 返回的 teaIds 若含池外教师(已被停用/删除),bindStepByTeaIds 解析姓名后标注「(已不在审批人名单)」灰色 chip,提交时后端池校验会拦截并提示——前端不静默丢弃,让用户可见可删
3.4 编辑回显兼容 存量单 approvalFlow[].approverTeaIds 含池外教师时,回显 chip 正常展示姓名(ApproverResolve 不依赖池),但重新打开选人弹窗时该教师不在过滤后树中 → 勾选态丢失;保存时若步骤含池外 teaId 后端报错,前端 catch 提示「审批人 XX 已不在名单,请重新选择」。已知边界,登记不阻塞

Phase 4 · 前端:审批中心增强

# 文件 改动
4.1 ApprovalList.vue columns 「审批进度」列后增加「当前审批人」列:key:"pendingApproverNames",render 逗号顿号拼接,空显示「—」;width 160,tooltip
4.2 types/notice.ts NoticeVo 增加 pendingApproverNames?: string[]
4.3 types/notice.ts ApprovalQuery status 字段改名 auditState(保留 status? 为 deprecated 别名可选);ApprovalList.vue 的 useRequest 参数与 RadioGroup v-model 同步改 auditState
4.4 api/module/notice.ts approvalList 参数序列化改传 auditState(后端别名兼容期结束后可删 status 支持,本轮不动后端)

Phase 5 · Mock 修复(P2,可与 Phase 1-4 并行或延后)

# 文件 改动
5.1 mock/noticeapi.ts 补 GET /api/dict/Approver(单数,含停用,返回增 teaId);现有 /api/dict/Approvers(复数)保留或删除
5.2 mock/noticeapi.ts 审批人种子数据增加 teaId 字段(与 getTeaTree mock 教师 id 对齐)
5.3 mock/noticeapi.ts 补 GET /api/base/getTeaTree(v5 登记缺失):学校→部门→教师组节点树,叶子 id=teaId
5.4 mock/noticeapi.ts 补 GET /api/notice/ApproverSuggest、GET /api/notice/ApproverResolve
5.5 mock/noticeapi.ts AddApprover/UpdateApprover 入参处理 teaId;ApprovalList 返回行增加 pendingApproverNames

Phase 6 · 回归验证

# 项
6.1 后端 mvn compile -q 通过;重启后 GET /api/dict/Approver 返回含 teaId;POST AddApprover 带 teaId 落库
6.2 前端 npm run build 通过
6.3 审核设置:新增→选择教师→自动填姓名/账号→提交→列表 teaId 列有值;编辑→重新选择教师→更新;停用/启用/删除正常
6.4 写通知:勾选审批→选人弹窗教师树只显示池内启用教师;池为空时提示;选池外教师(记忆带入残留)提交被后端拦截并提示
6.5 送审:Submit/Create(approve) 时池校验生效(池外 teaId 报错);SaveDraft/Edit 暂存不校验(草稿可含池外教师)
6.6 审批中心:表格「当前审批人」列显示待签人姓名;通过后流转下一步姓名更新;末步通过→发布→列显示「—」
6.7 存量兼容:改造前提交的待审批单可正常 Approve/Reject(按快照走不重校验);存量池记录 teaId=null 不出现在选人树,编辑补绑后恢复
6.8 旧链路回归:收件箱/一键已读/撤回/置顶/转发/附件/打印/接收班级弹窗 不受影响

四、实施顺序

Phase 1(后端)──→ Phase 2(审核设置)──→ Phase 3(选人过滤)──→ Phase 4(审批中心)
      │                                                              │
      └────────────── Phase 5(Mock,可延后)─────────────────────────┘
                                                                     ↓
                                                              Phase 6(回归)
  • Phase 1 先行:后端 teaId 化 + 池校验是前端改造的前提(前端选人过滤依赖 ApproverPool 返回 teaId)。
  • Phase 2/3 可并行:审核设置弹窗改造与选人过滤互不依赖(都消费 Phase 1 产物)。
  • Phase 4 独立:仅依赖 Phase 1.11/1.12(NoticeVo 新字段)。
  • Phase 5 不阻塞真实环境联调,可最后补。
  • 每个 Phase 完成后回填 §六实施记录。

五、风险与边界

# 风险 应对
R1 存量 sys_approver 记录 teaId=null,改造后这些审批人不出现在选人树(白名单按 teaId 过滤) 审核设置列表「教师ID」列显示「未绑定」Tag 提示逐条编辑补绑;可选提供一次性脚本:按 account 匹配本校教师名单反查 teaId 批量回填(UPDATE sys_approver a SET tea_id=(SELECT user_tea_ser_i_d FROM ...) WHERE ...,需人工核对)
R2 池为空时写通知无法选审批人 前端 loader 检测空池 → warning 提示引导去审核设置;不降级为全量教师树(保持白名单语义)
R3 草稿含池外教师,提交时被拦 预期行为(草稿宽松、送审严格);前端 catch 后端报错并高亮问题步骤
R4 教师离职/调岗后池内 teaId 失效 停用时池过滤自动排除;Approve/Reject 按已落快照走不受影响;建议定期核对池与教师名单
R5 UserCheckModal 新增 multiple prop 影响现有调用方 默认 true,接收班级/审批流弹窗不传行为不变;仅审核设置传 false
R6 后端 addApprover 按 teaId 解析 account 依赖 getSchTeaList 返回登录账号字段 已核实(2026-09-14):SchTeaList 无登录账号字段(仅 teaName/userTeaSerID/teaNo/teaTel);改走用户中心 YschBaseOpenApi.getUserById(teaId).getLoginName() 解析 account(currentAccount() L155 同口径),解析失败报错提示手填 实施按核实结论执行
R7 devtools 热重启对 DictService/NoticeService 签名变更可能不生效 Phase 1 完成后 IDEA 手动重启服务再联调

五-B、v7 补充(2026-09-14 · 旧系统截图对照)

补充资料:旧系统「审批列表」「公告详情」截图两张。对照结论:

# 截图证据 结论 / 范围补充
S1 旧审批列表列序:序号/标题/来源部门/发布人/提交时间/审批状态/审批意见/操作;待审批行审批意见恒「—」 补充 4.5:恢复「审批意见」列(v2-E6 曾以死字段删除)。后端 NoticeVo 增 lastOpinion(最新轮末条处理记录意见:驳回行=驳回意见、通过行=末步通过意见、pending/none=null),fillApprovalFields 从已批量加载的 records 装配;前端列 render row.lastOpinion || "—"
S2 旧详情审批流面板:每步「审批人:…/待其签:刘文娟/朱皓东 通过·时间·意见」 确认 4.1「当前审批人」列口径=当前步待签人姓名(与详情 approvalFlow[].pending 同源);详情渲染已具备,无需改
S3 旧列表操作列:非本步审批人显示灰字「等待他人审批」,本步待签人显示通过/驳回图标 与现 canApprove 逻辑一致,不改

补充实施项(并入 Phase 1 / Phase 4):

# 文件 改动
1.11b NoticeVo.java 增 private String lastOpinion;(最新轮末条处理意见;pending/none=null)
1.12b NoticeService.fillApprovalFields 装配 lastOpinion:status∈{rejected, approved(带步骤)} 时取最新轮按 id 末条 record(record 表 action=1 通过/2 驳回)的 opinion
4.5 ApprovalList.vue + types/notice.ts 增「审批意见」列(lastOpinion,width 160,tooltip,空显「—」),置于「当前审批人」列后;NoticeVo 类型增 lastOpinion?: string | null

六、实施记录(2026-09-14 实施回填)

项 状态 备注
1.1 getApproverList 返 teaId ✅ DictService 返回 Map 增 teaId
1.2 addApprover 接 teaId + 自动解析 account ✅ account 空且 teaId>0 时 YschBaseOpenApi.getUserById(teaId).getAccount() 解析(R6 核实:SchTeaList 无账号字段、BoaUser 用 getAccount 非 getLoginName);解析失败报「请手动填写账号」
1.3 updateApprover 接 teaId ✅ teaId>0 更新绑定、=0 清空(null 不改)
1.4 IDictService 签名同步 ✅ 含 javadoc(teaId=0 清空语义)
1.5 DictController.addApprover body 改 Map<String,Object> ✅ 新增 asString helper 防 String.valueOf(null)="null"
1.6 DictController.updateApprover 取 teaId ✅ 复用 toInteger
1.7 bindApprovers 增 validatePool 重载 ✅ 池校验抽为 requireInApproverPool helper(池外/停用/未绑定 teaId 报「请在「审核设置」中维护」)
1.8 buildSteps 增 validatePool 参数 ✅ 教师ID路径两处 bindApprovers 透传;补充:草稿已存定义复用早退分支也按 validatePool 校验(堵「沿用草稿流程」绕过池校验漏洞)
1.9 saveFlowDefinition 传 false ✅ 草稿/编辑暂存不校验池
1.10 doSubmitApproval 传 true ✅ 正式送审校验池
1.11 NoticeVo 增 pendingApproverNames ✅ 另增 lastOpinion(1.11b,审批意见列)
1.12 fillApprovalFields 装配待签人 ✅ 新增 pendingNamesOf(账号路径按账号比对/教师ID路径按姓名快照比对、仅最新轮)与 lastOpinionOf;顺带修复 toFlowVos 待签恒空缺陷(教师ID路径无账号快照 + 旧轮记录计入已处理,详情「待其签」恢复,对齐旧系统截图 S2)
1.13 mvn compile 通过 ✅ 首轮报 BoaUser.getLoginName 不存在→改 getAccount 后 EXIT=0
2.1 ApproverVo 增 teaId ✅ 另增 ApproverPoolItem 接口
2.2 notice.ts 恢复 approverPool + 入参增 teaId ✅ approverPool(): GET dict/ApproverPool 恢复(v3 曾删)
2.3 ApprovalSetting 弹窗选择教师 ✅ 「选择教师/重新选择教师」按钮 + UserCheckModal 单选教师树;选中自动填姓名+teaId;账号已选教师时禁用(后端解析);「账号或教师二选一」前置校验
2.4 列表教师ID列 ✅ 空显灰色「未绑定」Tag
2.5 Alert 文案更新 ✅ 白名单语义
2.6 UserCheckModal multiple prop ✅ 新增 enforceSingle:单选模式组节点勾选回退+提示、叶子勾选取消其他已选(callbackFlag=false 防递归);接收班级/审批流弹窗不传行为不变
3.1 pickerSources 池过滤 ✅ loader 改 loadPickerTree(教师树 ∩ approverPool 的 teaId 集合);空池 warning「请先在「审核设置」中添加并启用审批人」返回空树,不降级
3.2 filterTreeByTeaIds 工具函数 ✅ 组件内 filterTreeByPool(叶子按集合保留、空组剪枝)
3.3 记忆带入池外标注 ✅ 池外教师 chip 姓名追加「(不在审批人名单)」,提交由后端池校验拦截
3.4 编辑回显池外兼容 ✅ 回显 chip 走 names 兜底不受池影响;重开弹窗池外教师不在树中勾选态丢失(登记边界);保存含池外 teaId 后端报错由页面 catch 提示
4.1 审批中心当前审批人列 ✅ pendingApproverNames 顿号拼接、空显「—」
4.2 NoticeVo 类型增字段 ✅ pendingApproverNames/lastOpinion
4.3 ApprovalQuery status→auditState ✅ 旧名保留 deprecated 别名
4.4 approvalList 参数改 auditState ✅ ref/模板 RadioGroup/run() 同步改名
4.5 审批意见列(截图 S1 补充) ✅ lastOpinion 空显「—」,置于「当前审批人」列后
5.1-5.5 Mock 修复 ✅ 种子补 teaId(对齐 getTeaTree 教师 id 1/2/3/4/7/11);Approvers→Approver 单数;ApproverPool 补 teaId;Add/UpdateApprover 处理 teaId(account 空按教师树 mock 解析);ApprovalList 兼容 auditState;toVo 两处补 pendingApproverNames/lastOpinion;补 ApproverSuggest/ApproverResolve 端点(getTeaTree 已由 v8 补齐)
6.1 后端编译 ✅ mvn compile EXIT=0
6.2 前端构建 ✅ npm run build 44.55s 通过(仅 chunk 体积警告)
6.3-6.8 人工冒烟(需登录环境) ⏳ 待冒烟:审核设置选教师落库/列表 teaId 列;选人树仅池内教师+空池提示;送审池校验拦截/草稿暂存不拦;审批中心两列显示;存量待审批单 Approve/Reject 不受影响;旧链路回归

v8(2026-09-14)· 修复 v6 审批人教师树「显示异常」(对齐 v5 指定班级的样式与接口口径)

现象:发公告→审批→「选择审批人」弹窗内教师树不显示(面板空白,控制台报 TypeError)。 要求:参考 v5「指定班级」选择弹窗的样式与接口口径重做。

一、根因(源码级确认)

# 位置 问题
R1 UserCheckModal.formatTree if (typeof element.children !== "undefined" && element.children.length > 0):typeof null === "object",而 getTeaTree/getStuTree 的人员叶子 children 显式为 null(SchTreeNodeDto 注释「叶子为 null」;后端 application.yml 未配 jackson.default-property-inclusion: non_null,Jackson 输出 "children":null)→ null.length 抛 TypeError → createTree 建树中断,弹窗树整体空白。v6 §一.1 假设「教师叶子无 children」不成立
R2 UserCheckModal.currentTreeFlatten 解构默认值 children = [] 同样不拦截 null → flatten(null) 抛错,搜索框一输入即炸
R3 UserCheckModal.createTree 回显 getNodesByFilter(userids.includes(id)) 未限定叶子:teaId 与组节点 id 撞号时整组被误勾(教师ID/组ID 同空间风险)
R4 ApprovalFlowEditor 直接把 getTeaTree(toUser=true) 原始树塞给弹窗(未像 v5 班级树那样前端归一化);且 :show-selected="false" 单栏窄树与指定班级样式不一致;teaNames 仅靠弹窗提交回填 → 编辑回显 chip 只能显示「ID xxx」
R5 mock 缺 base/getTeaTree(v5/v6 两轮登记、v7 Phase 5.3 排期),mock 下无法验证审批选人

二、修复计划

  1. UserCheckModal.vue(通用防御,所有调用方受益):formatTree 统一 children || [] 归一(叶子删除 children 键,避免 zTree 内部同样踩 null);currentTreeFlatten 空值兜底;回显勾选限定 type === 2 叶子节点。既有调用方(接收班级/应用设置/联系组,叶子本就无 children 键)行为不变。
  2. ApprovalFlowEditor.vue(对齐指定班级口径):
    • 教师树 loader 归一化(同 ClassScopeInput.loadTree/toGradeTree 模式):缓存 getTeaTree?toUser=true 结果 → 重建纯净节点(叶子不带 children 键、isParent 显式标注、无教师的空组分支剪除),弹窗侧与姓名解析共用一次请求;
    • 弹窗改穿梭式(去掉 :show-selected="false",720px + 右侧已选列表/全选/计数),与指定班级同款(撤销 v6-5 的单栏收敛,仅审批弹窗恢复,showSelected prop 保留供其他场景);
    • 树加载后用叶子回填 teaNames,编辑回显 chip 直接显示教师姓名(ApproverResolve/详情 names 仍作池外兜底)。
  3. mock/noticeapi.ts:补 GET /api/base/getTeaTree(学校→部门组→教师,节点结构对齐 SchTreeNodeDto:type/isParent/children:null)——同时作为 R1 归一逻辑的回归载体;提前完成 v7 Phase 5.3。
  4. 回归:npm run build + 人工冒烟。与 v7 关系:本轮只修「显示」,v7 的池白名单过滤(Phase 3)在归一化 loader 上追加过滤即可,不冲突。

三、回归验证清单(v8)

  • 选择审批人弹窗:教师树正常渲染(组📁/教师👤图标区分、展开折叠、搜索教师名命中并勾选)
  • 穿梭式:勾选教师进右侧已选列表(组级联全选),全选/计数/删除选中可用;提交后当前步 chips 显示姓名
  • 编辑回显:已提交审批流 chips 直接显示姓名(不再「ID xxx」);重开弹窗已选教师自动打勾
  • teaId 与组 id 撞号时不误勾整组
  • 接收班级弹窗(v5)、应用设置/联系组等其他 UserCheckModal 调用方行为不变
  • mock 模式:审批选人弹窗可加载教师树(含 children:null 叶子,验证归一化)
  • npm run build 通过

四、实施记录(v8 回填)

项 状态 备注
R1-R3 UserCheckModal ✅ 2026-09-14 formatTree 改 Array.isArray(children) ? children : [] 归一、叶子 delete children 键(防 zTree 二次踩 null);currentTreeFlatten 改 `(data
R4 ApprovalFlowEditor ✅ 2026-09-14 新增 toPickerTree()(对齐 ClassScopeInput.toGradeTree 口径:叶子重建为 {id,name,isParent:false}、空组分支剪除、顺带回填 teaNames)+ loadTeaTree() 缓存 promise(失败置空可重试);弹窗去掉 :show-selected="false" 恢复穿梭式(showSelected prop 保留供其他场景);setFlow 末尾 warmTree() 补齐回显姓名
R5 mock getTeaTree ✅ 2026-09-14 补 GET /api/base/getTeaTree(学校→部门组→教师,人员叶子 children:null 复刻真实序列化,兼作 R1 回归载体);v7 Phase 5.3 提前完成
根因验证 ✅ 2026-09-14 .scratch/verify-tree.mjs 对照跑旧/新建树逻辑:旧实现抛 TypeError: Cannot read properties of null (reading 'length')(复现显示异常);新实现组 type=1、教师 type=2、叶子无 children 键;teaId 与组 id 撞号时回显命中数 旧 1(误勾整组)→ 新 0
构建回归 ✅ 2026-09-14 npm run build 通过(31.85s,仅 chunk 体积警告);其他 UserCheckModal 调用方(接收班级/应用设置/联系组/死代码 ClassCheckInput)叶子本就无 children 键,归一逻辑对其为空操作,行为不变

待人工冒烟(需登录环境):§三 v8 清单逐条过——重点真实教师树(多组重复教师、深层部门)渲染与搜索、穿梭式勾选/全选/计数、编辑回显 chip 姓名。与 v7 衔接:Phase 3 池白名单过滤在 toPickerTree 前追加 filterTreeByTeaIds 即可,无需再改弹窗。


v9(2026-09-14)· 应用标题「校园通知」→「校园公告」

需求 1:产品对外名称改为「校园公告」。仅文案层,不改路由、接口与模块 modId。

一、标题来源盘点

# 位置 作用
T1 vite.config.ts → createHtmlPlugin.injectOptions.data.title 浏览器标签页 <title>(index.html 里是 <%- title %> 占位,改模板无效)
T2 src/App.vue layoutData.appName 侧边栏顶部 <h4> + 面包屑首项(layout.vue 第 251 / 312 行消费);App.vue 不调 GetLayout,故这就是真实显示值
T3 src/components/layout.vue appName computed 兜底 props 缺省时的兜底文案
T4 src/assets/mock/noticeapi.ts base/GetLayout 的 topName mock 平台应用名(保持与线上一致口径)
T5 README.md 主标题 仓库文档标题

不改动:代码注释里的历史称谓「校园通知改造 / 校园通知测试台」(指向 v1–v8 方案条目,改动会破坏可追溯性);菜单项「通知统计」「已收到/待收到」等业务文案;后端 E:\YHXT\java\CampusNotice 无标题字段,平台侧模块名由用户中心库表配置(如需同步改名在平台管理后台处理)。

二、修改计划

T1–T5 五处字面量替换为「校园公告」;dist/ 为构建产物,随下次 npm run build 覆盖,不手改。

三、回归验证清单(v9)

  • 浏览器标签页标题显示「校园公告」
  • 侧边栏顶部应用名、面包屑首项显示「校园公告」
  • mock 模式下布局正常
  • npm run build 通过且产物 index.html 标题为「校园公告」

四、实施记录(v9 回填)

项 状态 备注
T1–T5 ✅ 2026-09-14 五处字面量替换完成;改动前原文件备份至 .backup/20260914-v9-title/(两工程均非 git 仓库)
构建回归 ✅ 2026-09-14 npm run build 通过(29.76s,仅 chunk 体积警告);产物 dist/index.html 的 <title> 已为「校园公告」

未改动的同名串(有意保留):源码注释里的历史称谓;mock 内非标题测试数据(附件名 校园通知附件N.png、占位图文字 校园通知 Mock 图片)——仅 dev:mock 下可见,不属「标题」范围,如需一并换名后续追加。


v10(2026-09-14)· 左侧栏目名与 GetUserMod.name 绑定(撤销前端改名覆盖)

需求:左侧菜单栏目名称直接绑定接口 base/GetUserMod 返回的 name,平台改什么前端显示什么。 决策(已确认):完全按接口 name 显示,生效范围 = 侧边栏 + 顶部面包屑;页面内部标题(routes.ts meta.title、各视图自身文案)不变。

一、现状盘点

# 位置 现状 结论
M1 src/App.vue L29 baseApi.getUserMod() → layoutData.modList,并透传 children 数据源已是接口,无需改
M2 src/components/layout.vue displayModList / menuArr 侧边栏与面包屑同源消费 normalizeModList(modList) 改一处即两处同时生效
M3 src/components/layout.vue MENU_NAME_MAP(L31–49) 展示层强制改名:已收到→我的通知、已发送→已发管理、发公告→写通知、公告列表→全部通知、审批列表→审批中心、公告统计→通知统计 本次移除(v3 结论「前端映射覆盖」被本次需求推翻)
M4 src/utils/menuRoute.ts NAME_ALIASES / ROUTE_BY_TITLE 按原始 name(映射前)解析路由,仅作为 path 配错时的兜底 保留,与显示解耦,不受影响
M5 normalizeModList 内草稿箱注入(modId 31) 平台无草稿箱模块 保留,仍用前端字面量「草稿箱」
M6 src/assets/config/menu.ts getMenuList() 无人引用(死代码) 不动,仅在 README 说明其为历史兜底

二、修改计划

  1. layout.vue 删除 MENU_NAME_MAP 常量及其在 normalizeModList 父/子两级中的引用,name 直接沿用接口值;注释同步改为「栏目名以平台 GetUserMod.name 为准」。
  2. 路由解析继续用接口原始 name + modId 兜底(M4 逻辑不变)——即显示名与跳转目标彻底解耦,平台改名不会导致菜单跳错页。
  3. 连带影响(预期行为,非缺陷):若平台库表仍配旧叫法(如 已收到),侧边栏/面包屑即显示旧叫法,需由平台管理后台改 name 才显示新名;mock 数据(已收到/已发送/审批列表)会直接呈现这一变化。
  4. README 菜单说明补一句「栏目名不做前端改名」。

三、回归验证清单(v10)

  • 侧边栏每项文字 == 接口 GetUserMod.name(mock 下应为 已收到/已发送/全部通知/审批列表/审核设置/通知统计/应用设置 + 注入的草稿箱)
  • 面包屑模块层级名与侧边栏同源一致;详情/写通知等子页仍取 meta.title
  • 平台名与内部新名不一致时点击菜单仍跳正确路由(page 失效时 NAME_ALIASES/modId 兜底仍生效)
  • 审批中心角标、菜单图标、isAlone 判定不受影响
  • npm run build 通过

四、实施记录(v10 回填)

项 状态 备注
M3 移除 MENU_NAME_MAP ✅ 2026-09-14 layout.vue 删映射表及父/子两处 name 覆盖,normalizeModList 只做「path 归一 + 外链标记 + 草稿箱注入」;全站 grep MENU_NAME_MAP 零残留
README 菜单说明 ✅ 2026-09-14 改为「栏目名直接显示接口 name,前端不改名」,并注明 menu.ts 为无引用历史文件
v2 盘点表定论修正 ✅ 2026-09-14 「菜单契约风险=前端映射覆盖」标注为已被 v10 推翻,防后续会话按旧结论回滚
备份 ✅ 2026-09-14 改动前 layout.vue / README.md 备份至 .backup/20260914-v10-menu-name-bind/(工程非 git 仓库)
构建回归 ✅ 2026-09-14 npm run build 通过(17.90s,仅 chunk 体积警告)
路由解析回归(改名安全性) ✅ 2026-09-14 .scratch/verify-menu-name-bind.mjs(esbuild 现场打桩 menuRoute.ts 后 Node 直跑)14/14 PASS:平台旧叫法 + 正确 path 全部命中内部路由;平台改名(重要通知/我发出的/完全没听过的名)时仍按 path 剥离 → 名称别名 → modId 兜底正确解析;外链模块保持整页跳转

结论:显示名与跳转目标彻底解耦,平台改 name 即时反映到侧边栏与面包屑;因平台库表仍配旧叫法,当前线上/mock 会显示「已收到 / 已发送 / 审批列表 / 通知统计」等旧名,需平台管理后台把模块 name 改为新叫法(我的通知 / 已发管理 / 全部通知 / 审批中心 / 审核设置 / 通知统计 / 应用设置)后才呈现新文案。草稿箱为前端注入项,名称仍由 layout.vue 字面量决定。


v11(2026-09-14)· 左侧栏目顺序按 GetUserMod.orderNumber 排序

需求:菜单顺序不再依赖平台/SDK 返回行序,直接按接口字段 orderNumber 升序排。与 v10 同构:name 定名、orderNumber 定序,平台侧改完前端刷新即生效。

一、现状盘点

# 位置 现状 结论
O1 后端 UserCenterService.buildModuleTree L265 注释「保持 SDK(用户中心)返回顺序,不再按 orderNumber 重排」,roots 与 children 均按扁平列表插入序 维持后端不动(本次为前端排序;如需全平台统一口径再改此处的 node.put("orderNumber", …) 消费点)
O2 后端 L227–228 javadoc 仍写「子节点按 orderNumber 排序」,与 L265 实现自相矛盾 登记为后端文档缺陷(本次不改后端,留待后续单独修)
O3 后端 L245 orderNumber = BoaModule.sortNo,null → 0 排序键口径:数值,0 可能表示「未配置」
O4 前端 App.vue L32 layoutData.modList = modList 原样接收,未排序 无需改
O5 前端 layout.vue normalizeModList 只做 path 归一 + 外链标记 + 草稿箱 splice,完全不管顺序;children 顺序同样沿用接口 本次加排序
O6 前端 mock base/GetUserMod 已带 orderNumber: 1..7,且数组顺序恰好一致 → 现状看不出问题 验证需构造乱序输入

二、修改计划

  1. src/utils/menuRoute.ts 新增并导出 sortByOrderNumber(list):以 Number(orderNumber) 为键升序稳定排序;orderNumber 缺失/非数值(NaN、null、undefined)视为未参与比序,统一沉到末尾并保持相互间原序;同值(含全 0)保持接口原行序。放这里与 resolveMenuPage 同属「平台模块配置 → 前端菜单」的解析层,便于脚本直测。
  2. src/components/layout.vue normalizeModList:顶层与 children 两级在进入 path 归一之前先过 sortByOrderNumber(先定序再做草稿箱 splice,草稿箱继续固定紧跟「已发管理」/list,不参与平台序号)。 2-B. 实施时新增的重构:normalizeModList 整体从 layout.vue 下沉到 utils/menuRoute.ts(layout 只保留注释与消费),否则「排序 + 归一 + 草稿箱注入」这条管线在 SFC 内无法脚本直测,v11 只能验到一半。
  3. README「菜单说明」补第 3 条:栏目顺序按接口 orderNumber 升序。
  4. 不改后端、不改 routes.ts、不改 mock 数据(mock 的 orderNumber 与数组序一致,排序后无变化,符合预期)。

三、回归验证清单(v11)

  • 乱序 orderNumber 输入 → 侧边栏按升序输出,且各项 page/路由解析结果与顺序无关(.scratch/verify-menu-order.mjs)
  • children 子项同样升序;父项顺序决定分组显示顺序
  • 缺失 / 非数值 orderNumber 沉底且不相互打乱;全 0 或全相同 → 与接口原序一致(稳定性)
  • 草稿箱仍紧跟 /list,即使 /list 被平台排到中间或末尾
  • v10 路由解析回归(verify-menu-name-bind.mjs)与 npm run build 仍通过

四、实施记录(v11 回填)

项 状态 备注
sortByOrderNumber ✅ 2026-09-14 utils/menuRoute.ts 导出;键取 Number(orderNumber)(字符串数字可比,如 "2" < "10"),缺失/非数值沉底、同值保持接口原序;显式带原始下标决胜,不依赖引擎 sort 稳定性,且不就地修改入参
normalizeModList 下沉(2-B) ✅ 2026-09-14 由 layout.vue 整体搬入 utils/menuRoute.ts:orderNumber 定序(顶层 + children 两级)→ resolveMenuPage 归一/外链标记 → 排序后 splice 注入草稿箱;DRAFTS_MENU 提为常量。layout.vue 只留一行注释说明管线落点,导入改 { resolveMenuPage, normalizeModList }(resolveMenuPage 仍被 flaten 使用,避免 noUnusedLocals 报错)
README 菜单说明 ✅ 2026-09-14 「两层兜底」改为「三层处理」,新增第 3 条 orderNumber 升序口径
备份 ✅ 2026-09-14 改前 layout.vue / menuRoute.ts / README.md → .backup/20260914-v11-menu-order/(基线为 v10 完成后状态)
排序回归(v11 清单) ✅ 2026-09-14 .scratch/verify-menu-order.mjs 11/11 PASS:乱序序号升序、children 升序、缺失/非法沉底且互保原序、全 0 不改序、字符串数字参与比序、入参不被修改、undefined/空数组安全、/list 排首位与末位时草稿箱均紧跟其后(splice 不越界)
v10 回归 + 构建 ✅ 2026-09-14 verify-menu-name-bind.mjs 14/14 仍 PASS;npm run build 通过(33.50s,仅既有 CSS * 兼容写法警告与 chunk 体积警告)

后端零改动:UserCenterService.buildModuleTree 继续透传用户中心行序(L265),排序完全由前端消费层负责,因此其他 GetUserMod 消费方(若有)口径不变。遗留缺陷 O2(L227–228 javadoc 与 L265 实现矛盾)本次未动,建议后续把该 javadoc 改为「保持 SDK 返回顺序,排序由调用方决定」。


v12(2026-09-14)· 界面文案「通知」→「公告」

需求:把界面上的「通知」字样统一改为「公告」,例如「暂无通知」→「暂无公告」。 已确认范围(三项决策):① 只改界面可见文案,源码注释 / JSDoc / 类型契约注释 / README 不动;② routes.ts 的 meta.title 一起改,同时在 menuRoute 保留旧叫法兜底;③ 侧边栏栏目名不改前端(v10 定论),只输出平台后台改名清单。

一、现状盘点

全站 src 内「通知」共 150 处,按是否影响显示分为四类:

# 类别 数量/位置 本次处理
C1 界面可见文案(按钮、表单标签、占位符、弹窗标题、Message/MyConfirm 提示、空态文案、Alert 说明) 约 30 处,分布在 11 个视图/组件 改为「公告」
C2 routes.ts meta.title 我的通知 / 写通知 / 全部通知 / 通知详情×2 / 通知统计 改为「公告」;同时是面包屑子页显示名与 ROUTE_BY_TITLE 匹配键
C3 源码注释与 JSDoc(api/module/notice.ts、types/notice.ts、mock/noticeapi.ts、各视图头部说明、App.vue 布局注释) 约 100 处 不动(保留 v1–v11 历史称谓可追溯性,且不影响显示)
C4 非界面数据/死代码(mock 种子标题与 mock 菜单 name、assets/config/menu.ts 无引用兜底表、ClassCheckInput.vue/UserCheckInput.vue/PushScopePanel.vue 未被 import 的旧链路组件) 其余 不动(仅 dev:mock 或不可达路径可见)

特殊保留:NoticeAccept.vue 回执提示中的「请确认已通知到学员/家长」是动词用法,只把同句主语的「本通知」改为「本公告」,动词不动。

二、修改计划(C1 + C2 逐处)

  1. router/routes.ts:我的通知→我的公告、写通知→写公告、全部通知→全部公告、通知详情→公告详情(两条详情路由)、通知统计→公告统计;已发管理、草稿箱、审批中心、审核设置、应用设置 本就不含「通知」,不动。
  2. utils/menuRoute.ts:NAME_ALIASES 已含 写通知/我的通知/全部通知/通知统计 旧名 → 路由的映射,保持不动即为旧兜底;新叫法(我的公告/写公告/全部公告/公告统计)在改名后由 ROUTE_BY_TITLE 直接命中,无需新增条目。本次仅在该表补注释说明「C2 改名后旧平台名依赖本表兜底,勿删」。
  3. 视图与组件文案:
    • NoticeList.vue:获取列表失败提示、删除/撤回确认文案、操作列「查看通知/删除通知」、写通知 按钮
    • NoticeDetails.vue:加载失败提示、待审批 Alert、空态「通知不存在或已被删除」
    • NoticeAccept.vue:加载失败提示、左列表空态与右侧空态「暂无通知」、回执 Alert 主语句
    • DraftList.vue:写通知 按钮、提交审批弹窗「通知:标题」
    • NewNotice.vue:标题超长校验、正文占位符、加载失败提示、删除弹窗标题、「通知标题:」标签与其占位符
    • ApprovalList.vue:通过成功提示、操作列「查看通知」、通过/驳回弹窗的「通知标题」标签、驳回通知 弹窗标题、会签或签提示
    • ApprovalSetting.vue:白名单 Alert「发布通知选择审批人时…」
    • NoticeSetting.vue:部门排序 Alert 中的「写通知」
    • NoticeStats.vue:「通知统计功能开发中…」
    • ClassScopeInput.vue:占位符「选择通知的班级」
  4. 不改后端、不改接口路径与 modId 契约、不改 dist/ 产物。

三、回归验证清单(v12)

  • grep -rn "通知" src/views src/components src/router 剩余命中全部为注释/未引用组件/mock 之外的白名单项
  • 面包屑:/list/new 显示「已发管理>写公告」,详情页显示「…>公告详情」
  • 平台模块名仍为旧叫法(已收到/已发送/全部通知/审批列表/通知统计)时,菜单点击仍解析到正确路由(verify-menu-name-bind.mjs 全绿)
  • 「请确认已通知到学员/家长」动词未被误改
  • npm run build 通过

四、实施记录(v12 回填)

项 状态 备注
C2 routes.ts meta.title ✅ 2026-09-14 6 条改名:我的通知→我的公告、写通知→写公告、全部通知→全部公告、通知详情→公告详情(modId 23/24 两条)、通知统计→公告统计;已发管理/草稿箱/审批中心/审核设置/应用设置 不含「通知」,未动
menuRoute.ts 兜底加固 ✅ 2026-09-14 NAME_ALIASES 条目一字未改(旧名即本次兜底),仅补注释说明「ROUTE_BY_TITLE 不再注册旧标题,旧平台名依赖本表,勿删」
C1 界面文案(10 个在用文件) ✅ 2026-09-14 NoticeList(6)、NoticeDetails(3)、NoticeAccept(4)、DraftList(2)、NewNotice(6)、ApprovalList(6,含「通知标题」标签 2 处)、ApprovalSetting(1)、NoticeSetting(1)、NoticeStats(1)、ClassScopeInput(1),共 31 处
动词用法保留 ✅ 2026-09-14 NoticeAccept.vue L253 改为「本公告需学员阅读确认,请确认已通知到学员/家长。」——句内动词「通知」原样保留
C4 的一处例外 ✅ 2026-09-14 ClassCheckInput.vue L9 占位符(旧链路组件、全工程无 import)一并改为「选择公告的班级」,避免其在被引用的 ClassScopeInput 口径不一致;该文件改前基线也补入备份目录
备份 ✅ 2026-09-14 14 个文件(含 执行计划任务方案.md)→ .backup/20260914-v12-notice-to-announce/(保持 src/... 相对目录结构;工程非 git 仓库)
菜单解析回归 ✅ 2026-09-14 重新 esbuild 打桩 .scratch/menuRoute.bundle.mjs 后跑 verify-menu-name-bind.mjs 14/14 PASS、verify-menu-order.mjs 全部通过;另补测新/旧叫法各 5+4 项:新名(我的公告/写公告/全部公告/公告统计/公告详情)经 ROUTE_BY_TITLE 命中,旧名(我的通知/写通知/全部通知/通知统计)经别名命中,同一路由不分裂
构建 + 产物抽查 ✅ 2026-09-14 npm run build 通过(19.38s,仅既有 chunk 体积警告);dist 内命中「暂无公告/写公告/公告详情/选择公告的班级」,旧串「暂无通知 / >写通知< / 通知不存在」零残留

结论与遗留:

  1. 侧边栏栏目名本次未动前端(决策③)。改名后 meta.title 只影响面包屑子页与标题匹配,侧边栏与面包屑的模块层级名仍等于平台 GetUserMod.name。要让左侧显示「公告」口径,需在平台用户中心后台把模块 name 改为:我的通知→我的公告、全部通知→全部公告、通知统计→公告统计(已发管理、审批中心、审核设置、应用设置 无需改;若平台当前仍配旧叫法 已收到/已发送/审批列表,一并改为 我的公告/已发管理/审批中心)。前端路由解析对新旧叫法均已兜底,平台何时改、改不改都不会跳错页。
  2. 代码注释/JSDoc/README/mock 种子数据仍写「通知」(决策①),其中 routes.ts L19/L33–34、NoticeStats.vue L5 等注释直接引用了已改名的显示文案,后续若要做全量口径统一可按 C3 单独立项。
  3. mock(dev:mock)下左侧菜单仍显示「全部通知/通知统计」,属平台数据镜像,非缺陷。

v13(2026-09-14)· 审批中心:tab 显示统计数字 + 「待我审批」改名「待审批」+ 修复待审批恒空缺陷

一、需求与现状盘点

用户三点需求:① 待审批/已通过/已驳回/全部四个标签页显示各自条数;② 「待我审批」文案改「待审批」;③ 待审批 tab 未显示应看到的单子(缺陷)。

现状 位置 说明
四 tab 仅文字无数字 ApprovalList.vue L186–191 RadioGroup 数字来源缺失:后端只有 GET /api/notice/ApprovalCount(单一「待我审批」数,供菜单角标)
文案「待我审批」 同上 L187 需改「待审批」;左侧菜单「审批中心」角标继续用 ApprovalCount,不改口径
pending 过滤 = 当前步审批人快照含我 NoticeService.approvalList L1067–1070 + approverMatchPredicate L1102–1125 口径经确认维持不变(仍只列需我审批的单),空列表属匹配链缺陷

二、证据表(本地库 campus_notice 直查,2026-09-14)

编号 证据 结论
S1 notice 表 5 条 status=2 待审批单:approver_accounts 全为 NULL,approver_tea_ids 分别 [10007]/[10007]/[10008]/[10007,10008,10006]/[10007,10006,10008] v7 前端教师树选人只走教师ID路径,buildSteps L841–851 该路径从不写账号快照(只有 step 表 approver_names),账号匹配条件恒落空
S2 DefaultLoginUserProvider(yh-auth-spring-boot-starter 反编译):getLoginUser 用 token 串构造 LoginUser 并 setLoginName/setDisplayName;LoginUser(String) 单参构造把同一串写进 userId 字段 LoginUser.userId 实际是登录名(如 lijing),不是数字用户ID
S3 NoticeService.currentTeaId() L170–180:Integer.valueOf(userId.trim()) 抛 NumberFormatException → 返回 null 教师ID匹配条件也恒缺席 → approverMatchPredicate 的 cb.or() 无任何可命中谓词 → 待审批 tab 恒空;同一缺陷使 canApproveNow/requireStepApprover/requireApprover 全部失效(通过/驳回会报「您不是本步骤的审批人」)
S4 YschBaseOpenApi.getUserInfo(account) 返回 BoaUser,有 getId()(用户中心ID,与教师树叶子 teaId 同空间)与 getAccount();工程内已有按账号反查用法(resolveUserName L2138) 用登录名反查 teaId 的兜底通道可用
S5 审批动作表 notice_approval 的 operator_account=lijing(登录名)与 S1 的 teaId 快照分属两个 ID 空间 印证修复需双路:读侧解析 teaId + 写侧补账号快照

三、错误清单与修复计划

编号 缺陷/需求 修复 落点
E1 待审批恒空(S1–S3) currentTeaId():userId 非数字时回退 YschBaseOpenApi.getUserInfo(currentAccount()).getId()(异常/查无 → null,保持现语义) NoticeService.currentTeaId
E2 教师ID路径缺账号快照,OpenAPI 故障时无法匹配 buildSteps 教师ID路径与旧包装步:按 teaId getUserById(teaId).getAccount() 补 step.approverAccounts(查不到跳过),主表当前步快照随首步自动带上 NoticeService.buildSteps
E3 四 tab 无数字 新增 GET /api/notice/ApprovalTabCount → {pending,approved,rejected,all},口径与各 tab 列表谓词一致(不含 keyword);菜单角标仍用 ApprovalCount NoticeController + INoticeService + NoticeService.approvalTabCount + 新 VO ApprovalTabCountVo
E4 「待我审批」文案 改「待审批」;数字后缀显示,为 0 不显示 ApprovalList.vue
E5 前端契约 notice.ts 加 approvalTabCount();types/notice.ts 加 ApprovalTabCountVo;mock noticeapi.ts 补 /api/notice/ApprovalTabCount(同口径) 前端三件套

四、回归验证清单(v13)

  • mvn compile 通过(JAVA_HOME=jdk-1.8,BUILD SUCCESS)
  • lijing(10008) 登录后「待审批」可见单 3/4/5(或新送审单),且能实际通过/驳回(待浏览器验证)
  • 四 tab 数字与各自列表条数一致;0 值不显示数字(待浏览器验证)
  • 「审批中心」菜单角标仍为待我审批数(ApprovalCount 端点与 layout.vue 未改)
  • npm run build 通过(18.87s,仅既有 chunk 体积警告)

五、实施记录(v13 回填)

项 状态 备注
E1 currentTeaId() 回退 ✅ 2026-09-14 NoticeService:userId 非数字时 YschBaseOpenApi.getUserInfo(登录名).getId() 反查;异常/查无仍返回 null。读侧 approvalList/ApprovalCount/fillApprovalFields/canApproveNow/approve/reject 全部受益
E2 教师ID路径补账号快照 ✅ 2026-09-14 buildSteps 教师ID分支与旧包装步调用新私有方法 bindAccountsByTeaIds(逐个 teaId getUserById().getAccount(),失败跳过,全空保持 null);主表当前步快照随首步自动带上账号
E3 ApprovalTabCount ✅ 2026-09-14 新 VO model/notice/ApprovalTabCountVo;tab 谓词从 approvalList 抽为共用方法 approvalStatePredicates(语义不变);countApprovalState 只加多租户过滤、不带 keyword;Controller 加 GET 端点
E4 前端 tab ✅ 2026-09-14 ApprovalList.vue:「待我审批」→「待审批」,四标签渲染 (n) 后缀、0 不显示;run() 并行 loadTabCounts()(进入/翻页/审批操作后自动刷新,统计接口异常静默保留上次数字)
E5 前端契约 ✅ 2026-09-14 notice.ts(api) 加 approvalTabCount();types/notice.ts 加 ApprovalTabCountVo;mock 补 /api/notice/ApprovalTabCount 并把 mock 列表 pending/all 口径对齐真实后端
备份 ✅ 2026-09-14 前端 4 文件 + 后端 3 文件 → 两工程各自 .backup/20260914-v13-approval-tab-count/(非 git 仓库)

遗留与说明:

  1. 存量单不回补账号快照:E2 只作用于新送审/重提的单;存量(approver_accounts=NULL)靠 E1 的 teaId 读侧解析匹配,用户中心不可用时这批老单仍会漏列。如需彻底补齐可一次性脚本回写,暂不立项。
  2. 存量脏数据:notice 3 步骤表存在同 step_no 重复行(改造前遗留),fillApprovalFields/canApproveNow 按 findByNoticeIds 顺序取首个审批中步,展示可能抖动;notice 1/2 无步骤行走 approveLegacy 旧或签兜底,本轮不清洗。
  3. 会签+教师ID路径:approve() 的会签步完结判定按 stepAccounts 比对(E2 补上账号后对新单生效);存量教师ID会签单仍会卡在会签判定,属旧缺陷,建议单独立项回归。
  4. IDEA Debug 实例带 devtools:mvn compile 已更新 target/classes,一般会自动热重启;若未生效请在 IDEA 里 Build 一次或重启实例(重启前先 kill-port.bat 8101)。

v14(2026-09-14)· 写公告页底部按钮:保存→存为草稿 + 新增直接发布 + 固定排列

需求(截图:编辑态底部为 保存 / 提交审批 / 定时发送 / 删除):① 「保存」改「存为草稿」;② 增加「直接发布」;③ 排列顺序 直接发布 → 存为草稿 → 提交审批 → 定时发送(删除仍居末)。

一、接口口径证据(决定改法,2026-09-14 实测/读码)

编号 证据 结论
S1 后端 NoticeController 全量 mapping 无 Publish;发布只有 Create(publish→publish())、SaveDraft、Edit、Submit 不存在「发布一条已存在记录」的接口
S2 NoticeService.edit() L1555–1615:只更新内容/范围/附件/流程定义,全程不改 status、不调 doRealPublish 编辑态点任何按钮都不会让草稿变成已发布
S3 NoticeService.publish() L204–249 按 publishType 分流(draft/approve/direct),saveDraft() L254 与 publish() 均 entity.setId(null) 「直接发布」的正确落点是 Create + publishType:"direct",且必然产生新记录
S4 api-test.html submitCompose('direct') → body.publishType='direct' 且 delete body.flow → doSubmitCompose 在 mode==='edit' 时只调 /notice/Edit 测试台编辑态本就没有直接发布按钮(只有 保存修改/提交审批/删除),本次是前端新增能力,契约沿用 Create
S5 NoticeRepository.findDueTimingPublish L107 谓词 n.status = 1 and timingSendTime<=now and timingPublished=0 编辑态「定时发送」今天只 Edit 存了时间、状态仍为草稿 → 到点不会被调度(现存缺陷)
S6 doSubmitApproval L496–498:步骤为空即抛「请选择审批人」 无法借 Submit 实现无审批发布

二、已确认决策

  1. 「直接发布」调 Create(publishType:"direct",不带 flow),成功后删除原草稿记录,避免同内容两条(决策来源:用户指定按 api-test 的直接发布接口实现)。
  2. 已驳回(status=3)不给发布类按钮:只能改对内容后重新「提交审批」;其「存为草稿」= 仅保存内容,状态保持已驳回。
  3. 编辑态「定时发送」一并改走 Create + timingSendTime,真正排期(修 S5 缺陷)。
  4. 已勾选审批流程且已选审批人时,点「直接发布/定时发送」拦截并提示走「提交审批」;该拦截同时作用于新建态(新建态原行为是静默跳过审批直接发布,属同类绕过,一并按决策口径收紧)。

三、按状态分化的按钮矩阵(目标态)

页面状态 判定 按钮(左→右)
新建 / 转发 !isEdit 直接发布 · 存为草稿 · 提交审批 · 定时发送
草稿续编 isEdit && editStatus===0 直接发布 · 存为草稿 · 提交审批 · 定时发送 · 删除
已驳回重提 isEdit && editStatus===3 存为草稿 · 提交审批 · 删除
旧链路已发布修订 isEdit && editStatus===1 保存 · 提交审批 · 定时发送(Edit 语义,按钮不叫「存为草稿」以免名实不符;带 pushScopes 的已发布记录后端本就拒绝编辑)

四、修改计划(仅 src/views/notice/NewNotice.vue)

  1. 新增 computed publishViaCreate = !isEdit || editStatus===0(发布类动作可用)与 draftBtnText(isEdit && editStatus===1 → 「保存」,否则「存为草稿」)。
  2. doAdd:
    • 入口新增审批拦截——act!=='draft' && act!=='approve' 且 flowModel.enabled && flow.length → Message.info("已配置审批流程,请点击「提交审批」发布") 并 return;
    • 编辑分支拆两路:draft/approve 仍走 sendEdit(approve 追加 sendSubmit,逻辑不变);direct/isTime 且 publishViaCreate → sendCreate({...buildBody(false), publishType:"direct", [timingSendTime]}),成功后 sendDelete(serid),删除失败只 Message.warning 提示去草稿箱手动删除(发布已成,不回滚);direct/isTime 且 editStatus===1 → 维持原 sendEdit 行为;
    • 成功文案:定时 → 「已设置定时发送,到点自动发布」,直接 → 「发送成功」,跳转 /list;draft 文案按状态区分「草稿已保存」/「已保存,状态仍为已驳回」。
    • Create 路径不带 sourceId(避免新公告挂在将被删除的草稿上形成断链血缘)。
  3. 模板 bottom-btn:直接发布 v-if="publishViaCreate";草稿按钮改用 {{ draftBtnText }} 并去掉 v-if="!isEdit";定时发送 v-if="publishViaCreate || editStatus === 1";提交审批、删除 维持;顺序按第三节矩阵排列。
  4. 注释「三态动作区」同步为四态口径。

五、回归验证清单(v14)

  • 新建态:直接发布 / 存为草稿 / 提交审批 / 定时发送 顺序与文案正确
  • 草稿续编:五按钮齐全;直接发布 → /notice/Create 新记录 status=1 且原草稿从草稿箱消失(代码路径已落地,待联调实测)
  • 草稿续编:定时发送 → 新记录 status=1 + timing_published=0 + timing_send_time 到点被 TimingPublishJob 触发(S5 缺陷修复,待联调实测)
  • 草稿续编:存为草稿 → 仍为草稿、留在草稿箱
  • 已驳回:无「直接发布」「定时发送」;存为草稿后状态仍为已驳回(留在已发管理,未回流草稿箱);提交审批可走通
  • 勾选审批并选人后点直接发布/定时发送 → 被拦截提示
  • 删除按钮仍只在草稿/已驳回出现
  • npm run build 通过

六、实施记录(v14 回填)

项 状态 备注
新增 computed ✅ 2026-09-14 publishViaCreate = !isEdit || editStatus===0、draftBtnText(isEdit && editStatus===1 → 保存,否则 存为草稿),置于 editStatus 声明之后
doAdd 审批拦截 ✅ 2026-09-14 接在既有「勾了审批未选人」校验之后:act==="direct" && flowModel.enabled && flow.length → 提示「已配置审批流程,请点击「提交审批」发布」并 return;isTime 归一为 direct,定时发送同样被拦
doAdd 编辑态分流 ✅ 2026-09-14 新增 else if (act==="direct" && publishViaCreate):publishType="direct" → sendCreate → sendDelete(serid)(失败仅 Message.warning("原草稿删除失败,请到草稿箱手动删除"),不回滚已发布结果)→ 文案按 isTime 区分 → 跳 /list;不设 data.sourceId(避免新公告血缘指向待删草稿)。原 else 保留 Edit(draft / approve / 已发布修订),draft 提示按状态拆为「草稿已保存」/「已保存,状态仍为已驳回」
模板按钮区 ✅ 2026-09-14 固定顺序 直接发布(v-if=publishViaCreate) → 存为草稿/保存({{draftBtnText}} 常显) → 提交审批 → 定时发送(v-if=publishViaCreate || editStatus===1) → 删除(v-if=isEdit && (editStatus===0||3));已驳回态无主按钮时草稿按钮提为 primary(:type="publishViaCreate ? 'default' : 'primary'")
注释同步 ✅ 2026-09-14 doAdd JSDoc 由「三态」改「四态」并写清分流;动作区注释改为矩阵口径
备份 ✅ 2026-09-14 改前 NewNotice.vue → .backup/20260914-v14-compose-buttons/(工程非 git 仓库)
构建 + 产物校验 ✅ 2026-09-14 npm run build 通过(40.46s,仅既有 chunk 体积警告);NewNotice-*.js 内 9 个新串(直接发布/存为草稿/提交审批/定时发送/已配置审批流程/已设置定时发送/状态仍为已驳回/原草稿删除失败/保存成功)全部命中
待人工联调(8101 + CAS 登录) ⏳ ① 草稿续编点直接发布后 notice 表新增 status=1 行、原草稿不再出现在草稿箱;② 定时发送后 timing_published=0,到点由 TimingPublishJob 圈人发布(本次修复的 S5);③ 已驳回单「存为草稿」后 status 仍为 3

结论与提示:

  1. 后端零改动。编辑态「直接发布」按 api-test 契约落 Create,必然生成新公告 ID(原草稿被删除)——这是现有接口能力下唯一正确解。若希望「同一条草稿原地发布、保留 ID 与审批历史」,需后端补 notice/Publish?serid=(复用 publish() 的 direct 尾段 + doRealPublish),届时前端把该分支换回单次调用即可。
  2. 行为变更提醒:新建态此前允许「勾了审批、选了人,仍点直接发布」静默绕过审批,本次按决策一并拦截(新建 + 编辑一致)。若只想约束编辑态,给该守卫条件加 isEdit.value 即可。
  3. 与并行的 v13(审批中心 tab 数字)无文件交集:v14 只改 NewNotice.vue。

v15(2026-09-14)· 审批中心改造为「已收到」左右分栏 + 审批人名/教师ID 口径

需求四点:① 通过/驳回按钮显示在公告详情里;② 审批详情按钮为 上一篇 / 下一篇 / 通过 / 驳回 / 打印;③ 审批详情显示审批轨迹与审批流程;④ 所有轨迹与流程显示人名,数据库保存同步教师ID(用户中心 teaId,非本项目表自增 id)。

一、现状与接口证据(2026-09-14 读码 + 本地库直查)

编号 证据 结论
S1 NoticeController L73–77 Detail→detailMap、L84–87 GetDetail→getDetailMap;NoticeService.detailMap L2633–2641 两种 mode 都转 getDetailMap 审批详情应走 GetDetail(不标读),避免审批人点开就把阅读统计搅浑
S2 getDetailMap L2549–2562 已装配 approvalRecords(loadApprovalRecords L2585)+ approvalFlow(toFlowVos L1402,含每步 records/pending)+ canApprove;isMobiRequest() 不返回 需求③后端数据已具备,属前端展示活
S3 前端 types/notice.ts L245–290 已定义 approvalRecords/approvalFlow,但 ApprovalRecordVo 缺后端已下发的 assigneeTeaIds 补一个可选字段即可
S4 本地库 notice_approval 6 行 operator_account=operator_name=lijing;notice_approval_record 1 行 approver_name=lijing;notice.publisher_name=lijing 需求④是真缺陷:凡取自登录态的“姓名”都落成了登录名
S5 NoticeService.currentName() L161–164 直接返回 user.getDisplayName();v13 已证明 DefaultLoginUserProvider 用 token 串同时喂 loginName/displayName/userId 根因:displayName 恒等于登录名且无回退
S6 notice_approval_step:approver_tea_ids=[10007,10006,10008] 正确、approver_names 有真名,但 approver_accounts 全 NULL,历史行含 ["测试教师2","ceshiteacher","李 Jing"](账号被当姓名兜底) 审批人快照的 teaId 口径本就正确(sys_approver.id 从不下发:SysApprover L33–51,DictService L290/308 仅池列表 UI 用);缺的是“操作人”侧
S7 NoticeApproval / NoticeApprovalRecord 实体均无 teaId 列;application.yml L24 ddl-auto: update 加 operator_tea_id / approver_tea_id 两列由 JPA 自动建,无需手写 ALTER
S8 批量人名通道 classService.getSchTeaList(schId) → SchTeaList{teaName, userTeaSerID}(无 account 字段);resolveUserName(account) L2204 逐条外呼 teaId→姓名可批量;account→姓名只能逐条,需去重缓存
S9 approve(serid, opinion?) L328、reject(serid, opinion?/remark?) L339 无审批人入参;requireStepApprover L681、canApproveNow L1298 判定“我是本步待签人” 详情区按钮直接复用现有接口,签名不动

二、已确认决策

  1. 左列用已收到同款卡片:标题 +(重要/置顶/状态徽标)+「部门·时间」+ 副行审批进度与当前审批人;顶部保留四 tab(含 v13 条数)与关键字搜索,pageSize=50 滚动。
  2. 通过/驳回仍弹窗填意见(通过默认“同意”、驳回必填),仅触发点从表格操作列移到详情按钮行。
  3. 需求④做到写入链 + 表结构层:修 currentName()、两张留痕表补 teaId 列并写入、历史数据出回填脚本。
  4. 上一篇 / 下一篇 在当前筛选结果列表内移动(与左列选中联动),不复用 GetNextPrev(它按已发/已收全局序列,会跳出当前 tab)。

三、修改计划

前端 src/views/notice/ApprovalList.vue(整页重构)

  1. 布局改 .accept-wrap:左 .title-list(tab + 搜索 + 卡片 + 分页)+ 右 .detail-content;样式从 NoticeAccept.vue 移植并补审批块样式(scoped,不外溢)。
  2. 详情源换 noticeApi.getNoticeDetails(serid);渲染:标题头 + 按钮行(上一篇/下一篇/通过/驳回/打印)+ 元信息(来源部门/发布人/发布时间/接收范围)+ 审批状态 Alert + 审批流程(approvalFlow 每步:步名·或签/会签·状态·审批人姓名·每人结论与时间·意见·当前待签人)+ 审批轨迹(approvalRecords 时间线)+ 正文 + 附件(预览/下载/全部下载)。
  3. 交互:selected 索引驱动左列高亮;上一篇/下一篇在 page.content 内移动并加载详情;通过/驳回成功后同时刷新列表与当前详情(本步流转后进度与待签人会变)。
  4. canApprove=false 或状态非 pending 时不显示通过/驳回,改为“等待他人审批 / 该单已处理”文字提示。
  5. types/notice.ts 的 ApprovalRecordVo 补 assigneeTeaIds?: number[]。

后端 E:\YHXT\java\CampusNotice(需求④)

  1. currentName():displayName 为空或等于 loginName 时回退用户中心真实姓名(异常保持原值),加请求内去重缓存避免多次外呼。
  2. NoticeApproval 加 operator_tea_id、NoticeApprovalRecord 加 approver_tea_id;saveApprovalRow L1533、saveRecordRow L934 写入 currentTeaId()。
  3. 读取兜底:loadApprovalRecords、toFlowVos 在姓名缺失或姓名==账号时补名——先按 teaId 走 getSchTeaList 批量 map,仍缺的按去重账号逐个 resolveUserName。
  4. 历史回填脚本 .scratch/backfill-approver-name-teaid.mjs:经 .scratch/oapi-call.mjs 拉本校教师(account↔teaId姓名),生成 UPDATE notice_approval / notice_approval_record / notice / notice_approval_step 语句,先预览后执行、可重复跑。

四、回归验证清单(v15)

  • 审批中心为左右分栏,左列卡片含进度与当前审批人,四 tab 数字仍正确
  • 点卡片右侧出详情;上一篇/下一篇 在当前 tab+关键字结果内移动,首/末条置灰
  • 详情按钮行含 通过/驳回/打印;非当前审批人只见 打印 与等待提示
  • 通过弹窗默认“同意”、驳回意见必填;提交后列表与详情同时刷新
  • 审批流程区显示每步 步名/或签会签/审批人人名/状态/意见;轨迹时间线显示提交与各级处理人人名(存量数据由读取兜底 + 回填双路保障,不再依赖 lijing)
  • 新送审与新审批动作后,operator_tea_id/approver_tea_id 落用户中心 teaId,姓名落真实姓名(需 8101 + CAS 登录实测)
  • 回填任务对历史 6 条 notice_approval、1 条 notice_approval_record、notice.publisher_name 生效且幂等(需带开关重启实测)
  • mvn compile(JAVA_HOME=jdk-1.8)与 npm run build 均通过

五、实施记录(v15 回填)

项 状态 备注
前端整页重构 ✅ 2026-09-14 ApprovalList.vue 由「表格 + 跳详情页」改为 .accept-wrap 左右分栏:左列 tab(含 v13 条数)+搜索+卡片(标题/置顶/重要徽标 + 部门·时间 + 审批进度 + 当前审批人)+ Page simple 分页;右列详情。删除 Table/MyConfirm/useRouter 依赖,详情源改 getNoticeDetails(GetDetail,不标读)
详情按钮行 ✅ 2026-09-14 上一篇 ‹ / 下一篇 › / 通过 / 驳回 / 打印;goStep(±1) 在 page.content 内移动并联动左列高亮,首末置灰(prevDisabled/nextDisabled);通过/驳回 仅 canAct = approvalStatus==='pending' && canApprove 时出现,否则 pending 态显示「等待他人审批」文字
审批流程 + 轨迹 ✅ 2026-09-14 流程块渲染 approvalFlow[](步名/或签·会签/步骤状态色/审批人姓名/每人 records 结论+时间+意见/pending 待签);轨迹块渲染 approvalRecords[] 时间线(提交·通过·驳回 + 人名 + 意见 + 提交行「选定审批人 N 人」);统一走 personName(name, account),缺值显示「—」而非账号
动作后刷新 ✅ 2026-09-14 run(keepSelected=true):重拉列表与 tab 数,优先保持原公告选中(该单已流转出当前筛选时回落首条),列表与详情同步刷新
前端类型 ✅ 2026-09-14 types/notice.ts 的 ApprovalRecordVo 补 assigneeTeaIds?: number[](后端早已下发)
后端写入链 ✅ 2026-09-14 currentName():displayName 空或等于 loginName → cachedUserName(account)(用户中心真名,ConcurrentHashMap 进程内缓存,>2000 清空防无界);saveApprovalRow 落 operator_tea_id、saveRecordRow 落 approver_tea_id(均取 currentTeaId(),签名不变)
后端表结构 ✅ 2026-09-14 NoticeApproval.operatorTeaId → operator_tea_id、NoticeApprovalRecord.approverTeaId → approver_tea_id;ddl-auto: update 自动建列,无需手写 ALTER
后端读取兜底 ✅ 2026-09-14 loadApprovalRecords 改 for 循环 + nameMissing() 判定 + 懒加载 teaNameMap(schId)(一次教师名单外呼)→ displayApproverName()(teaId → 账号缓存 → 原值);toFlowVos 同步处理步骤审批人姓名快照与每步 records 姓名,teaId 仅在长度与姓名列表一致时用于对齐
历史回填(方案变更) ✅ 2026-09-14 计划里的 .scratch/backfill-approver-name-teaid.mjs 未采用:实测猜 /oapi/user/* 路径全 404,而应用内已有签名客户端,Node 侧硬编 SDK 路径太脆。改为 INoticeService.backfillApproverIdentity() + job/ApproverIdentityBackfillRunner(ApplicationRunner,@ConditionalOnProperty(campus-notice.backfill-approver-identity=true) 才执行):按账号去重解析后回填 notice_approval(姓名+teaId)、notice_approval_record(姓名+teaId)、notice_approval_step(按 teaId 用教师名单权威姓名覆盖快照)、notice.publisher_name,返回 {trail,record,step,notice,unresolved} 统计;只改需改的行,可重复执行
mock 对齐 ✅ 2026-09-14 assets/mock/noticeapi.ts 详情补 approvalRecords: approvalTrailOf(n)(提交行 + 各步 records,按时间升序),此前 dev:mock 无轨迹数据可渲染
备份 ✅ 2026-09-14 .backup/20260914-v15-approval-detail/:ApprovalList.vue、types/notice.ts 改前副本(NoticeService.java 基线为其时磁盘状态)
构建与载荷验证 ✅ 2026-09-14 mvn -q compile BUILD SUCCESS(含并行 v16–v18 改动后复验);npm run build 通过(44.21s,仅既有 *zoom SCSS 警告与 chunk 体积警告);dist 内 ApprovalList chunk 命中 15 个新串;起 dev:mock 实测 ApprovalList?auditState=pending/approved/rejected/all 行数 1/2/1/3、ApprovalTabCount {1,2,1,3}、GetDetail?serid=3/5/8 的 approvalFlow(步名/会签·或签/状态/人名/pending/records)与 approvalRecords(提交+通过+驳回,均为人名)字段与模板消费口径一致,验完已关停 8077 端口进程

结论与遗留:

  1. 与并行会话的交叠(重要):本次整页重写落地后,v19/v19.1 在同一文件的左列 tab 样式上继续改造(nowrap + 等宽 + 去掉库分段控件的 box-shadow/伪元素),v16 扩展了本次新增的 cachedUserName(教师ID形态回退 getUserById)并加了 displayPublisherName,v18 下线「审核设置」、v20 改了 NewNotice.applyTiming。当前磁盘状态是这些改动的叠加结果,mvn compile 与 npm run build 均在叠加后复验通过;后续再动 ApprovalList.vue/NoticeService.java 前必须先重读磁盘。
  2. 需求④的显示侧已闭环(新数据写入真名 + 存量数据读取兜底 + 可重跑回填);notice_read.reader_name 与 notice_approval_step.approver_accounts(历史全 NULL)未纳入本次回填范围——前者由阅读回执展示层的账号→姓名映射兜底,后者 v13-E2 已在写入侧补齐,存量不影响展示(姓名快照走 teaId 路径)。
  3. 待人工实测两项(需 IDEA 起 8101 + CAS 登录):新送审/新审批动作后两列 teaId 与真名落库;带 -Dcampus-notice.backfill-approver-identity=true 重启一次后历史行被改写、二次重启无新增(幂等)。
  4. 本机无 headless 浏览器(未装 puppeteer/playwright),左右分栏的真实字体渲染与选中态视觉需在 #/approval 人工过一眼。

六、v16:发布人显示人名(不再显示教师ID/账号)(2026-09-14)

6.1 需求与错误清单

编号 现象 期望
E1 列表页「发布人」列、详情页 TeaName、统计图例按发布人分布,显示的是账号(如 lijing)或教师ID(如 10008) 一律显示教师真实姓名

6.2 根因证据(用户中心 oapi 实测,账号 lijing→id 10008)

编号 证据 结论
S1 /oapi/user/byLoginName?loginName=lijing → {id:10008, account:"lijing", name:"李晶"};/oapi/user/byId?id=10008 → 同 name:"李晶" 姓名可从用户中心解析
S2 /oapi/user/byLoginName?loginName=10008 → code:0 且 data 空 按数字教师ID走 byLoginName 查不到,必须走 byId
S3 本地 notice.publisher_name 6 行全为 lijing(=publisher_account) v15 前旧代码只存 displayName(=登录名)落库,快照即账号
S4 NoticeService.currentName() 的 cachedUserName 仅调 getUserInfo(String)(=byLoginName) 当发布人落库值是数字教师ID时解析失败→回退ID本身,即用户所述「数据库保存的是教师id 不是账号」

6.3 修复实施(仅后端 NoticeService.java,前端与契约不动)

  1. cachedUserName:byLoginName 查不到且值纯数字时,回退 YschBaseOpenApi.getUserById 再解析(覆盖教师ID形态)。写侧新发布快照随之落真实姓名。
  2. 新增 displayPublisherName(snapshot, account):快照已是正常人名直返;快照==账号 或 快照为数字ID 时经用户中心(byLoginName→byId 兜底)解析,解析失败保持原值。
  3. toVo 统一调用 displayPublisherName 归一 publisherName——列表页与详情页(getDetail/getDetailMap→TeaName)共用 toVo,存量错误数据无需回填脚本即刻显示人名。
  4. 统计 byPublisher 分组 key 同经 displayPublisherName 解析,图例不再出现账号/ID。

6.4 回归清单(v16)

  • 列表/详情/统计三处「发布人」均显示真实姓名,不再出现 lijing 或数字ID
  • 存量 6 条历史数据在未回填情况下前端即显示人名
  • 新发布公告 publisher_name 落真实姓名;publisher_account 仍为登录标识
  • 用户中心不可达时优雅回退原值、不抛异常、有 cachedUserName warn 日志
  • mvn compile(JAVA_HOME=jdk-1.8)通过(已验证 BUILD SUCCESS)

v17(2026-09-14)· 「已收到」三 tab 显示数量(新增后端统计接口)+ 详情 上一篇/下一篇/打印

需求两点,均落在「已收到」NoticeAccept.vue 左右分栏:① 左列 全部 / 未读 / 已读 三个 tab 显示条数,后端补统计接口;② 右侧公告详情加 上一篇 / 下一篇 / 打印。 与 v15(审批中心左右分栏)无文件交集:v17 只动 NoticeAccept.vue、新抽的打印工具、NoticeDetails.vue(去重复)、mock、后端 Notice*。v15 的「上下篇在当前 tab 结果内移动」是审批中心口径,本页口径不同(见决策 2)。 行号按本轮读码记录;v15/v16 并行会话在 NoticeService 头部(currentName/cachedUserName/toVo 区)持续改动会使命名行号漂移,以方法名为准。

一、现状与接口证据(2026-09-14 读码)

编号 证据 结论
S1 NoticeAccept.vue L189-193 RadioGroup v-model="readType"(0 全部/1 未读/2 已读)三个 Radio 只有文字;L183 顶部「未读 N 条」取 page.unreadCount;L236-239 详情 .ops 只有「打印」 tab 无数字、详情无上下篇,属功能缺失而非样式问题
S2 NoticeController L49-54 List → userList,@SortDefault({"topmost","createTime"} DESC);NoticeService.userList(L1905-1929);userSpec(L1969-1991)已实现 readType=0/1/2 三分支 三个 tab 的条数 = 同一 Specification 在 readType=0/1/2 下 count 三次,可直接复用,无需另立查询口径
S3 现有 unreadCount(L1921-1922)= count(visibleSpec(account,true)) − noticeReadRepository.countByReaderAccount(account),再 clamp≥0 口径不同源:减数不带 schId/状态/范围可见性过滤,读者曾读过但已删除/已撤回/跨校的通知会长期虚减未读数(clamp 把负数掩盖成 0)。新接口以 userSpec 为准,并顺手让 List.unreadCount 复用同一结果
S4 NoticeService.getNextPrev(L2355-2381)type=1(已收)分支只读 noticeReadRepository.findNoticeIdsByReaderAccount() 再 reverse 序列只含已读公告;未读公告 indexOf = −1 → prevId/nextId 恒 0,即「未读公告点上下篇没反应」。且不按 status/schId/范围可见性与 keyword/type 过滤,也不含 topmost 排序
S5 NoticeQuery.orderByTime(L47)后端从未参与排序(appendCommonPredicates L2054-2072 只处理 keyword/type/deptId/时间;PageUtils.toPageable 不读它) 列表实际顺序恒为 topmost DESC, createTime DESC;相邻查询按同键 + id DESC 做稳定 tie-break;不顺手“修”orderByTime(登记遗留 E2)
S6 全工程 grep EntityManager 0 命中(Service 只用 Repository);Notice.content 为 LONGTEXT 普通字段(L36-38,非懒加载) 为拿两个邻居而 findAll(spec, Sort) 会整表加载正文(现有 type=0 分支正是这个反模式)→ 本次用「count 判定 + size=1 定向分页」,每次只落 1 行实体
S7 前端 noticeApi.getNextPrevNotice(serid, type) + NextPrevVo 已存在,NoticeDetails.vue L19-20/L38-40/L182-183 已接(navType 随视角取 0/1) 契约与调用位就绪,v17 只扩展可选筛选参数并保持不传参时的行为可用
S8 打印两份逐字重复实现:NoticeAccept.vue L144-164、NoticeDetails.vue L111-131(只有 标题 + 来源/发布人/时间 + 正文) 缺接收范围、附件清单、回执提示,且两处会持续漂移 → 抽公共工具,两页共用
S9 同类范式已在 v13 落地:ApprovalTabCount(Controller L379-381 + ApprovalTabCountVo + ApprovalList.vue L28-38 tabCounts/tabCountText(为 0 不显示)/异常静默保留上次) ReadTabCount 的命名、VO 形态、前端刷新与静默策略全部对齐 v13
S10 /api/* 受 CAS 拦(curl 不可直测),手工联调入口 resources/static/api-test.html(收件箱块 L735-830 已调 List/Detail/GetNextPrev,L273-275 三个 tab 按钮) 顺带在 api-test 收件箱 tab 上渲染数量,让新接口可在控制台自证
S11 mock:src/assets/mock/noticeapi.ts L722 /api/notice/List(本地算 unreadCount)、L1006 /api/notice/ApprovalTabCount 不同步补 mock,则 .env.mock 模式下新 tab 数字空白、上下篇跳不动

二、已确认决策(本轮提问回复)

  1. 数量口径 = 随筛选联动:ReadTabCount 接收与 List 相同的 keyword / type(另支持 deptId、时间范围)过滤,readType 不作为入参(它就是要统计的三个桶),恒满足 全部 = 未读 + 已读。搜索、切类型、标读、一键已读后刷新。
  2. 上下篇序列 = 后端「我可见的已收公告」序列,沿用当前 keyword / type 筛选,但不吃 readType。 关键理由:在未读 tab 点开第一条即被标读,若序列带 readType=1,当前 serid 已不在该集合内 → 判定「当前公告不在筛选集」→ 上下篇立刻全灰、用户卡在原地。不吃 readType 后,未读/已读两个 tab 共用同一条「全部可见」序列,跳得动。 跨 tab 目标不在左列当前结果内时:若 readType!==0 先把左列切回「全部」重载再定位高亮;仍找不到则不高亮(selected=-1),不擅自改用户的关键字/类型筛选。
  3. 打印抽公共工具并完善:新增 src/utils/noticePrint.ts,纯函数 buildPrintHtml(detail) + 薄封装 printNotice(detail);打印件 = 标题 / 来源部门·发布人·发布时间 / 接收范围 / 正文(原样富文本)/ 附件文件名与大小清单 / 需回执时一行提示 / 页脚打印时间;带 @media print 与 A4 边距,不含按钮等界面元素。NoticeDetails.vue 一并换用,消除 S8 重复。

三、修改计划

后端 E:\YHXT\java\CampusNotice

# 文件 改动
B1 model/notice/ReadTabCountVo.java(新增) int all / unread / read,javadoc 写明「口径 = GET /api/notice/List 同过滤下 readType=0/1/2 的 count;不含 readType 入参」
B2 service/INoticeService.java 加 ReadTabCountVo readTabCount(NoticeQuery query);getNextPrev 签名扩为 (Integer serid, String type, NoticeQuery query)
B3 service/impl/NoticeService.java ① userSpec 增加 readTypeOverride 重载,原三参版委托 query.getReadType()(单一口径,不复制过滤逻辑);② 新增 readTabCount:readIds/receiverNoticeIds 各取一次 → 三次 noticeRepository.count(spec);③ userList 的 unreadCount 改取同一次 readType=1 的 count(修 S3,并省掉一次全表 count);④ 重写 getNextPrev 的 type=1 分支:当前公告须落在筛选集内否则 0/0;prev = 排序键 (coalesce(topmost,0), createTime, id) 大于当前者中最小(PageRequest.of(0,1, ASC)),next = 小于当前者中最大(DESC);NULL topmost 用 cb.coalesce 归 0 与列表 @SortDefault 对齐;type=0 分支本次不动(遗留 E1)
B4 controller/api/NoticeController.java 新增 @GetMapping("ReadTabCount")(绑定 NoticeQuery);GetNextPrev 加 NoticeQuery query 绑定(参数可选,旧调用不传照旧工作)
B5 resources/static/api-test.html 收件箱三 tab 渲染数量(调 ReadTabCount,随 keyword/type 刷新);详情弹窗 GetNextPrev 补传当前筛选

前端 src/

# 文件 改动
F1 types/notice.ts 加 ReadTabCountVo { all; unread; read }(就近放在 NoticePageResult 之后)
F2 api/module/notice.ts 新增 readTabCount(params) → GET /api/notice/ReadTabCount;getNextPrevNotice(serid, type, filters?) 把 keyword/type 拼进 query
F3 views/notice/NoticeAccept.vue ① Radio 文本改 全部 (n)(n=0 只显示 全部,静默策略同 v13),loadTabCounts() 与 run() 一起触发(搜索/切类型/标读/一键已读后刷新);② 详情 .ops 行改 上一篇 / 下一篇 / 打印,prevId/nextId=0 置灰,点击走 loadDetail(id, true) 并按决策 2 定位左列高亮;③ run() 后按 detail.serid 重算 selected(修「列表重载后高亮错位」,上下篇联动必需);④ onPrint 改调 noticePrint
F4 utils/noticePrint.ts(新增) buildPrintHtml(detail) / printNotice(detail)(弹窗被拦截返回 false,调用方提示)+ receiveScopeLabel(detail)(pushScopes → ReceiveList 的 ALL/[/兜底三态,与 NoticeDetails 现口径一致)
F5 views/notice/NoticeDetails.vue onPrint 换用 printNotice;receiveLabel computed 换用共享 receiveScopeLabel
F6 assets/mock/noticeapi.ts 补 /api/notice/ReadTabCount(与 /List 共用同一过滤函数算三桶);/api/notice/GetNextPrev 改为「可见已收序列(含未读)+ keyword/type 筛选」,忽略 readType

遗留登记(不在 v17 修)

编号 内容
E1 GetNextPrev?type=0(已发序列)仍是「全量加载实体 + 无筛选」老实现,NoticeDetails 管理视角受其影响;可与本轮同法修,待单独排期
E2 NoticeQuery.orderByTime 后端从未生效(S5),前端固定传 "1" 掩盖了它——要么后端接上排序,要么删字段,需与 api-test 契约一起定
E3 InitializeRed(一键已读)返回条数与改口径后的 List.unreadCount 是否一致,回归时确认

四、回归验证清单(v17)

  • 「已收到」左列 全部 / 未读 / 已读 带条数且 全部 = 未读 + 已读;输入关键字、切「普通/重要」后数字随列表变化
  • 点开未读公告(自动标读)后 tab 数字与顶部「未读 N 条」同步 −1;「一键已读」后未读归 0、全部不变
  • 详情右上依次 上一篇 / 下一篇 / 打印;首条「上一篇」置灰、末条「下一篇」置灰
  • 未读 tab 第一条点开后上下篇仍可跳(决策 2 核心用例);跳到不在当前左列的已读公告时,左列自动切「全部」并高亮该条
  • 列表重载后高亮跟随 detail.serid,不错位到相邻条目
  • 打印件含 标题/来源部门/发布人/发布时间/接收范围/正文/附件文件名清单(无附件不出现该段)/需回执时一行提示;无按钮等界面元素,A4 样式生效
  • NoticeDetails.vue 的打印与接收范围文案与「已收到」一致(同一工具函数)
  • 后端:GET /api/notice/ReadTabCount 三值与 GET /api/notice/List?readType=0|1|2 的 total 两两相等
  • mvn compile(JAVA_HOME=jdk-1.8)BUILD SUCCESS 与 npm run build(43.32s)均通过;.scratch/verify-accept-print.mjs 22/22 全绿(其余项需登录环境人工回归,见五、实施记录)
  • 改前文件已备份至 .backup/20260914-v17-accept-count-nav/(含 java 源码树快照 be-src-full.tgz;工程非 git 仓库)

五、实施记录(v17 回填)

待实施后回填。

项 状态 备注
B1 ReadTabCountVo ✅ 新增 model/notice/ReadTabCountVo.java(all/unread/read),javadoc 锁死「与 List 同过滤、readType 非入参、all = unread + read」
B3① userSpec 重载 ✅ 四参版 (query, readIds, receiverNoticeIds, int readType) 承载全部过滤逻辑,三参版只委托 query.getReadType();新增阅读桶常量 ALL/UNREAD/READ 替换裸 0/1/2
B3② readTabCount ✅ readIds/receiverNoticeIds 各取一次,三次 noticeRepository.count(userSpec(...))
B3③ 未读数口径统一 ✅ 新增 countUnreadForCurrentUser();userList.unreadCount 与 statistics.unreadForCurrentUser 都改走它,删除 S3 的「可见总数 − 全部已读记录」相减(并清掉 statistics 里失效的 account 局部变量)
B3④ getNextPrev 已收分支 ✅ 新增 adjacentReceivedId(base, current, newer) + noticeKeySidePredicate:先 count(base ∧ id=current) 判定当前条是否在集合内(不在则 0/0),再按排序键 (coalesce(topmost,0), createTime, id) 三档字典序 OR 取相邻;每次只 PageRequest.of(0,1,sort) 拉 1 行。旧实现(只遍历 notice_read、未读公告跳不动、为取邻居整表加载 LONGTEXT 实体)删除
B4 Controller ✅ 新增 GET notice/ReadTabCount(绑定 NoticeQuery);GetNextPrev 加 noticeType + NoticeQuery 绑定,由控制器把 noticeType 写进 query.setType(...)、并把 readType 置 null
B5 api-test.html ✅ 三个阅读状态按钮加 <span class="seg-cnt"> + .seg .seg-cnt 样式;loadInboxCounts() 调 ReadTabCount(随 keyword/type 刷新),在 loadInbox 内非阻塞调用;详情弹窗 GetNextPrev 补传 keyword/noticeType(仅收件箱视角)
F1–F2 类型与接口封装 ✅ types/notice.ts 加 ReadTabCountVo 并入头契约清单;api/module/notice.ts 加 readTabCount() 与 getNextPrevNotice(serid, type, filters?: NextPrevFilter)
F3 NoticeAccept ✅ tab 文案 全部 (n)(0 不显示、异常静默);详情操作行 上一篇 / 下一篇 / 打印(prevId/nextId=0 走 .op.disabled 置灰,goNav 内部再短路一次);reposition() + ensureHighlighted() 让高亮跟随 detail.serid,目标不在当前结果时自动切「全部」重载、仍找不到则不高亮;左列条目函数式 ref + scrollIntoView 滚进可视区;allRead 改 await 串行刷新
F4 utils/noticePrint.ts ✅ 新增 buildPrintHtml(纯函数)/printNotice(弹窗被拦返回 false;onload + 1.5s 兜底触发打印)/receiveScopeLabel;自由文本统一 HTML 转义、正文按原样富文本;A4 @page 样式 + 附件清单 + 回执行 + 打印时间
F5 NoticeDetails ✅ 本地重复的 onPrint 与 receiveLabel computed 换用公共工具(两处文案口径从此同源)
F6 mock ✅ 抽 inboxVisible(query) 供 List/ReadTabCount/GetNextPrev 共用;顺带补上 mock 此前完全缺失的 readType 分桶,List.unreadCount 改为全局未读(对齐后端),GetNextPrev 按 type 分流(此前忽略 type)
备份 ✅ .backup/20260914-v17-accept-count-nav/:前端 5 文件(types.notice.ts/api-notice.ts 前缀区分同名)+ 后端 4 文件 + be-src-full.tgz(java 源码树快照,供与并行会话交叉时回滚)
构建验证 ✅ mvn -o compile BUILD SUCCESS;npm run build 通过(43.32s,仅既有 chunk 体积/CSS 告警);产物字符串命中:ReadTabCount→index-*.js、noticeType + 打印窗口被浏览器拦截→NoticeAccept-*.js、打印工具独立分片 noticePrint-*.js(两页共用的实证)
打印回归脚本 ✅ .scratch/verify-accept-print.mjs 22/22 通过(要素齐全 / 条件段落 / 转义安全 / 接收范围三态 / 无界面文字混入);打桩命令 npx esbuild src/utils/noticePrint.ts --bundle --format=esm --platform=node --external:*.vue --tsconfig=tsconfig.json --outfile=.scratch/noticePrint.bundle.mjs

关键纠偏与踩坑:

  1. type 参数名冲突(本轮最重要的发现):GetNextPrev 的 type 一直是「序列类型(0 已发/1 已收)」。若按原计划让 NoticeQuery 直接绑定请求参数,type=1 会被 Spring 顺手绑进 NoticeQuery.type(公告重要程度),于是每条已收公告的跳篇序列都会被误筛成「普通」。改为新增 noticeType 显式入参、由控制器写回 query.setType(...);前端 NextPrevFilter 类型也只暴露 noticeType,从签名上堵住再传 type 的路;api-test.html 同步。
  2. 序列刻意不带 readType 的实证价值:最初按「与列表完全同一条序列」实现,未读 tab 里点开第一条即标读 → 该条离开未读集合 → count(集合 ∧ id=当前)=0 → 上下篇双双置灰、用户卡死。这正是决策 2 预判的坑,落到代码后确认必须忽略 readType,控制器再兜一层 setReadType(null)。
  3. 本次自引入的编译错误:findAll(Specification, Pageable) 返回 Page<Notice> 而非 List<Notice>(首次 compile 报「不兼容的类型」),已改为 Page + getContent()。教训:定向取 1 行也要按 Spring Data 的返回类型写。
  4. 与并行会话交叉:NoticeService.java 在本轮期间被 v15/v16 会话连续改写,两次触发 Edit 的「文件已修改」守卫(须重读并在 grep 里重新定位——目标行一度从 2499 漂到 2466);期间 compile 还出现过 buildSteps 参数不匹配的两条 ERROR,属对方 v15 送审改造的中间态,非本次改动引入,本轮未介入;最终 compile 仅剩我那条类型错误并已修。
  5. vue-tsc 本机不可用(既有环境缺陷),类型核对仍以 npm run build + 与后端 Controller 人工契约比对为准。

遗留与更新:

  • E3 部分收敛:List.unreadCount、Statis.unreadForCurrentUser 已与 tab 统计同源;但 initializeRed() 仍用 visibleSpec(account, **false**)(不带学校过滤)批量补阅读记录,理论上会给跨校可见的已发布通知记账,与 tab 统计口径不严格一致 → 保留登记,待单独处理。
  • E1(GetNextPrev?type=0 已发序列仍全量加载实体 + 无筛选)、E2(orderByTime 后端从未生效)原样保留。
  • 侧边栏栏目名仍取平台 GetUserMod.name(若仍显示旧叫法,须平台后台改名,与本轮无关)。

待人工联调(8101 + CAS 登录):

  • 「已收到」三 tab 数字与 List?readType=0/1/2 的 total 三者两两相等;输入关键字 + 选「重要」后数字随列表变化
  • 未读 tab 第一条点开即标读,上下篇仍可继续跳;跳到已读公告时左列自动切「全部」并高亮、滚动到该条
  • 序列首条「上一篇」置灰、末条「下一篇」置灰;当前公告不在筛选结果内时两键置灰而非乱跳
  • 两处打印入口的打印件都含接收范围与附件清单;含图公告的图片能出现在纸上(1.5s 兜底);浏览器拦弹窗时出现提示文案
  • 一键已读后未读归 0、全部不变;InitializeRed 返回条数与未读数是否一致(E3)
  • mock 模式(npm run dev:mock)下三 tab 数字与 readType 分桶生效(此前 mock 根本不按 readType 过滤)

v18(2026-09-14)· 去掉「审核设置」功能;写公告审批人选择直显教师树

一、需求与现状

用户要求:① 去掉审核设置功能;② 写公告页「审批 → 选择审批人」弹窗直接显示教师树接口(base/getTeaTree?toUser=true,含教师叶子)。

现状证据:

编号 证据
S1 ApprovalFlowEditor.vue v7 起把选人树过滤为「教师树 ∩ 审批人池(dict/ApproverPool 启用者 teaId)」;池为空时 loadPickerTree 返回 [] 并 warning「请先在审核设置中添加审批人」→ 弹窗左右两栏全空白(用户截图即此形态)
S2 审核设置页 views/notice/ApprovalSetting.vue(路由 /approval-setting,modId 6)维护 sys_approver 白名单;NoticeService.buildSteps(validatePool=true)(L636 正式送审)经 requireInApproverPool 拦截池外教师,报错文案引导去「审核设置」
S3 菜单真实来源=平台 GetUserMod(modId 6=审核设置);前端仅删路由/别名不够——normalizeModList 解析失败仍会保留原始 page 渲染菜单项,点击跳空路由。须在菜单管线剔除 modId 6
S4 前端只发 approverTeaIds(教师ID路径);FlowStepItem.approvers(账号路径→bindAccounts 池校验)新版前端与 api-test 均不使用,属遗留兼容,本轮不动

二、改动清单

前端 src/(工程非 git,改前备份 .backup/20260914-v18-drop-approval-setting/)

# 文件 改动
F1 components/ApprovalFlowEditor.vue 删 v7 池过滤整块(poolTeaIds/loadPool/filterTreeByPool/loadPickerTree);pickerSources loader 改直连 loadTeaTree()(全量教师树,含教师叶子);bindStepByTeaIds 去「(不在审批人名单)」标注与 loadPool 调用;删 approvalSettingApi/ApproverPoolItem import;注释同步
F2 views/notice/ApprovalSetting.vue 删除(移入备份目录)
F3 router/routes.ts 删 /approval-setting 路由;头注释 modId 对照表去掉 6=审核设置
F4 assets/config/menu.ts 删本地兜底菜单 id 6 条目
F5 utils/menuRoute.ts 删 审核设置/审批设置 别名;normalizeModList 顶层+children 剔除 modId===6 模块(平台仍授权时也不显示,前端契约兜底)
F6 components/layout.vue getMenuIcon 删 /approval-setting 行
F7 api/module/notice.ts 删 ApprovalSettingApi 类与 approvalSettingApi 导出、ApproverVo/ApproverPoolItem import
F8 types/notice.ts 删 ApproverVo、ApproverPoolItem 接口
F9 assets/mock/noticeapi.ts 删 dict/ApproverPool、Approver、AddApprover、UpdateApprover、DelApprover mock 与 GetUserMod 中 id 6 模块;approvers 数据保留(mock 审批流/记忆仍消费)

后端 E:\YHXT\java\CampusNotice(git 仓库,diff 可回滚)

# 文件 改动
B1 service/impl/NoticeService.java 去池白名单校验:buildSteps 删 validatePool 参数与「沿用草稿定义时的池校验」块;bindApprovers 合并为单签名(不校验池);删 requireInApproverPool;bindAccounts(账号路径遗留)与 sysApproverRepository 保留不动
B2 controller/api/DictController.java 等审批人维护接口 不删(表与接口保留,前端不再调用;彻底下线另行排期)

三、回归清单(v18)

  • 写公告/草稿箱提交审批弹窗:勾选审批 →「选择审批人」打开穿梭式弹窗,左栏显示完整教师树(组→教师叶子可勾),不再出现空树与「请先在审核设置…」提示
  • 选任意本校教师送审成功,后端不再报「不在本校审批人名单」
  • 侧边栏不再出现「审核设置」菜单(mock 与平台仍返回 modId 6 两种来源均验证)
  • 直接访问 #/approval-setting 落到 404/兜底,不白屏崩溃
  • ApproverSuggest 记忆带入、编辑回显 chip 姓名正常
  • npm run build、后端 mvn compile 通过;.scratch/verify-menu-order.mjs、verify-menu-name-bind.mjs 重打桩后全绿

四、实施记录(v18 回填)

项 状态 备注
F1 ApprovalFlowEditor.vue ✅ 2026-09-14 池过滤整块(poolTeaIds/loadPool/filterTreeByPool/loadPickerTree)删除;pickerSources loader 直连 loadTeaTree()(base/getTeaTree?toUser=true 归一化全量教师树,含教师叶子);bindStepByTeaIds 去「(不在审批人名单)」标注;approvalSettingApi/ApproverPoolItem import 清除
F2 ApprovalSetting.vue ✅ 已移入系统回收站;副本存 .backup/20260914-v18-drop-approval-setting/views/
F3 routes.ts ✅ /approval-setting 路由删除;头注释 modId 对照表登记「6 已于 v18 下线」
F4 menu.ts ✅ 本地兜底菜单 id 6 条目删除
F5 menuRoute.ts ✅ 审核设置/审批设置 别名删除;normalizeModList 新增 DROPPED_MOD_IDS={6} 剔除(顶层+children,按 modId ?? id 判定)——平台仍授权该模块时侧边栏也不出现,且不点不进
F6 layout.vue ✅ getMenuIcon 去 /approval-setting 行
F7 api/module/notice.ts ✅ ApprovalSettingApi 类、approvalSettingApi 导出、ApproverVo/ApproverPoolItem import 删除
F8 types/notice.ts ✅ ApproverVo、ApproverPoolItem 接口删除
F9 mock ✅(偏差) dict/ApproverPool、Approver、AddApprover、UpdateApprover、DelApprover mock 端点与随之失效的 approverSeq/approverPoolAccounts 已删;GetUserMod 中 id 6「审核设置」条目保留(偏差①:故意模拟「平台仍授权」场景,端到端验证 F5 剔除链路)。approvers 演示数据保留(mock 审批流/ApproverSuggest 仍消费)
B1 NoticeService.java ✅ buildSteps 去 validatePool 参数与「沿用草稿定义时池校验」块;bindApprovers 合并为单签名(仅按本校教师名单+资格组校验);requireInApproverPool 删除。bindAccounts(账号路径遗留)与 sysApproverRepository 保留
B2 后端 dict 维护接口 ✅ 未动 按计划保留(表/接口在,前端不再调用)
附带 ✅ UserCheckModal.vue multiple prop 注释去「审核设置」引用(prop 本身保留为通用能力)

验证结果(2026-09-14):

  • npm run build 通过(exit 0);后端 mvn compile 通过(exit 0,JAVA_HOME=jdk-1.8)
  • .scratch/verify-menu-order.mjs 重打桩后 13 项全绿(含新增「children 中 modId 6 剔除」断言,测试①期望已改为无审核设置)
  • .scratch/verify-menu-name-bind.mjs 14 项全绿(/approval-setting 期望改为解析空串,即路由/别名彻底下线)
  • 残留引用全局 grep:仅剩注释与 mock GetUserMod 兜底条目(有意保留)

遗留登记:

编号 内容
E1 平台用户中心仍授权 modId 6「审核设置」模块(GetUserMod 照常返回);前端已剔除显示,如需彻底回收授权请在平台管理后台取消模块授权
E2 后端 sys_approver 表、dict/Approver* 接口与 bindAccounts 账号路径成为无入口的死代码,彻底下线另行排期
E3 人工冒烟(需登录环境):教师树选任意教师 → 直接发布/提交审批全链路;已驳回单重提;草稿箱提交审批弹窗同样直显教师树

v19(2026-09-14)· 审批中心状态 tab 改为一行显示

一、需求与现状

用户要求:审批列表左列的四个状态 tab「待审批 (1) / 已通过 (1) / 已驳回 (1) / 全部 (2)」在一行内显示,不再折行。

编号 证据
S1 ApprovalList.vue 左列 .title-list 宽 360px(与「已收到」同款),.filter-box 左右 padding 12px + 1px 边框 → tab 行可用宽 ≈ 334px
S2 v13 给 tab 文字加了条数后缀 tabCountText(),同时把 :deep(.ivu-radio-group) 设为 display:flex; flex-wrap:wrap; gap:6px —— 换行是当时的兜底,正是本需求的直接成因
S3 view-ui-plus 实测 CSS(dist/styles/viewuiplus.css):.ivu-radio-group-button.ivu-radio-group-small .ivu-radio-wrapper{height:24px;padding:0 12px;font-size:14px},且 .ivu-radio-group-button 父级 font-size:0 不作用于 wrapper。按 14px 字号估宽:待审批 (1) ≈ 3×14 + 「 (1)」≈18 + 左右 padding 24 ≈ 84px,四个 ≈ 330px + 3 个 gap 18px ≈ 348px > 334px → 必然折行
S4 tab 文字来自前端字面量,条数为 0 时不显示后缀(tabCountText),故最宽形态是「三位数条数」;DOM 结构为 div.ivu-radio-group.ivu-radio-group-button.ivu-radio-group-small > label.ivu-radio-wrapper,flex 项是 wrapper 而非 group

二、改动计划(仅 src/views/notice/ApprovalList.vue 样式层)

# 位置 改动
F1 .filter-box :deep(.ivu-radio-group) flex-wrap: wrap → nowrap + width:100%,保留 6px 间距观感
F2 新增 .filter-box :deep(.ivu-radio-group-button .ivu-radio-wrapper) flex:1 1 0; min-width:0; box-sizing:border-box; padding:0 4px; font-size:12px; text-align:center; white-space:nowrap; overflow:hidden; text-overflow:ellipsis —— 四项等宽铺满(约 79px/项),文字最宽形态约 62px,留有余量;极端条数(四位数)时省略号兜底而非撑破
F3 注释 更新 v13 折行兜底说明为 v19 一行等宽口径

不动:tab 文案、tabCountText 规则、接口契约、右列与「已收到」页样式。

三、回归清单(v19)

  • 四个 tab 同一行等宽显示,无折行、无横向溢出
  • 条数为 0 时仍只显示「待审批」等裸文案;条数为两位数时仍不换行
  • 选中态(蓝底)与 hover 态样式正常,切换 tab 触发 doSearch 正常
  • 窄屏(左列固定 360px,不随窗口变化)下不出现溢出
  • npm run build 通过(SCSS + scoped :deep 编译无误)

四、实施记录(v19 回填)

项 状态 备注
F1 group 改 nowrap ✅ 2026-09-14 flex-wrap: wrap → nowrap,补 width:100% 让四项铺满 334px 内容宽;6px gap 保留
F2 wrapper 等宽收紧 ✅ 新增 :deep(.ivu-radio-group-button .ivu-radio-wrapper):flex:1 1 0(每项 ≈79px)+ min-width:0 + box-sizing:border-box + padding:0 4px + font-size:12px + 居中 + white-space:nowrap; overflow:hidden; text-overflow:ellipsis 兜底极端长文案
F3 注释 ✅ 折行兜底说明替换为 v19 一行等宽口径,并记录库默认盒模型与宽度算式
备份 ✅ .backup/20260914-v19-approval-tabs-one-line/views/ApprovalList.vue(工程非 git)
未动 — tab 文案、tabCountText、接口契约、右列样式;「已收到」页 .filter-box 未改(其 3 tab + 类型 Select 估宽约 297px < 328px,一行放得下)

验证结果(2026-09-14):

  • npm run build 通过(exit 0,仅既有 chunk >1000kB 提示)
  • 编译产物核对(dist/static/css/ApprovalList-*.css):.title-list .filter-box[data-v-888bcda5] .ivu-radio-group{display:flex;flex-wrap:nowrap;gap:6px;width:100%} 与 ... .ivu-radio-group-button .ivu-radio-wrapper{flex:1 1 0;...} 均正确落位;选择器特异性(4 class + 1 属性)高于库默认 .ivu-radio-group-button.ivu-radio-group-small .ivu-radio-wrapper(3 class),字号/内边距覆盖生效
  • 宽度核算:12px 字号下 待审批 (1) ≈ 53px 文字 + 8px 内边距 ≈ 61px < 单项 79px;两位数条数仍在余量内,四位数将走省略号而非撑破
  • 遗留(需登录环境人工冒烟):本机无 headless 浏览器(未装 puppeteer/playwright),真实字体渲染宽度与选中态视觉需人工在 #/approval 页确认一次

五、v19.1 缺陷返工(用户截图反馈「存在样式问题」)

编号 缺陷 根因(库 CSS 证据)
D1 选中 tab 左侧出现游离蓝色短竖线 .ivu-radio-wrapper-checked{box-shadow:-1px 0 0 0 #2d8cf0} —— 该阴影是为分段控件「补上被 border-left:0 省掉的左边界」,v13 的 gap:6px 把分段拆成独立 chip 后,它画到了 chip 左侧 1px 外的空隙里
D2 选中 tab 外圈多出一层淡蓝光晕,看起来像双边框 .ivu-radio-wrapper-checked.ivu-radio-focus{box-shadow:...0 0 0 2px rgba(45,140,240,.2)} + ::after{left:-3px;top:-3px;height:28px;opacity:1};点击后 radio 常驻 focus,光晕不会消失
D3 首项右两角、末项左两角是方角(chip 形态下应四角全圆) :first-child{border-radius:3px 0 0 3px} / :last-child{border-radius:0 3px 3px 0} 只在相接时成立
D4 非首项左边缘本无 border(靠 ::before 1px 分隔线补位),chip 之间留缝时左边缘缺线 .ivu-radio-wrapper{border-left:0} + ::before{left:-1px;width:1px;background:#dcdee2}

取舍(F4):与其逐条对抗库的分段假设(还要自己补圆角、四边边框、隐藏伪元素),不如回到库原生的无间隙分段控件形态 —— 与「已收到」页观感一致,自定义量最小。

项 状态 改动
F4 ✅ 2026-09-14 :deep(.ivu-radio-group) 去掉 gap:6px(保留 nowrap + width:100%);wrapper 上 &:before,&:after{display:none};新增 :deep(.ivu-radio-group-button .ivu-radio-wrapper-checked) 与 ...checked.ivu-radio-focus 两条 box-shadow:none。等宽 79px/项、12px 字号、nowrap+ellipsis 不变 → 选中态只剩蓝边蓝字,无光晕、无游离竖线、圆角由 first/last 原生规则正确切分

验证(2026-09-14):npm run build 通过;dist/static/css/ApprovalList-BBUuPZPq.css 中四条覆盖规则均按预期落位(.title-list .filter-box[data-v-c541a2a1] .ivu-radio-group-button .ivu-radio-wrapper:before,:after{display:none}、...checked, ...checked.ivu-radio-focus{box-shadow:none}),特异性 5 class + 1 属性 > 库默认 3 class。构建期 11 条 Expected identifier but found "*" 警告来自既有 src/assets/styles/utils/_utils.scss 的 *zoom 老式 hack,与本次无关。

v20(2026-09-14)· 修复「写公告→提交审批」被过期定时时间卡死

一、现象与根因

编号 证据
S1 用户 #/list/new?serid=4 点「提交审批」弹「请选择大于当前的时间!」,无法送审
S2 库表 notice id=4:status=3(已驳回)、timing_send_time=2026-09-11 15:00:00(早于当前 2026-09-14)
S3 NewNotice.vue onMounted 用 detail.DsfsTime 回填 timeForm.date1(L163);驳回态按钮只有「存为草稿+提交审批」(publishViaCreate=false 且 editStatus!==1,L446-449),页面上根本没有定时发送入口可改这个过期时间
S4 doAdd('approve') → applyTiming(data,false) → 编辑分支 t<now 抛错并 return false(L274-282),把送审整条链路掐断

定性:编辑态沿用记录里已有的定时时间时,把「过期排期」当成硬校验失败。对直接发布/存草稿/提交审批都应视为失效排期静默丢弃,而不是阻断。真正需要「必须大于当前时间」校验的只有用户主动选时间的「定时发送」弹窗(fromModal 分支,本就独立校验,保持不动)。

二、改动(仅 src/views/notice/NewNotice.vue)

项 改动
F1 applyTiming 编辑分支:去掉 t<now 的 return false;仅当 timeForm.date1 解析为未来时间才写 data.timingSendTime,过期即不写(后端 edit() 仅在 getTimingSendTime()!=null 时更新,不写=保留原值但驳回重提走的是送审、且后端 isTiming 把过期时间判为立即发布,无副作用);恒 return true
F2 fromModal(定时发送弹窗提交)分支的「请选择发送日期/时间」「大于当前时间」校验原样保留

三、回归清单(v20)

  • #/list/new?serid=4(已驳回+过期定时)点「提交审批」不再弹「请选择大于当前的时间」,正常进入送审
  • 同上「存为草稿」不被过期时间阻断
  • 草稿续编(status=0)带未来定时时间,「直接发布」仍按该时间排期(携带 timingSendTime);带过期定时时间,直接发布改为立即发布不报错
  • 「定时发送」弹窗:空日期/空时间/过去时间三种仍分别弹对应提示(行为不变)
  • npm run build 通过

四、实施记录(v20 回填)

项 状态 备注
F1 applyTiming 编辑分支 ✅ 2026-09-14 去掉 t<now 的 Message.info+return false;改为 t>now 才写 data.timingSendTime,过期静默丢弃,末尾恒 return true;补 5 行根因注释
F2 fromModal 分支 ✅ 未动 定时发送弹窗的「请选择发送日期/时间」「大于当前时间」三条校验原样保留
备份 ✅ .backup/20260914-v20-submit-approve-timing/views/notice/NewNotice.vue(工程非 git)

验证(2026-09-14):npm run build 通过(exit 0,43.87s,仅既有 chunk >1000kB 提示,NewNotice chunk 已重命名为 NewNotice-BfdcofXZ.js)。人工冒烟(需登录环境):#/list/new?serid=4 点「提交审批」不再弹过期时间提示、正常送审;「定时发送」弹窗三种校验不变。


v21(2026-09-14)· 审批中心右列内容过长被裁切,无滚动条

一、现象与根因

用户截图:#/approval 右列详情中「审批流程 / 审批轨迹」把面板撑满,底部「公告内容」只露半行即被切断,整个右列没有任何滚动条,正文与附件无法查看。

编号 证据
S1 .detail-content 为 display:flex; flex-direction:column; overflow:hidden,子块自上而下:.notice-title(标题+操作+元信息)、Alert、.flow-block、.track-block、.notice-content
S2 全列唯一滚动区是 .notice-content{flex:1; overflow-y:auto};前四块未设 flex:none,但作为普通块其内容高度不可压缩,flow/track 条目一多就把 flex:1 的正文区挤到 0 高
S3 超出容器高度的部分被 overflow:hidden 直接裁掉 → 既无外层滚动,正文滚动区又高 0,形成「内容过长没有伸缩框」
S4 对照「已收到」NoticeAccept.vue:同款结构但其右列没有 flow/track 两块固定内容,正文永远占大头,故未暴露此问题

定性:滚动职责放错了层级——应让「Alert + 审批流程 + 审批轨迹 + 正文 + 附件」整体成为一个滚动区,标题与操作按钮行(上一篇/下一篇/通过/驳回/打印)保持常驻。

二、改动(仅 src/views/notice/ApprovalList.vue)

项 改动
F1 模板:在 .notice-title 之后新增包裹层 <div class="detail-scroll">,把 Alert、.flow-block、.track-block、.notice-content 一并纳入(内部不重排缩进,减小 diff)
F2 样式:.notice-title{flex:none};新增 .detail-scroll{flex:1; min-height:0; overflow-y:auto};.notice-content 去掉 flex:1; overflow-y:auto(改为随包裹层自然流式)

不动:左列(.notice-items 本就是 flex:1; overflow-y:auto 的正常滚动区)、按钮行为、接口契约、「已收到」页。

三、回归清单(v21)

  • 审批流程/轨迹条目很多时,右列出现一条纵向滚动条,可滚到「公告内容」末尾与附件区
  • 标题 + 上一篇/下一篇/通过/驳回/打印 按钮行始终常驻可见,不随滚动消失
  • 内容不超时(如截图第一条 2 步流程)布局与改前一致,无双滚动条
  • 「暂无公告/加载中」空态仍居中
  • npm run build 通过

四、实施记录(v21 回填)

项 状态 备注
F1 模板包裹层 ✅ 2026-09-14 .notice-title 之后插入 <div class="detail-scroll">,Alert×3 / flow-block / track-block / notice-content 全部纳入;内部未重排缩进
F2 样式 ✅ .notice-title{flex:none}、.detail-scroll{flex:1;min-height:0;overflow-y:auto};.notice-content 去掉 flex:1; overflow-y:auto 仅留 padding-top:10px
备份 ✅ .backup/20260914-v21-approval-detail-scroll/views/notice/ApprovalList.vue(工程非 git)

验证(2026-09-14):npm run build 通过(exit 0,18.18s)。编译产物 dist/static/css/ApprovalList-CTyoM2OJ.css 核对:.detail-scroll[data-v-26beade0]{flex:1;min-height:0;overflow-y:auto}、.notice-content[data-v-26beade0]{padding-top:10px}(flex/overflow 已移除)、.notice-title[data-v-26beade0]{flex:none} 均落位。遗留(需登录环境人工冒烟):本机无 headless 浏览器,多步流程长单滚动到底、按钮行常驻需人工在 #/approval 确认一次。


v22(2026-09-14)· 审批:驳回后「审批轨迹」无记录 + Approve 误报「已离开待审批状态」

一、现象与证据

现场 URL:GET /api/notice/Approve?serid=9&opinion=同意;notice 9 报「该通知已离开待审批状态,无需重复处理」,且此前该单被驳回后「审批轨迹」无任何处理记录。

DB 取证(campus_notice,serid=9):

证据 数据
S1 notice status=2(PENDING)、approval_level=0 —— 仍处「待审批」,Approve 却报错
S2 notice_approval 仅 1 行 id=7 action=1(提交) lijing;无 action=2/3(通过/驳回) 行
S3 notice_approval_record 空 —— 多步 approve/reject 只写这张表,S2 因此缺处理留痕
S4 notice_approval_step 两条 step_no=1:id=10 status=0(待流转)、id=11 status=1(审批中),同为 lijing/[10008]

二、根因

问题1(审批轨迹无驳回记录):NoticeService.reject() 的多步分支(L793–809)只调 saveRecordRow(...) 写 notice_approval_record(驱动「审批流程」approvalFlow),从不写 notice_approval;而前端「审批轨迹」= approvalRecords = loadApprovalRecords() 只读 notice_approval(L2924)。∴ 多步驳回(及中间多步通过)在「审批轨迹」里完全不留痕。(旧单步 legacy 分支正常写 saveApprovalRow,故只有走步骤表的单子复现。)

问题2(Approve 误报已离开待审批):approve()/reject() 用 steps.get(Math.min(level, size-1)) 按数组下标定位当前步。S4 里靠前的 id=10 是陈旧 WAITING 脏行,get(0) 命中它,status(0)≠STATUS_ACTIVE(1) → 进 L690/L795 抛 alreadyHandledError()。该方法又只查 notice_approval 的通过/驳回行(S2 为空)→ 落到兜底文案「该通知已离开待审批状态」。真正待办的 id=11(ACTIVE) 永远取不到,审批卡死。

三、修复方案(仅 NoticeService.java)

项 改动 位置
F1 多步 reject():在 saveRecordRow(... ACTION_REJECT ...) 后补 saveApprovalRow(serid, ACTION_REJECT, meAccount, meName(meAccount), opinion, null),使「审批轨迹」显示驳回,并让 alreadyHandledError 能给出「已被X驳回」正解文案 reject 多步分支
F2 新增 resolveCurrentStep(steps, level):优先返回 status==STATUS_ACTIVE 的步骤,无则回退 steps.get(min(level,size-1));approve()/reject() 的下标取步改用它,规避陈旧/重复步骤行 approve/reject + helper
F3 新增 normalizeSteps(raw):按 step_no 去重(每步留 id 最大=最近提交写入行,丢弃陈旧脏行)再升序;approve()/reject()/getDetailMap() 三处查询包裹它。修复脏数据下 steps.get(level+1)/steps.size() 误把同一步脏行当「下一步」、前端流程渲染重复步 三处 + helper

不改:legacy 分支、publishAfterFinalApproval(末步已写 APPROVE 轨迹行,不重复写)、alreadyHandledError 本身、submit() 的删旧建新逻辑、前端。

四、回归清单(v22)

  • 多步审批流驳回后,「审批轨迹」出现「驳回」条目(含处理人姓名+意见)
  • 再次点 Approve:提示为「该通知已被X于…驳回」(不再是「已离开待审批状态」)
  • notice 9 这类带陈旧 WAITING 步骤脏数据的单,Approve 能命中 ACTIVE 步正常通过并流转/发布
  • 正常新建→提交→逐步通过→发布链路不回归
  • mvn -q compile 通过

五、实施记录(v22 回填)

项 状态 备注
F1 多步驳回补写审批轨迹 ✅ 2026-09-14 reject() 多步分支 saveRecordRow(...REJECT...) 后新增 saveApprovalRow(serid, ACTION_REJECT, meAccount, meName(meAccount), opinion, null);轨迹出现驳回条目,alreadyHandledError 给出「已被X驳回」正解
F2 当前步定位健壮化 ✅ approve()/reject() 取步由 steps.get(level) 改 resolveCurrentStep()(优先 STATUS_ACTIVE)
F3 步骤列表去重 ✅ normalizeSteps() 按 step_no 去重升序,approve()/reject()/getDetailMap() 三处包裹
编译 ✅ mvn -q compile EXIT=0(JAVA_HOME=jdk-1.8,maven-3.6.1)

对 notice 9 脏数据(step_no=1 两行 id=10 WAITING / id=11 ACTIVE):normalizeSteps 归一为单条 ACTIVE → resolveCurrentStep 命中 → level+1 !< size 走 publishAfterFinalApproval 正常发布;旧脏行无需手工清库即在运行时被读侧/动作侧忽略。重复行属更早构建遗留,未动 submit() 删旧建新;若复发需另查提交侧 delete 未生效的具体路径。

生效方式:后端经 IDEA Debug + devtools 运行,命令行 mvn compile 产物落 target/classes,须让 IDEA「Build → 重新编译触发 devtools 热重启」或重启后端实例(先 scripts/kill-port.bat 8101 释放端口)才被加载。浏览器 #/approval 待人工冒烟:驳回后审批轨迹可见 + Approve 不再误报。


v23(2026-09-14)· 审批中心:状态提示条常驻 + 「查看审批」折叠/展开审批信息

一、需求(用户截图标注)

#/approval 右列详情,用户圈定两点:

  1. 提示条「该公告正在审批中:第2/2步 第1步·或签」固定——不随右列滚动而消失,始终常驻在标题/元信息之下。
  2. 新增「查看审批」功能——在提示条右侧放一个可点开关,用于折叠 / 展开审批信息(即「审批流程」+「审批轨迹」两块)。

二、现状与根因

编号 证据
S1 v21 把 Alert + .flow-block + .track-block + .notice-content 一并塞进唯一滚动区 .detail-scroll,故 Alert 会随滚动被推走,不满足「固定」
S2 现 Alert 仅三态文案(pending/rejected/approved),无任何折叠交互;审批信息恒显示,无法收起
S3 工程无 icon-down/up/zhankai 类字形(icons.css 缺),chevron 不能硬引 iconfont,须用 CSS 绘制或文字符号

定性:把 Alert 从滚动区里提出来做成常驻条(flex:none,位于 .notice-title 与 .detail-scroll 之间);在其右侧挂一个开关,用 approvalOpen 控制 .flow-block/.track-block 的显隐(仍在滚动区内,随展开/收起出现或消失)。

三、改动(仅 src/views/notice/ApprovalList.vue)

项 改动
F1 脚本:新增 approvalOpen(默认展开)、hasApprovalInfo(流程或轨迹有数据)、alertType/alertText/showAlert(合并原三态 Alert 判定)
F2 模板:.notice-title 之后、.detail-scroll 之前插入常驻 .approval-bar(内含单一 <Alert> + 右侧「查看审批/收起审批」开关,chevron 用 CSS 边框绘制并随开合旋转);.detail-scroll 内原 Alert×3 移除,.flow-block+.track-block 包进 <template v-if="approvalOpen">
F3 样式:.approval-bar{flex:none}(常驻不滚);.alert-row flex 两端对齐;.alert-toggle 手型 + hover 变色;.chevron 旋转动画

不改:左列、按钮行为(上一篇/下一篇/通过/驳回/打印)、正文/附件、接口契约、其它页面。

四、回归清单(v23)

  • 右列内容较长时上下滚动,黄色「该公告正在审批中…」提示条始终固定在标题下方不动
  • 提示条右侧「查看审批」可点:收起后审批流程 + 审批轨迹两块隐藏、只剩正文可滚;再点展开恢复
  • 无审批流程/轨迹数据的公告不显示「查看审批」开关(避免点了没反应)
  • 通过/驳回/已发布三态提示条文案与配色不变(warning/error/success)
  • npm run build 通过

五、实施记录(v23 回填)

项 状态 备注
F1 脚本状态 ✅ 2026-09-14 approvalOpen 默认 true(保持既有「审批信息可见」口径);hasApprovalInfo 门控开关显隐
F2 模板 ✅ Alert 提出滚动区成 .approval-bar 常驻;flow/track 包 v-if="approvalOpen" 留在 .detail-scroll 顶部
F3 样式 ✅ .approval-bar{flex:none};chevron 用 CSS 边框绘制(工程无对应 iconfont 字形),开合旋转 180°
备份 ✅ .backup/20260914-v23-approval-view-toggle/views/notice/ApprovalList.vue(工程非 git)

验证:待 npm run build + 人工在 #/approval 冒烟(提示条常驻、查看审批折叠/展开)。


v24(2026-09-14)· 移除审批列表左列顶部「共 X 条 / X 条待我处理」统计行

一、需求(用户截图圈红)

#/approval 左列顶部的 .total 行(共 {page.total} 条 + 右侧 {tabCounts.pending} 条待我处理)用户圈红并明确要求移除。

二、现状与影响面

编号 证据
S1 模板 ApprovalList.vue L298–301:.title-list 内首个子节点 .total,含 .num(总数)与 .tip-text(待我处理)两个 span
S2 样式 L542–556:.total{flex/space-between} 及其内 .num、.tip-text
S3 page.total 另用于分页(L334 v-if、L338 :total),tabCounts.pending 另用于「待审批」tab 计数(L300),二者均不可随本行删除

定性:仅删展示层 .total 节点与其配套样式;数据源 page.total/tabCounts 仍被分页与 tab 引用,保留不动,删除后无死变量。

三、改动(仅 src/views/notice/ApprovalList.vue)

项 改动
F1 模板:删除 .title-list 下整个 .total div,.filter-box(状态 tab 行)上移为首个元素
F2 样式:删除 .total{...} 块(含内嵌 .num、.tip-text)

不改:状态 tab、搜索框、列表项、右列详情、分页、接口契约、tabCounts/page.total 的其它引用。

四、回归清单(v24)

  • 左列不再显示「共 X 条 / X 条待我处理」,状态 tab 行成为顶部第一行
  • 「待审批」等四个 tab 的条数仍正常显示(tabCounts 未受影响)
  • 底部翻页控件在结果超一页时仍正常(page.total 未受影响)
  • npm run build 通过,产物 ApprovalList-*.js 不再命中「条待我处理」

五、实施记录(v24 回填)

项 状态 备注
F1 模板 ✅ 2026-09-14 删除 .total div
F2 样式 ✅ 删除 .total 及内嵌 .num/.tip-text
构建验证 ✅ npm run build EXIT=0(43.72s,仅既有 chunk 体积告警);grep 条待我处理 dist/.../ApprovalList-*.js 无命中;page.total/tabCounts.pending 仍分别在分页与 tab 处引用
备份 ✅ .backup/20260914-v24-drop-approval-total/views/notice/ApprovalList.vue(工程非 git)

验证:待人工在 #/approval 冒烟确认左列首行即为状态 tab、无残留空白。


v25(2026-09-14)· 审批详情伸缩框扩大为「标题以下整块」

一、需求(用户截图圈红)

#/approval 右列,用户圈红标题以下整块(元信息 + 提示条 + 正文),要求把「伸缩框」从 v23 的「仅折叠审批流程/轨迹」改为包含标题外整个详情区。经确认取「扩大折叠区但不动开关」:折叠开关仍留在常驻提示条内。

二、现状与取舍

编号 证据 / 决策
S1 v23:approvalOpen 仅包 .flow-block+.track-block(在 .detail-scroll 内),元信息(.title-content)与正文(.notice-content)恒显示
S2 折叠开关 .alert-toggle 位于常驻 .approval-bar(提示条)内,非在 .detail-scroll 内
D1 若把提示条也纳入折叠区,收起后开关随之消失、无法再展开 → 定:提示条作为伸缩框「把手」保持常驻,折叠区 = 元信息 + 审批 + 正文附件(标题以下除提示条外的全部)
D2 「不动开关」按位置理解:开关位置与文案(查看审批/收起审批)均不改

三、改动(仅 src/views/notice/ApprovalList.vue)

项 改动
F1 .title-content(元信息)加 v-if="approvalOpen"
F2 .detail-scroll 整块加 v-if="approvalOpen",并删除其内部原只包审批信息的 <template v-if="approvalOpen">(flow/track 与正文现同属该折叠块)
F3 更新 approvalOpen 注释:语义由「审批信息展开」扩为「标题以下整块详情展开」

不改:.notice-title(标题行)、.approval-bar(常驻提示条 + 开关)、hasApprovalInfo 门控、开关文案、接口与其它页面。

收起态视觉:仅标题行 + 提示条(含「查看审批」把手);展开态:标题 + 元信息 + 提示条 + 审批流程/轨迹 + 正文附件。

四、回归清单(v25)

  • 点提示条右侧「查看审批/收起审批」,收起后元信息 + 正文 + 审批一起隐藏、只剩标题行与提示条;再点全部恢复
  • 收起后开关仍在提示条上可见可点(不会把自己收进去)
  • 默认进入详情为展开态(approvalOpen=true)
  • npm run build 通过

五、实施记录(v25 回填)

项 状态 备注
F1 元信息折叠 ✅ 2026-09-14 .title-content 加 v-if="approvalOpen"
F2 滚动区折叠 ✅ .detail-scroll 加 v-if="approvalOpen",移除内层 <template> 包裹
F3 注释 ✅ approvalOpen 语义更新为整块详情
构建验证 ✅ npm run build EXIT=0(20.19s,仅 _utils.scss 既有 *zoom 告警);ApprovalList-*.js 正常产出
备份 ✅ .backup/20260914-v25-approval-fold-whole-detail/views/notice/ApprovalList.vue(工程非 git)

验证:待人工在 #/approval 冒烟(收起只剩标题行+提示条、展开恢复整块、开关常驻可点)。

v26(2026-09-14)· 已发送/全部公告:标题列不显示 + 超 8 字截断加略号

一、需求(用户截图)

#/list(已发送)与 #/noticelist(全部公告)共用 NoticeList.vue。用户截图反馈两点:①标题列不显示内容(只见「…」);②要求标题超过 8 个字时截断并在末尾显示略号。

二、根因盘点

编号 证据 / 结论
S1 后端 NoticeVo.java 有 title 字段,DB notice.title 均有值(如「这是一条公告」)→ 数据侧无问题
S2 端口探测:8101=后端 IDEA Debug;4173=vite preview(PID 13552)在跑,未起 dev(8077)
S3 vite preview 载的是旧 dist/,旧「标题」列 {key:"title", tooltip:true} 无 width,被定宽列挤压塌成窄列渲染成「…」,且不含截断逻辑 → 截图现象由此而来
D1 源码侧改「标题」列为自定义 render:t.length>8 ? slice(0,8)+"…" : t,并加 minWidth:200 防塌、原生 title 属性悬浮看全文;改完必须 npm run build 重打 dist,preview 才会反映

三、改动(仅 src/views/notice/NoticeList.vue)

项 改动
F1 「标题」列由 key+tooltip 改为 render:按字符数截断(>8 取前 8 字 + …),minWidth:200,attrs.title 存全文供悬浮

两页共用同一组件,一处改动同时覆盖「已发送」与「全部公告」。

四、回归清单(v26)

  • #/noticelist、#/list 标题列正常显示文字(不再是空「…」)
  • 标题 >8 字显示前 8 字 + …;≤8 字原样
  • 鼠标悬停标题单元格可看到完整标题
  • npm run build 通过

五、实施记录(v26 回填)

项 状态 备注
F1 标题 render ✅ 2026-09-14 slice(0,8)+"…",minWidth 200,native title 全文
构建 ✅ npm run build EXIT=0(41.51s);NoticeList-MS8GxHwa.js 内含 a.length>8?\${a.slice(0,8)}…`:a与attrs:{title:a}`
备份 ✅ .backup/20260914-v26-notice-title-ellipsis/views/notice/NoticeList.vue(工程非 git)

验证:用户当前跑 vite preview(4173),须硬刷新(Ctrl+Shift+R)加载新 dist;反复调样式建议改 npm run dev(8077) 免每次 build。


v27(2026-09-14)· 审批详情伸缩框含提示条整块收起,折叠开关上移标题行

注:编号顺延——v26 已被并行会话(已发送/全部公告标题列)占用,本节记 v27。承接并修订 v25:v25 让提示条常驻作把手,本次按用户新截图(红框把黄色提示条也框进折叠区)改为提示条一并收起。

一、需求(用户截图圈红)

#/approval 右列,红框 = 标题以下整块(元信息 + 黄色提示条 + 审批流程/轨迹 + 正文附件),要求伸缩框覆盖到含提示条的全部;即收起后只剩标题行。

二、冲突与决策

编号 证据 / 决策
S1 v25 折叠开关 .alert-toggle 长在提示条 .approval-bar 内,且提示条常驻作「把手」
S2 若把提示条纳入折叠区而开关仍在其内,则收起后开关随之消失、无法再展开(v25 遗留的取舍点)
D1 定:折叠开关上移到标题行(.title-header 内 .detail-toggle,常驻可见),提示条并入折叠区 v-if="showAlert && approvalOpen"
D2 开关文案由「查看审批/收起审批」改为「展开/收起」——现折叠的是整块详情而非仅审批信息
D3 hasApprovalInfo(原门控旧开关显隐)失去唯一消费者,删除;标题行开关恒可见

三、改动(仅 src/views/notice/ApprovalList.vue)

项 改动
F1 模板:.title-header 内新增 .title-left(.tt + .detail-toggle),开关 @click="approvalOpen=!approvalOpen",chevron 随开合旋转
F2 模板:.approval-bar 的 v-if 由 showAlert 改为 showAlert && approvalOpen,并删除其内部 .alert-toggle(只留状态文案)
F3 脚本:删 hasApprovalInfo;更新 approvalOpen 注释为「控制标题以下整块」
F4 样式:.title-header 加 .title-left/.detail-toggle(含 chevron 边框绘制);删 .alert-row 内已无引用的 .alert-toggle 块

折叠区(approvalOpen)现覆盖:.title-content(元信息)+ .approval-bar(提示条)+ .detail-scroll(审批 + 正文附件)。不改:标题行、ops(上一篇/下一篇/通过/驳回/打印)、接口与其它页面。

收起态:仅标题行(标题 + 展开 + ops);展开态:标题 + 元信息 + 提示条 + 审批 + 正文附件。

四、回归清单(v27)

  • 点标题行「收起」→ 元信息、黄色提示条、审批、正文一并隐藏,只剩标题行;点「展开」全部恢复
  • 收起后「展开」开关仍在标题行可见可点(不再随提示条消失)
  • 默认进入详情为展开态
  • 通过/驳回/打印等 ops 与提示条文案配色不受影响
  • npm run build 通过

五、实施记录(v27 回填)

项 状态 备注
F1 标题行开关 ✅ 2026-09-14 .title-left 包 .tt+.detail-toggle,文案「展开/收起」
F2 提示条并入折叠 ✅ v-if="showAlert && approvalOpen",移除内嵌开关
F3 脚本清理 ✅ 删 hasApprovalInfo(grep 确认无其它引用)
F4 样式 ✅ 加 .title-left/.detail-toggle+chevron,删 .alert-toggle
构建验证 ✅ npm run build EXIT=0(18.25s,仅 _utils.scss 既有 *zoom 告警);ApprovalList-*.js/.css 正常产出
备份 ✅ 承接 v25 备份链(.backup/20260914-v25-approval-fold-whole-detail/ 为改前态,工程非 git)

验证:待人工在 #/approval 冒烟(收起只剩标题行、展开恢复整块含提示条、开关常驻标题行可点)。

v28(2026-09-14)· 公告详情「上一篇/下一篇」点击后不跳转(goNav 竞态)

一、需求(用户反馈)

#/noticelist/detail?serid=…&type=0(及 /list/detail)点「下一篇」报「接口错误」,用户贴出请求 Detail?serid=1&type=0。

二、排查

编号 证据 / 结论
S1 后端 NoticeVo/checkDetailAccess:serid=1 为 lijing 本人待审批单,本人可访问;notice_approval_step 无行、数据干净 → Detail?serid=1&type=0 正常应 200
S2 端口:4173=vite preview(载 dist)、8101=后端。curl 打 4173 /api 返回 302→SSO(无会话),证明 preview 已代理 /api(继承 server.proxy),代理链路正常
S3 无法用无会话 curl 复现「接口错误」(后端 302 由 alova 层转跳 SSO,不弹错);需用户登录态才能抓具体响应
D1 定位到明确代码缺陷:goNav 在 await 前同步调用 load(),而 router.replace 是异步提交 → load() 读到旧 serid,点「下一篇」实际重拉当前公告、序列不前进(表现为「点了没反应/异常」)

三、改动(仅 src/views/notice/NoticeDetails.vue)

项 改动
F1 import 补 watch
F2 新增 watch(() => route.query.serid, id => Number(id) && load()):加载统一由路由变化驱动(兼顾上一篇/下一篇 + 浏览器前进后退)
F3 goNav 去掉同步 load(),只 router.replace 改地址,避免读到未提交的旧 serid

四、回归清单(v28)

  • 详情页点「下一篇/上一篇」能真正切换到相邻公告、URL 与内容一致
  • 浏览器前进/后退改变 serid 时详情随之刷新
  • 首屏进入仍正常加载(onMounted 的 load 与 watch 不重复触发)
  • npm run build 通过

五、实施记录(v28 回填)

项 状态 备注
F1/F2/F3 ✅ 2026-09-14 竞态修复:watch 驱动 load,goNav 只导航
构建 ✅ npm run build EXIT=0(18.26s)
备份 ✅ .backup/20260914-v27-detail-nav-race/views/notice/NoticeDetails.vue(工程非 git;目录名沿用早期编号,内容即本次改前态)

待确认:若硬刷新后「下一篇」仍报接口错误,请提供该失败请求的 HTTP 状态码 + 响应体(Network 面板标红那条),因后端需登录态、无法离线复现。

v29(2026-09-15)· 删除 notice/ScopePreview 接口请求

一、需求(用户)

删除前端对 GET /api/notice/ScopePreview(推送范围「已选学员数」预估)的接口请求。

二、影响面

编号 文件 说明
S1 src/api/module/notice.ts noticeApi.scopePreview() 发出该请求(唯一封装点)
S2 src/components/ClassScopeInput.vue 在用组件(NewNotice 引用):created/watch 调 runPreview → 拉 ScopePreview,模板 .scope-preview 显示「已选学员 N 人」
S3 src/components/PushScopePanel.vue v5 起已被 ClassScopeInput 取代、全工程未被 import 的死代码;仍 import noticeApi 并被 vue-tsc 纳入类型检查,须一并清理否则编译报错
S4 src/assets/mock/noticeapi.ts dev:mock 下的 /api/notice/ScopePreview 桩
S5 src/types/notice.ts ScopePreviewVo 类型仅服务于该接口

三、改动

项 改动
F1 notice.ts:删 scopePreview() 方法、「推送范围(改造方案 3.1)」分节、ScopePreviewVo 类型 import;头部变更要点注释同步
F2 ClassScopeInput.vue:删模板 .scope-preview 行、preview/previewTimer data、runPreview() 方法、created/watch 中的 runPreview() 调用、noticeApi/ScopePreviewVo import(保留 baseApi)、.scope-preview 样式
F3 PushScopePanel.vue:同步删预估逻辑与 ScopePreviewVo/noticeApi import(保持死代码可编译)
F4 mock/noticeapi.ts:删 /api/notice/ScopePreview 桩条目
F5 types/notice.ts:删 ScopePreviewVo 接口

四、回归清单(v29)

  • 全局 grep ScopePreview 仅剩 .backup/、dist/、本方案文档
  • 写公告页不再发起 /api/notice/ScopePreview 请求;「已选学员 N 人」预估行随之移除
  • npm run build 通过(vue-tsc 无「ScopePreviewVo 未使用 / noticeApi 未定义」报错)

五、实施记录(v29 回填)

项 状态 备注
F1–F5 ✅ 2026-09-15 见上
备份 ✅ .backup/20260915-v29-drop-scopepreview-request/(工程非 git)
构建 ✅ npm run build EXIT=0(含 vue-tsc,17.24s);全局 grep ScopePreview 仅剩 .backup/、dist/、本文档

v30(2026-09-15)· 发文部门/联系组等:新增与修改后名称内容重复且带逗号(校园部,校园部)

一、现象(用户)

「应用设置 → 发文部门」新增部门、修改(改名/启停)后,部门名称变成重复且以逗号分隔的串,如 校园部,校园部;且随着操作次数增多继续翻倍(实测库中 sys_dict.id=2 = 校园部,校园部,校园部,校园部)。用户判断:新增和修改都会、内容重复、且有逗号。

二、根因(定位链,证据表 S)

编号 证据 结论
S1 D:\...\mysql.exe ... "SELECT ... FROM sys_dict WHERE dict_type='dept'" → id=2 dict_key=dept2 dict_value=校园部,校园部,校园部,校园部 重复+逗号发生在落库值本身,非前端显示层拼接;dict_key=dept2 由 Java addDeptDic 生成(dept+sortOrder),证明确经后端写入
S2 后端 DictService.addDeptDic/updateDeptDic 均 dict.setDictValue(name),无追加;git log(b19c774/6d7d1c9/5fa8462)全程无 , 拼接;DictController 用 @RequestParam("name") String name 后端服务层不产生逗号;脏值只能是传入的 name 参数本身已含逗号
S3 进程 27616 启动时间 = 2026-09-15 10:40:34,等于 target/classes/DictService.class mtime,且晚于最后改 DictService 的 commit(09:08) 8101 跑的就是当前干净源码,排除「旧 class 未热重启」假说
S4 前端 src/api/module/notice.ts 的 postForm():把同一组 pairs 既拼进 URL query(fullUrl=url?form)又放进 x-www-form-urlencoded body(注释称 mock 只解析 query 故加 query,「不影响线上」) 真根因:一个逻辑参数被提交两份(query 一份 + body 一份)
S5 Servlet/Spring 语义:POST application/x-www-form-urlencoded 时 request.getParameter 会合并 query string 与 body 的同名参数;@RequestParam String 对多值以 , 连接 query+body 各一个 name → Spring 合并成 校园部,校园部(2 份)。之后改名/启停把已含逗号的当前值原样回发、再 query+body 各一份 → 翻倍成 4。mock 只读 query(1 份)故本地 dev:mock 不复现

波及面:所有走 postForm 的 @RequestParam 接口同病——发文部门 Add/Update/Delete/Sort、联系组 AddGroup/EditGroup/DelGroup/SetContactsTea、收文人 ReceiveUsers 等。本次以部门为切入点,修复落在 postForm 一处即全覆盖。

三、修复计划

项 改动
F1 src/api/module/notice.ts postForm():仅当 mock 模式才把参数拼进 URL query;真后端只发 form body、URL 不带 query。使 Spring 只从 body 取到单个参数,消除合并重复
F2 以 import.meta.env.VITE_IS_MOCK === "true" 判定 mock(与 .env.mock 一致),保持 dev:mock 本地取参能力不回退
F3 数据清理:把 sys_dict 中被污染的 dept 行 dict_value 逗号去重还原(校园部,校园部,校园部,校园部 → 校园部);改前先 SELECT 备份原值,执行前与用户确认

四、回归清单(v30)

  • 真后端(4173 preview):新增部门「校园部」→ 列表与 sys_dict.dict_value 均为干净 校园部(无逗号)— 需登录态,待用户浏览器点验
  • 改名、启用/停用后仍为单值,不再逐次翻倍 — 待用户浏览器点验
  • 联系组新建/改名同样不再出现逗号重复 — 待用户浏览器点验
  • dev:mock 下增删改仍正常取参(query 生效),无回归
  • npm run build EXIT=0

五、实施记录(v30 回填)

项 状态 备注
F1–F2 ✅ 2026-09-15 notice.ts postForm():fullUrl 仅在 conf.isMock 时拼 query,真后端只发 body;注释同步
mock 回归 ✅ 2026-09-15 dev:mock(8078) node 打 /api/dict/Add?name=… + ManageList:参数经 query 正常解析、落库为单值无逗号 → F2 未回退
备份 ✅ .backup/20260915-v30-postform-dup-param/notice.ts.orig(工程非 git)
构建 ✅ 2026-09-15 npm run build EXIT=0(21.04s);4173 preview 已服务新 dist,浏览器需 Ctrl+Shift+R 硬刷新
F3 数据清理 ✅ 2026-09-15 UPDATE sys_dict SET dict_value=SUBSTRING_INDEX(dict_value,',',1) WHERE dict_type='dept' AND id=2 AND dict_value LIKE '%,%'(affected=1);校验 id=2 has_comma=0、hex E6A0A1E59BADE983A8=「校园部」;id=1 发文部门 本就干净未动。公告存 dept_key,改显示名不影响已发公告

六、补充(同日)· 同源 bug 的另两处表现

用户复测反馈:自定义联系组「添加/编辑」组名同样变 111,111;「添加组员」调 POST /api/contacts/SetContactsTea?gid=1&ids=10007 报「接口异常」。二者与 v30 同一根因(postForm query+body 双发,Spring @RequestParam 合并),只是受害方式不同:

编号 表现 机理
S6 联系组名 111,111(contacts_group.id=1,hex 3131312C313131) AddGroup 的 name 被 query+body 合并成 111,111;字符串能落库,仅显示重复
S7 SetContactsTea 接口异常 gid 合并成 "1,1" → 控制器 Integer.parseInt("1,1") 抛 NumberFormatException → 500。数字参数遇逗号直接崩,比字符串更致命

处置:F1–F2 的 postForm 改动已一并覆盖 AddGroup/EditGroup/SetContactsTea(真后端只发 body,gid 恢复单值 1,parseInt 正常)。用户截图 URL 仍带 ?gid=1&ids=10007 = 加载的是修复前旧包,须 Ctrl+Shift+R 硬刷新 4173(或重起 dev)载入新 dist 方生效。数据侧清理:

项 状态 备注
联系组脏名 ✅ 2026-09-15 UPDATE contacts_group SET group_name=SUBSTRING_INDEX(group_name,',',1) WHERE id=1 AND group_name LIKE '%,%'(affected=1)→ 还原 111
SetContactsTea 后端 ✅ 复核 控制器/仓储(deleteByGroupIdBulk 带 @Modifying(clearAutomatically=true))在拿到干净 gid 后无其它缺陷;异常纯由入参 "1,1" 触发