diff --git a/changelogs-v2/2026-09/22_8182_HOUSE站内信link订正到后台路由与bizId不是路由键-修复-管理后台.md b/changelogs-v2/2026-09/22_8182_HOUSE站内信link订正到后台路由与bizId不是路由键-修复-管理后台.md new file mode 100644 index 00000000..e920fa31 --- /dev/null +++ b/changelogs-v2/2026-09/22_8182_HOUSE站内信link订正到后台路由与bizId不是路由键-修复-管理后台.md @@ -0,0 +1,353 @@ +--- +schema: "hl-changelog/v2" +ticket: "8182" +title: "通知中心: HOUSE 内部员工站内信 link 由小程序路径订正为管理后台路由,并明确 bizId 不是路由键" +consumer: "admin" +author: "wx(GIT)" +change_type: "修复" +backend_status: "deployed" +gateway_status: "not_required" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "本条不改变 AdminMessageRespVO / AdminMessagePageReqVO 的字段结构,只修正 notification_event_config.inapp_link_template 中三个内部员工事件码的路由域(小程序 /pages/* → 管理后台 /housekeeper/*),并在 HouseNotificationPublisher 类注释上固化「(bizType, bizId) 不是路由键」这一约束。gateway_status=not_required:零新增路由,受影响的 GET /admin/message/list 与 GET /admin/message/{id} 是既有端点。frontend_status=pending:本条与 22_8155 不同,它需要前端改动——hl-ui 的 jumpToBiz(origin/v2.1 590c1155,src/views/notification/MyMessages/index.vue:268-286)目前只读 bizId/bizModule/bizType/peerRole,对 HOUSE 核房通知会落进 getOrderReadableRoute(bizId) 分支,把 hotelId 当 orderId 打开订单详情页;正确做法是改读 link 字段(该字段早已存在于 AdminMessageRespVO:31,不是本次新增)。backend_status=deployed:hl-user-service 与 hl-order-service-v3 均已滚动到测试服 776c0023d,Flyway 20260922.210 success=1(installed_on 2026-09-22 17:18:51),并在自建酒店夹具上走完「核房提交 → 站内信产出 → GET /admin/message/list 读回」全链路,实测读数见正文第八节。⚠️ admin_message 是快照表:link 在 createMessage 落库那一刻按当时的模板一次性渲染写入行内,不是按需渲染;因此本修复只影响生效后新产生的消息,测试库里此前已产出的 213 条 HOUSE_INVENTORY_CHECKED 历史消息 link 仍是旧的小程序路径,不做批量回刷——order-v3 / fleet 尚未上生产,不涉及生产数据。" +updated_at: "2026-09-22" +base: "dev-v3" +--- + +# 通知中心: HOUSE 内部员工站内信 link 由小程序路径订正为管理后台路由,并明确 bizId 不是路由键 + +> **存放目录**: 二期(order-v3)→ `changelogs-v2/2026-09/` +> +> **服务**: hl-user-service(通知分发 / 站内信收件箱 / 配置表)+ hl-order-service-v3(通知发布方,本次仅注释) +> **PR**: [#8189](https://git.1814.love:8443/wx/HL/pulls/8189)(迁移 + 注释 + 迁移测试)、[#8190](https://git.1814.love:8443/wx/HL/pulls/8190)(补注释漏枚举) +> **Issue**: [#8182](https://git.1814.love:8443/wx/HL/issues/8182) +> **日期**: 2026-09-22 +> **影响范围**: 管理后台站内信中心里 HOUSE 类通知的「跳转」行为 + +--- + +## ⚠️ 关键变化 + +1. **三个内部员工事件码的 `link` 换了路由域**:`HOUSE_INVENTORY_CHECKED` / `HOUSE_INQUIRY_TIMEOUT` / `HOUSE_INQUIRY_ESCALATED` 的 `link` 由小程序路径 `/pages/house/...` 改为管理后台路由 `/housekeeper/calendar` 与 `/housekeeper/todos`。字段名、类型、位置都没变,**变的是值**。 + +2. **「跳转」必须改读 `link`,不能再从 `(bizType, bizId)` 反推。** 这是本条与 `22_8155` 的关键差别:`22_8155` 的正文写过「前端 `jumpToBiz` 只读 `row.bizId/bizModule/bizType/peerRole`,从不读 `row.link`,无需任何前端代码改动」——**那句话在 `REQUIREMENT_REJECTED` 这一个事件上成立,不能推广到 HOUSE 全域**。`REQUIREMENT_REJECTED` 的 `bizId` 修好之后确实就是 `orderId`,按订单跳是对的;但核房、团期两类事件的 `bizId` **根本不是订单**(见第四节 bizId 三义表),任何「拿 bizId 当订单 id 跳」的写法在它们身上必然打开一个不存在的订单。因此本条的 `frontend_status` 是 `pending`,不是 `not_required`。 + +3. **`link` 为空、或其路径不在管理后台路由表内时,不要渲染「跳转」入口**。存量历史消息的 `link` 仍是旧的小程序路径,它们在后台是打不开的。 + +--- + +## 一、背景 + +### 缺陷形态 + +缺陷**不是**「`link` 为空」,而是「`link` 非空、但写的是另一个端的路由」——这两者在「`link` 是否非空」这个判据下完全同形,所以此前没人发现。 + +```sql +-- notification_event_config 里 category_code='HOUSE' 的 6 行,inapp_link_template 全部非空 +-- 且全部形如 /pages/house/...(小程序路由) + +-- 管理后台路由的权威源里,没有任何一条 /pages/* 路径 +SELECT COUNT(*) FROM sys_menu WHERE path LIKE '/pages/%' AND status='ACTIVE'; + -> 0 + +-- 测试库里已产出的 HOUSE 站内信,link 100% 落在小程序路由域 +SELECT IFNULL(event_code,'') ec, kind, COUNT(*) cnt, + SUM(link IS NULL) null_link, SUM(link LIKE '/pages/%') pages_link + FROM admin_message WHERE biz_type='HOUSE' GROUP BY 1,2; + -> HOUSE_INVENTORY_CHECKED NOTIFY 213 0 213 + -> CHAT 107 107 NULL +``` + +(那 107 行 `link IS NULL` 的是房务 IM 聊天消息,`kind='CHAT'`、`event_code IS NULL`,不经通知中心分发,本就不该有 `link`,与本缺陷无关。) + +### 触发路径 + +管理后台 → 消息中心 → 一条「核房记录已更新」通知 → 操作列「更多」→「跳转」。 + +前端侧当前实现(hl-ui `origin/v2.1` @ `590c1155`,只读查证、未改动): + +- `src/views/notification/MyMessages/index.vue:477` — `if (row.bizId) more.push({ text: '跳转', ... onClick: jumpToBiz })`,**只要 `bizId` 非空就渲染「跳转」**,NOTIFY 行同样渲染。 +- `src/views/notification/MyMessages/index.vue:268-286` — `jumpToBiz(row)` 依次判断 `mod === 'FLEET'` → 车务看板;`peer === 'CUSTOMIZER' && (mod === 'HOUSE' || mod === 'HOUSE_LEAD')` → `/housekeeper/orders?orderId=`;**否则 → `getOrderReadableRoute(bizId, userStore)`**。 +- `src/utils/orderAccess.js:48-54` — `getOrderReadableRoute` 对受限房务角色返回 `/housekeeper/orders?orderId=`,其余返回 `/order-v2/detail/`。 + +核房通知是 `kind='NOTIFY'`、没有 `peerRole`,于是落进最后那个 `else` 分支 ⇒ 用 **hotelId** 去打开 `/order-v2/detail/`。雪花 ID 形态一致,页面不会报「参数非法」,只会报「订单不存在」或空白——**失败是静默的**。 + +### 数据落地路径(已核实的完整链路) + +| 环节 | 位置 | +|---|---| +| 业务入口 | `POST /v3/admin/house/hotels/{hotelId}/check-log` — `HouseHotelAdminController.java:63-70` | +| 组装 extras | `HouseInventoryCheckService.java:153-164`,放入 `hotelId` / `checkDate` / `roomTypeCount` / `operatorId` / `operatorName` / `hotelName` | +| 发布事件 | `HouseNotificationPublisher.publishNotificationEvent("HOUSE_INVENTORY_CHECKED", hotelId, notifyExtras)` — `HouseNotificationPublisher.java:111-139`,**bizId 实参就是 hotelId** | +| MQ | `HL_NOTIFICATION_EVENT_TOPIC` → `NotificationEventConsumer` | +| 渲染 link | `NotificationDispatcher` 按 `notification_event_config.inapp_link_template` 用 extras 渲染占位符 | +| 落库 | `admin_message`(快照表,`link` 与 `biz_id` 在 `createMessage` 那一刻写死在行内) | +| 读回 | `GET /admin/message/list` — `AdminMessageController.java:44-50` | + +--- + +## 二、变更接口清单 + +| 方法 | 路径 | 变更类型 | 说明 | +|---|---|---|---| +| GET | `/admin/message/list` | 出参**取值**变化 | `link` 字段的值域由 `/pages/house/*` 改为 `/housekeeper/*`;字段结构不变 | +| GET | `/admin/message/{id}` | 出参**取值**变化 | 同上 | + +**没有**新增 / 删除 / 改名任何字段,也没有新增任何路由。 + +--- + +## 三、接口详情 + +### 1. 站内信分页列表 `GET /admin/message/list` + +**使用场景**:管理后台消息中心列表。本条只影响其中 `bizType='HOUSE'` 且 `kind='NOTIFY'` 的行。 + +**入参**:无变化。 + +**出参**:`AdminMessageRespVO` 结构无变化。与本条相关的字段: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `messageId` | String | 消息 ID(雪花,字符串透传) | +| `categoryCode` | String | **`SYSTEM`**,不是 `HOUSE`——见第六节「边界行为」第 1 条 | +| `title` | String | 如「核房记录已更新」 | +| `link` | String | **跳转目标。本条改的就是它。** 字段早已存在(`AdminMessageRespVO.java:31`),不是本次新增 | +| `bizId` | String | 关联业务 ID,**语义随事件码变化,不是路由键**,见第四节 | +| `bizType` | String | HOUSE 域内恒为 `"HOUSE"` | +| `kind` | String | `NOTIFY`=系统通知 / `CHAT`=聊天 | + +**实测响应片段**(自建夹具,完整取证见第八节): + +```json +{ + "messageId": "2102328540089466882", + "categoryCode": "SYSTEM", + "title": "核房记录已更新", + "link": "/housekeeper/calendar?hotelId=2102328465699270657&date=2026-09-23", + "bizId": "2102328465699270657", + "bizType": "HOUSE", + "kind": "NOTIFY" +} +``` + +**错误码**:无新增。 + +### 2. 站内信详情 `GET /admin/message/{id}` + +同上,返回结构与 `link` 取值口径一致。 + +--- + +## 四、契约约束与正确调用方式 + +### 「跳转」的唯一正确来源是 `link` + +```js +// ✅ 正确 +function jumpToBiz(row) { + const link = String(row.link ?? '') + if (!link || !isKnownAdminRoute(link)) { + // 不渲染 / 不跳转,见下方「兜底要求」 + return + } + router.push(link) +} + +// ❌ 错误:从 (bizType, bizId) 反推页面 +router.push(`/order-v2/detail/${row.bizId}`) // bizId 可能是 hotelId / groupBatchId +``` + +### `bizId` 三义表(HOUSE 域,截至 #8182 的全部生产调用点) + +`bizType` 在 HOUSE 域恒为 `"HOUSE"`,而 `bizId` 承载三种互不相同的业务实体,且三者都是雪花 ID、**单看值分辨不出是哪一类**: + +| 事件码 | `bizId` 实际是 | 发布位置 | +|---|---|---| +| `HOUSE_HOTEL_SWAPPED_OLD_HOTEL` / `_NEW_HOTEL` / `_CUSTOMER` | **orderId** | `HouseAssignmentService.java:1941/1942/1943` | +| `GROUP_BATCH_HOUSE_CLAIMED` / `GROUP_BATCH_HOUSE_RELEASED` | **groupBatchId** | `HouseGroupGrabService.java:731` | +| `GROUP_BATCH_REQUIREMENT_CONFIRMED` | **groupBatchId** | `GroupBatchRequirementService.java:1286` | +| `HOUSE_INVENTORY_CHECKED` | **hotelId** | `HouseInventoryCheckService.java:164` | + +合计 6 条 publish 语句 / 4 个发起方法(换店三条同在一个方法内)。该约束已固化在 `HouseNotificationPublisher` 的类注释里(PR #8189 写入、#8190 补齐漏枚举),新增调用点时要一并维护。 + +**为什么不把 `bizType` 拆细**:核房是「某酒店某天」的房态盘点,团期抢单是「某个团期班期」的归属变更,它们**在业务上根本没有订单维度**,强行编一个 orderId 只会制造假关联。`bizId` 传各自的真实主键是对的语义,代价就是它不能再兼任路由键。 + +### 兜底要求 + +`link` 为空、或 `link` 的路径部分不在管理后台已知路由表内时,**不渲染「跳转」入口**(而不是渲染出来再跳到 404)。理由见第五节:`admin_message` 是快照表,历史行里存着的就是旧路由域的字符串。 + +### 前端需要知道的取值边界 + +- `link` 是**相对路径 + query**,不含域名,可直接交给 `router.push`。 +- query 里的 ID 一律按**字符串**处理,禁 `Number()`——雪花 ID 超 `Number.MAX_SAFE_INTEGER`。 +- `${占位}` 取不到值时会被渲染成空串,形如 `?inquiryId=`。把这种情况按「参数缺失」处理,降级为打开列表页即可。 + +--- + +## 五、数据库行为 + +### `notification_event_config`(Flyway `V20260922_210`,hl-user-service) + +纯 UPDATE 无 DDL,按 `event_code`(唯一键 `uk_event_code`)定位,可重复执行: + +| event_code | 改前 | 改后 | +|---|---|---| +| `HOUSE_INVENTORY_CHECKED` | `/pages/house/inventory?hotelId=${hotelId}&date=${checkDate}` | `/housekeeper/calendar?hotelId=${hotelId}&date=${checkDate}` | +| `HOUSE_INQUIRY_TIMEOUT` | `/pages/house/inquiry?inquiryId=${inquiryId}` | `/housekeeper/todos?inquiryId=${inquiryId}` | +| `HOUSE_INQUIRY_ESCALATED` | `/pages/house/inquiry?inquiryId=${inquiryId}` | `/housekeeper/todos?inquiryId=${inquiryId}` | + +改与不改的判据是**收件人域**:这三条的收件人(`HOUSE_TEAM` / `INQUIRY_CLAIMER` / `HOUSE_LEAD`)都是内部员工,点开只会落在管理后台;而换酒店三事件的收件人是酒店联系人与 C 端客户,本就不属于后台路由域,**一个字节未动**。 + +两个目标路由在后台菜单表里都存在: + +```sql +SELECT path, menu_name, status FROM sys_menu WHERE path IN ('/housekeeper/calendar','/housekeeper/todos'); + -> /housekeeper/calendar 日历视图 ACTIVE + -> /housekeeper/todos 待处理 ACTIVE +``` + +### `admin_message`(快照表) + +`link` 与 `biz_id` 在 `createMessage` 落库那一刻一次性写入行内,**不是按需渲染**。所以: + +- 生效后**新产生**的消息,`link` 是新路由; +- 此前已产出的 **213 条** `HOUSE_INVENTORY_CHECKED` 历史消息,`link` 仍是旧的小程序路径,**不做批量回刷**(order-v3 / fleet 尚未上生产,测试库脏数据不值得回刷,且回刷会掩盖兜底逻辑缺失)。前端按上面的「兜底要求」处理即可。 + +--- + +## 六、边界行为 + +1. **`categoryCode` 是 `SYSTEM` 而不是 `HOUSE`。** 核房通知虽然 `bizType='HOUSE'`,但落库时 `category_code='SYSTEM'`(`AdminMessageRespVO.java:20-22` 的注释即写明「系统通知为 SYSTEM」)。**用 `categoryCode=HOUSE` 过滤会得到 0 条**,这是实测踩到的坑,按 `bizType` 或不过滤来取。 + +2. **换酒店三事件当前 5 个渠道零产出。** 它们的 `wework_receiver_type`(`OLD_HOTEL_CONTACT` / `NEW_HOTEL_CONTACT` / `ORDER_CUSTOMER`)不在 `NotificationDispatcher.INTERNAL_INAPP_RECEIVER_TYPES`(`NotificationDispatcher.java:100-103`)内 ⇒ 站内信不落 `admin_message`;`HouseNotificationPublisher` 构造消息时不设 `userId` ⇒ C 端 `user_message` 也不落;其余渠道开关均为 0。所以前端在消息中心里**看不到**这三类消息——这是它们自身的既有状态,与本次改动无关。 + +3. **三个 `GROUP_BATCH_*` 事件码在配置表里没有行。** 实测 `notification_event_config` 全表 62 行,`event_code LIKE 'GROUP%'` 零命中(同一条 SQL 查 `HOUSE_INVENTORY_CHECKED` 可返回,作阳性对照)。`NotificationDispatcher` 查不到 config 只记日志 ⇒ 团期房务抢单 / 释放 / 需求整体确认**一条通知都不产生**,即 `bizId` 三义中的 `groupBatchId` 那一义对任何消费方目前都不可见。已另立单跟进配置补齐。 + +4. **`HOUSE_LEAD` 不在站内信白名单内。** 所以 `HOUSE_INQUIRY_ESCALATED` 即便将来接上发布方,站内信也不会落 `admin_message`——它缺的是扩白名单,不是改 `link`。本次对它的订正是「把路由域写对」,当前实际效果为零。同理 `HOUSE_INQUIRY_TIMEOUT` / `_ESCALATED` 在 Java 主代码里目前没有发布方。 + +5. **聊天消息(`kind='CHAT'`)不受影响**:`link` 本就是 `null`,`event_code` 也是 `null`,不经通知中心分发。 + +--- + +## 六.5、枚举 / 数据字典 + +`bizType`(`AdminMessageRespVO.bizType`)HOUSE 域取值恒为 `"HOUSE"`,**不具备区分事件类型的能力**,不要用它做路由分支。事件类型看 `event_code`(列表接口不返回该字段),跳转看 `link`。 + +`kind`:`NOTIFY`=系统通知(有 `link`)/ `CHAT`=会话消息(`link` 为 `null`)。 + +--- + +## 六.6、修改前后对比 + +### 字段级对比 + +| 字段 | 改前 | 改后 | +|---|---|---| +| `AdminMessageRespVO.link`(核房事件) | `/pages/house/inventory?hotelId=...&date=...` | `/housekeeper/calendar?hotelId=...&date=...` | +| `AdminMessageRespVO.bizId`(核房事件) | hotelId | hotelId(**不变**,本次未动 bizId) | +| 其余字段 | — | 无变化 | + +### 行为级对比 + +| 场景 | 改前 | 改后 | +|---|---|---| +| 后端渲染出的 link 落在哪个路由域 | 小程序(后台不存在该路径) | 管理后台(`sys_menu` 内可查到) | +| 前端按 `bizId` 反推跳转 | 打开 `/order-v2/detail/`,订单不存在 | **仍然错**——所以前端必须改读 `link` | +| 前端按 `link` 跳转 | 跳到后台不存在的 `/pages/*` | 正确落到核房日历页 | + +⚠️ 这张表的第二行是重点:**只改后端不改前端,「跳转」依然是坏的**。后端这次把 `link` 修对了,把它变成一个可用的跳转来源;真正让用户点对页面的动作在前端侧。 + +--- + +## 六.7、影响评估 + +- **前端是否必须同步上线**:**是**。`jumpToBiz` 需改读 `link` 并补兜底判断,否则核房类通知的「跳转」仍落空。 +- **兼容性**:纯取值变化,老前端不会报错(只是跳转依旧不对),不会因为本条出现新的异常或白屏。 +- **数据**:仅 `notification_event_config` 三行 UPDATE,无 DDL、无数据迁移、无回刷。 + +--- + +## 七、不影响范围 + +- 不影响任何接口的字段结构、必填性、类型。 +- 不影响 C 端小程序站内信:小程序读的是 `user_message`,本次改的三个事件码收件人都是内部员工,不落 `user_message`。 +- 不影响换酒店三事件(`link` 一字节未改)。 +- 不影响聊天消息与会话聚合。 +- 不影响 `bizId` 的取值——本次没有动任何 `bizId` 实参。 +- 不影响 `22_8155` 的 `REQUIREMENT_REJECTED` 结论:那条改的是 `bizId`,本条改的是 `link`,两者互不覆盖。 + +--- + +## 八、测试环境已验证 + +**部署基线**:`hl-user-service` 与 `hl-order-service-v3` 均已滚动到测试服 `776c0023d`(`deploy-status.sh` 读数:两者 `BEHIND=0/N`)。 + +**Flyway**: + +``` +version success installed_on +20260922.210 1 2026-09-22 17:18:51 +``` + +**自建夹具**(全部自建,未复用他人数据): + +| 项 | 值 | +|---|---| +| 自建酒店 | `hotelId=2102328465699270657`(`8182验收自建酒店-1790069137`,`status=0` 下架) | +| 自建核房记录 | `checkLogId=2102328538344615937`,`checkDate=2026-09-23` | +| 产生的站内信 | `messageId=2102328540089466882` | + +**链路实测**:`POST /v3/admin/house/hotels/2102328465699270657/check-log` → `GET /admin/message/list` 读回该条,`link` 为: + +``` +/housekeeper/calendar?hotelId=2102328465699270657&date=2026-09-23 +``` + +三条断言逐条成立:① 以 `/housekeeper/calendar` 开头;② `hotelId=` 后为自建酒店 ID,逐字相同;③ `date=` 后为自建核房日期 `2026-09-23`,与请求体逐字相同。 + +**库侧独立复核**(不依赖接口返回): + +``` +id : 2102328540089466882 +category_code : SYSTEM +event_code : HOUSE_INVENTORY_CHECKED +kind : NOTIFY +biz_type : HOUSE +biz_id : 2102328465699270657 +link : /housekeeper/calendar?hotelId=2102328465699270657&date=2026-09-23 +title : 核房记录已更新 +``` + +`biz_id` 与 `link` 里的 `hotelId` 逐字一致 ⇒ 证明 `bizId` 语义未被本次改动影响。 + +**存量未回刷的对照**:同一次响应里的历史消息 `messageId=2100088394469044226`(迁移执行前产生)`link` 仍为 `/pages/house/inventory?hotelId=...`,与第五节「不回刷」一致。 + +**迁移单测**:`HouseInappLinkMigrationMysqlTest`(Testcontainers 真 MySQL 8.0.33 跑迁移)`Tests run: 5, Failures: 0, Errors: 0, Skipped: 0`,含阴性对照(换店三事件 `link` 零字节变化)与幂等复跑。 + +**改前阴性基线**:`SELECT COUNT(*) FROM sys_menu WHERE path LIKE '/pages/%' AND status='ACTIVE'` → `0`,即改动前没有任何一条 HOUSE link 能在后台路由表里查到——这条读数同时说明旧验收口径「`link` 非空」对本缺陷**没有分辨力**(改动前 6 条就全非空)。 + +--- + +## 十、相关文档 + +- `22_8155_需求驳回站内信bizId改为orderId点跳转落空-修复-管理后台.md` — 同一收件箱、相邻缺陷。⚠️ 其中「前端从不读 `row.link`,无需任何前端代码改动」一句的适用范围**仅限 `REQUIREMENT_REJECTED`**,不适用于本条覆盖的三个事件码,理由见「关键变化」第 2 条。 +- `HouseNotificationPublisher` 类注释(hl-order-service-v3)— `bizId` 三义的权威清单,新增调用点时同步维护。 +- Flyway `V20260922_210__fix_house_inapp_link_to_admin_routes.sql`(hl-user-service)— 脚本头注写明了「按收件人域取舍」以及为什么不能改用白名单当判据。 + +--- + +## 关联 / 联系人 + +### 链接 + +- Issue: [#8182](https://git.1814.love:8443/wx/HL/issues/8182) +- PR: [#8189](https://git.1814.love:8443/wx/HL/pulls/8189) / [#8190](https://git.1814.love:8443/wx/HL/pulls/8190) + +### 联系人 + +- 后端: wx +- 前端: 待认领(`frontend_status: pending`)