# 通知第二批:删除、多选与回收站对接 日期:2026-10-28。面向前端开发/联调。仅描述本批后端实现,**尚未提交、推送或发布**。第一批“业务关联与已读”交接文档继续适用;本批没有新增历史记录页面、永久删除或自动清空。 ## 1. 接口与字段 前缀 `/api/v1`,全部接口使用正常 access token:`Authorization: Bearer `,身份从安全上下文取得。不得传 userId 来指定通知归属;企业仍受已有认证门禁限制。 | 操作 | 方法与路径 | 请求 | 成功 data | |---|---|---|---| | 正常列表(已有) | `GET /notifications?page=0&size=20` | page≥0,size=1~50 | 原分页信封,仅正常通知 | | 回收站 | `GET /notifications/trash?page=0&size=20` | 同上 | 回收站分页信封 | | 单条移入回收站 | `DELETE /notifications/{id}` | 无请求体,正整数通知 ID | `{"changed":true}` | | 单条恢复 | `POST /notifications/{id}/restore` | 无请求体,正整数通知 ID | `{"changed":true}` | | 批量移入回收站 | `POST /notifications/batch-delete` | `{"ids":[28,29]}` | `{"requested":2,"changed":2}` | | 批量恢复 | `POST /notifications/batch-restore` | `{"ids":[28,29]}` | `{"requested":2,"changed":2}` | 均为统一响应 code=200,data 外仍有 message、requestId、timestamp。重复删除已删除通知、恢复正常通知均成功但 changed=false;批量 changed 是实际改变状态的条数,requested 是去重后的 ID 数。 ### 批量约束 - ids 必须为非空数组,原始数组长度 1~100。超过 100 条即拒绝,不会先去重再绕过上限。 - 只接受 JSON 正整数,不能是字符串、浮点数、布尔值、null、0、负数或超出 int64 范围的值。 - 重复 ID 去重,例如 `[28,28]` → requested=1,最多改变一次。 - **整批原子操作**:任意 ID 不存在、归属其他账号,整批失败,无部分成功。已经处于目标状态的本人通知是幂等项,不构成失败。 - 前端现有通知 id 若存为 string,发送批量请求前必须严格校验并转为安全整数;不能把 string 数组直接发给后端,也不能用 parseInt 静默截断。 ```ts function batchIds(ids: string[]): number[] { if (ids.length < 1 || ids.length > 100) throw new Error('一次请选择 1~100 条通知'); return [...new Set(ids.map(id => { if (!/^[1-9]\d*$/.test(id)) throw new Error('通知 ID 无效'); const value = Number(id); if (!Number.isSafeInteger(value)) throw new Error('通知 ID 超出安全整数范围'); return value; }))]; } ``` ### 回收站响应 ```json { "code": 200, "message": "ok", "data": { "items": [{ "id": 28, "type": "INTERVIEW_INVITED", "title": "面试邀请", "content": "你收到一条面试邀请", "read": false, "readAt": null, "createdAt": "2026-10-27T10:00:00", "businessType": "INTERVIEW", "businessId": 123, "deletedAt": "2026-10-28T10:00:00" }], "total": 1, "page": 0, "size": 20, "totalPages": 1 } } ``` - 正常列表仍为第一批九个条目字段,不额外返回 deletedAt;回收站在原字段上增加非空 deletedAt。 - 正常列表按原 createdAt/id 倒序;回收站按 deletedAt/id 倒序。恢复后保留原 createdAt,回到正常列表的原时间位置,不保证出现在第一条。 - 删除/恢复不改 type、正文、业务关联、read 或 readAt,也不创建新通知;重复删除不刷新 deletedAt。 ## 2. 左滑、多选与回收站流程 ### 正常通知列表 1. **左滑**:露出“删除”按钮,点击后才发送 DELETE。不要一次滑动就直接执行。 2. 成功后从正常列表移除该项,并刷新分页总数和全局未读数。失败保留或回滚该项,显示可重试反馈。 3. **长按**:进入多选模式,显示选择数、“取消”“删除所选”,可以提供“全选当前已加载项”。 4. 单次最多选择 100 条。已加载更多时应明确限制或提示分批处理,禁止静默截断,也不要后台拆成多批却声称整次操作原子成功。 5. 批量删除前确认所选数量。成功后退出或更新选择状态;失败不应清空选择,也不能只移除一部分记录。 6. 正常点击继续走第一批“通知全文/业务详情 + 标记已读”,与左滑、长按手势区分,避免误触。 ### 回收站 1. 提供独立回收站入口和分页列表,支持单条恢复、长按多选及恢复所选。 2. 回收站不提供“再次删除”“永久删除”“清空回收站”,也不自动过期清理。 3. 回收站中可以查看通知内容,但不要因浏览而调用标记已读。恢复后再按正常通知流程阅读。 4. 恢复成功后刷新正常列表、回收站、两个通知入口和全局未读数;恢复失败保留条目并提示重试。 ### 红点与已读规则 - 未读通知移入回收站后,不再计入 `/notifications/unread-count`。 - 恢复时保留原 read/readAt:原未读通知重新计数;原已读通知仍已读,不增加未读数。 - 原 `POST /notifications/read-all` **只处理正常列表**,不会修改回收站中的通知。 - 对回收站里的通知调用 `POST /notifications/{id}/read` 会拒绝,不得把它当作已读成功。 - 不要直接用 changed 扣减未读数:删除项可能混合已读与未读。成功后以未读数接口为准,并保护账号切换和旧响应覆盖。 ## 3. 权限、错误与业务边界 | 场景 | 行为 | |---|---| | 未登录、过期或禁用账号原 Token | 401,走登录恢复 | | 参数或 JSON ID 格式非法 | 400,通常 code=1001,修正请求后重试 | | 通知不存在 | 沿用既有 HTTP 400 / code=9105 | | 对回收站条目标记已读 | HTTP 400 / code=9105,提示先恢复,不假装已读 | | 删除/恢复其他账号的通知 | 403 / code=2002;批量整批失败 | | 未认证企业 | 沿用企业认证门禁,按具体错误码展示,不一律解释成通知归属问题 | | 网络超时,是否成功未知 | 先刷新状态或安全重试;删除/恢复幂等,不能宣称已成功 | 删除仅是**当前用户自己的通知软删除**,不取消面试、不删除投递/Offer、不影响企业或对方收到的通知。业务详情的访问权限仍由各业务接口校验;业务目标已失效也不妨碍删除或恢复通知。 软删除不是物理清除数据;个人数据导出及账号注销清理继续遵循现有范围和规则。本模块不增加永久删除能力或历史操作记录页面。 ## 4. 联调验收 - 未读删除 → 红点计数减少 → 回收站保留 read=false → 恢复后重新计数。 - 已读删除/恢复后 readAt 不变,未读数不增加;重复操作 changed=false 或 0。 - “全部已读”只影响正常列表;回收站原未读恢复后仍未读。 - 正常列表和回收站分别分页;相同删除时间下顺序稳定,无重复/漏项。 - 1 条、100 条、重复 ID、101 条、非法 ID 和空选择均正确处理。 - 批量混入他人或不存在 ID,整批失败,无部分消失或部分恢复。 - A/B 账号隔离、切账号时旧响应不回写;未认证企业、禁用账号不放宽。 - 并发点击已读与删除,不会让通知意外复活;并发批量删除幂等。 - 前端只提交明确选择的 ID,不进行永久删除、不取消关联业务。 后端定向/迁移测试、完整构建、同次 JAR 黑盒、前端实机手势和线上部署分别登记,不互相替代。第一批验收快照保存在 `D:\Acceptance\qsp-notification-full-20261028-a1\`;当前工作区已叠加第二批,**不能再用第一批旧 13 文件清单直接发布当前工作区**。后续须按第一批依赖 + 第二批改动的完整合并基线重新验收。