Files
CampusnoticeFront/执行计划任务方案.md
T
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

1806 lines
180 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 通知公告前端改造成「校园通知」执行计划任务方案(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 || [])` + `children || []`;回显 `getNodesByFilter` 限定 `x.type == 2` 人员叶子 |
| 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)
- [x] `grep -rn "通知" src/views src/components src/router` 剩余命中全部为注释/未引用组件/mock 之外的白名单项
- [x] 面包屑:`/list/new` 显示「已发管理>写公告」,详情页显示「…>公告详情」
- [x] 平台模块名仍为旧叫法(`已收到/已发送/全部通知/审批列表/通知统计`)时,菜单点击仍解析到正确路由(`verify-menu-name-bind.mjs` 全绿)
- [x] 「请确认已通知到学员/家长」动词未被误改
- [x] `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)
- [x] `mvn compile` 通过(JAVA_HOME=jdk-1.8,BUILD SUCCESS)
- [ ] lijing(10008) 登录后「待审批」可见单 3/4/5(或新送审单),且能实际通过/驳回(待浏览器验证)
- [ ] 四 tab 数字与各自列表条数一致;0 值不显示数字(待浏览器验证)
- [x] 「审批中心」菜单角标仍为待我审批数(`ApprovalCount` 端点与 `layout.vue` 未改)
- [x] `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)
- [x] 新建态:`直接发布 / 存为草稿 / 提交审批 / 定时发送` 顺序与文案正确
- [x] 草稿续编:五按钮齐全;直接发布 → `/notice/Create` 新记录 status=1 且原草稿从草稿箱消失(代码路径已落地,**待联调实测**)
- [x] 草稿续编:定时发送 → 新记录 `status=1 + timing_published=0 + timing_send_time` 到点被 `TimingPublishJob` 触发(S5 缺陷修复,**待联调实测**)
- [x] 草稿续编:存为草稿 → 仍为草稿、留在草稿箱
- [x] 已驳回:无「直接发布」「定时发送」;存为草稿后状态仍为已驳回(留在已发管理,未回流草稿箱);提交审批可走通
- [x] 勾选审批并选人后点直接发布/定时发送 → 被拦截提示
- [x] 删除按钮仍只在草稿/已驳回出现
- [x] `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)
- [x] 审批中心为左右分栏,左列卡片含进度与当前审批人,四 tab 数字仍正确
- [x] 点卡片右侧出详情;`上一篇/下一篇` 在当前 tab+关键字结果内移动,首/末条置灰
- [x] 详情按钮行含 `通过/驳回/打印`;非当前审批人只见 `打印` 与等待提示
- [x] 通过弹窗默认“同意”、驳回意见必填;提交后列表与详情同时刷新
- [x] 审批流程区显示每步 步名/或签会签/审批人**人名**/状态/意见;轨迹时间线显示提交与各级处理人**人名**(存量数据由读取兜底 + 回填双路保障,不再依赖 `lijing`)
- [ ] 新送审与新审批动作后,`operator_tea_id`/`approver_tea_id` 落用户中心 teaId,姓名落真实姓名(**需 8101 + CAS 登录实测**)
- [ ] 回填任务对历史 6 条 `notice_approval`、1 条 `notice_approval_record`、`notice.publisher_name` 生效且幂等(**需带开关重启实测**)
- [x] `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` 两两相等
- [x] `mvn compile`(JAVA_HOME=jdk-1.8)BUILD SUCCESS 与 `npm run build`(43.32s)均通过;`.scratch/verify-accept-print.mjs` 22/22 全绿(其余项需登录环境人工回归,见五、实施记录)
- [x] 改前文件已备份至 `.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 条 / X 条待我处理」,状态 tab 行成为顶部第一行
- [x] 「待审批」等四个 tab 的条数仍正常显示(`tabCounts` 未受影响)
- [x] 底部翻页控件在结果超一页时仍正常(`page.total` 未受影响)
- [x] `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` 均为干净 `校园部`(无逗号)— *需登录态,待用户浏览器点验*
- [ ] 改名、启用/停用后仍为单值,不再逐次翻倍 — *待用户浏览器点验*
- [ ] 联系组新建/改名同样不再出现逗号重复 — *待用户浏览器点验*
- [x] `dev:mock` 下增删改仍正常取参(query 生效),无回归
- [x] `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"` 触发 |