13 KiB
【新增接口·管理后台】内部员工站内信收件箱 + SSE 实时推送(房务/车务可用)
PR: #3424 #3425(工单 #3414) 服务: hl-user-service + hl-gateway | 更新时间: 2026-06-04
存放目录:
changelogs-v2/2026-06/影响范围: 管理后台「通知中心 / 顶部消息铃铛 / 自定义推送」 状态: 已合并 dev-v3 + 测试服双实例部署 + API 全链路实测通过(含 SSE 实时推送过网关 9443)
⚠️ 关键说明
原站内信只能发给 C 端微信客人,内部员工(管理后台账号 admin_user:房务管家、车务调度、运营等)收不到站内信、只能收企微。本次补齐:
- 内部员工有了自己的站内信收件箱(
/admin/message/*,与 C 端/mp/message完全独立)。 - SSE 实时推送:前端用
EventSource连一条长连接,后端产生新站内信时实时下发,刷新顶部铃铛角标(无需轮询)。 - 自定义推送可选内部员工:原「自定义推送」选人只能搜 C 端客人,现可按角色 / 按人选内部员工发站内信。
前端需要做三块:① 顶部消息铃铛 + 未读角标 ② 站内信收件箱页(列表/分类/已读/删除)③ EventSource 客户端(连接 + 收到事件刷新角标)。下面给全套接口契约 + SSE 对接细节。
🔔 入口位置(重点,别漏)
当前管理后台「通知中心」只有 通知配置 / 消息分类管理 / 通知日志 / 自定义推送,缺一个让登录员工看「发给自己」站内信的入口。请加两个入口(指向同一套 /admin/message/* 接口):
- 全局顶部铃铛(所有页面常驻):红点未读数 =
unread-count,由 SSE 实时刷新;点击下拉显示最近几条 + 「查看全部」。 - 「我的消息」页:平铺收件箱——分页列表(
list)+ 单条/全部已读 + 删除(内部员工站内信统一 SYSTEM 分类,不需要分类 tab)。
⚠️ 菜单是 DB 驱动(sys_menu 表):本系统后台菜单存在 sys_menu,每行 component 指向前端组件(如「自定义推送」→ notification/CustomPush)。所以加「我的消息」要两步一起做:① 在 sys_menu「通知中心」(menu_id=950) 下加一行(component 指向新页面如 notification/MyMessages,可经 系统管理→菜单管理 加或随前端发版)② 实现该前端组件。只加菜单行不建组件 = 点开空白页,务必同步。
注意区分:现有「自定义推送」是发消息(运营发给员工/客人),本次新增的是收消息(员工看自己的站内信),是两个不同入口。
1. 接口清单
| # | 用途 | 方法 / 路径 |
|---|---|---|
| 1 | SSE 实时长连接 | GET /ws/admin-msg/stream(text/event-stream,见 §3) |
| 2 | 收件箱列表(分页,可按分类) | GET /admin/message/list |
| 3 | 未读总数(铃铛角标) | GET /admin/message/unread-count |
| 4 | 分类未读汇总 | GET /admin/message/categories |
| 5 | 单条标已读 | PUT /admin/message/{id}/read |
| 6 | 全部标已读 | PUT /admin/message/read-all |
| 7 | 某分类标已读 | PUT /admin/message/category/{categoryCode}/read |
| 8 | 删除单条 | DELETE /admin/message/{id} |
| 9 | 自定义推送选内部员工(搜人) | GET /admin/notification/custom-push/admins |
| 10 | 自定义推送(发内部员工站内信) | POST /admin/notification/custom-push(channel=ADMIN_INAPP,见 §5) |
全部走管理后台 admin token(顶部铃铛/收件箱用当前登录员工身份,后端从网关注入的 X-Admin-Id 取人,前端无需传 adminId)。
2. 背景
房务/车务等业务需要给「内部员工」实时通知(到岗/到人),而站内信此前是 C 端专属。本次给内部员工建独立收件箱 admin_message(按 adminId 维度),并用 SSE 做实时角标刷新。SSE 仅作「有新消息了」的轻量信令,正文仍走收件箱列表接口拉取,DB 为唯一真相源(即便 SSE 偶尔断线也不丢消息,重连后拉未读对账即可)。
3. SSE 实时推送对接(重点)
3.1 建立连接
前端用浏览器原生 EventSource 连接。EventSource 不能自定义请求头,所以 token 走 query 参数 ?token=:
const token = /* 当前登录 admin token */;
const es = new EventSource(`https://api.test.1814.love:9443/ws/admin-msg/stream?token=${token}`);
- 鉴权:网关校验 token 通过后注入身份,未带 token / 无效 token 返 401。
- 缓冲:后端已下发
X-Accel-Buffering: no关闭 nginx 反代缓冲,前端无需处理。 - 保活:服务端每 25 秒发
:ping心跳,浏览器自动忽略。 - 重连:
EventSource断线自动重连;重连后建议主动调一次/admin/message/unread-count对账(防断线期间漏推)。
3.2 事件类型
| event | data | 说明 |
|---|---|---|
connected |
ok |
连接建立确认(可忽略,仅表示连上了) |
message |
JSON(见下) | 有新站内信,按此刷新角标 |
(:ping 注释行) |
— | 心跳,浏览器自动忽略 |
message 事件的 data 是轻量信令:
{ "adminId": 1001, "unreadCount": 2, "title": "realtime through nginx" }
unreadCount:该员工当前未读总数 → 直接用它刷新铃铛角标。title:新消息标题 → 可选弹个 toast 提示。- 正文/列表:收到
message后调/admin/message/list拉最新一页展示,不在 SSE 里传全文。
3.3 前端处理示例
es.addEventListener('message', (e) => {
const sig = JSON.parse(e.data);
setBadge(sig.unreadCount); // 刷新铃铛角标
toast(`新消息:${sig.title}`); // 可选
if (inboxOpen) reloadInboxList(); // 收件箱打开着就刷新列表
});
es.onerror = () => { /* EventSource 会自动重连;重连后 fetchUnreadCount() 对账 */ };
4. 收件箱接口详情
4.1 列表 GET /admin/message/list
Query:pageNo(默认1) / pageSize(默认20,≤100) / categoryCode(可选,按分类过滤)
curl -H "Authorization: Bearer <adminToken>" \
"https://api.test.1814.love:9443/admin/message/list?pageNo=1&pageSize=20"
响应 data:{ records: AdminMessageRespVO[], total, ... }(标准分页)。
4.2 未读数 / 分类汇总
# 铃铛角标
GET /admin/message/unread-count -> data: 2 (整数)
# 分类未读汇总
GET /admin/message/categories -> data: [{categoryCode, categoryName, unreadCount}, ...]
⚠️ 内部员工站内信分类统一为 SYSTEM(不用 C 端 订单/行程/促销 分类)。所以
categories只会返回一条SYSTEM,「我的消息」做平铺列表即可,不需要分类 tab(category/{code}/read也基本用不到,标已读直接用read-all或单条)。
4.3 标已读 / 删除
PUT /admin/message/{id}/read # 单条已读
PUT /admin/message/read-all # 全部已读
PUT /admin/message/category/{categoryCode}/read # 某分类已读
DELETE /admin/message/{id} # 删除(软删)
进入某条消息 / 某分类时调对应「已读」,未读数随即下降;越权防护:只能操作自己的消息(后端按 adminId 限定)。
5. 自定义推送选内部员工
5.1 搜人 GET /admin/notification/custom-push/admins
Query:keyword(姓名模糊,可空) / roleKey(按角色过滤,可空)
curl -H "Authorization: Bearer <adminToken>" \
"https://api.test.1814.love:9443/admin/notification/custom-push/admins?keyword=张"
响应 data:AdminPushTargetVO[](adminId/name/username/roleKey/roleName),供选人下拉。
🔴 2026-06-05 前端接线 bug(mmg 必看):测试环境「自定义推送」页选「站内信(内部员工)」+「指定员工」后,「选择员工」下拉显示「无数据」。查测试服 nginx 访问日志实证:该页面浏览器只调了
/admin/role/all(给「按角色」用),对本接口/admin/notification/custom-push/admins零调用。即下拉 UI 建了但没接搜索接口。后端实测 200、返 20 个员工,后端没问题。
- 修法:把「指定员工」的选择员工下拉接成远程搜索——输入时调
GET /admin/notification/custom-push/admins?keyword={输入值},用返回的adminId作 option value、name(无则 username) 作 label,最终提交 §5.2 的adminIds。- ⚠️ 字段名和 C 端不一样别照搬:C 端选人接口
/custom-push/users返id/realName/phone,本员工接口返的是adminId/name/username。若克隆 C 端组件,必须改字段映射(id→adminId、realName→name),否则即便接上了也会因取不到字段而「无数据」。
5.2 发内部员工站内信 POST /admin/notification/custom-push
在原「自定义推送」请求上新增:channel="ADMIN_INAPP" + 两种目标类型:
# 按指定员工
curl -X POST -H "Authorization: Bearer <adminToken>" -H 'Content-Type: application/json' \
-d '{"channel":"ADMIN_INAPP","targetType":"ADMIN_SPECIFIC","adminIds":[1001,1002],
"title":"系统维护通知","content":"今晚 22:00 维护","categoryCode":"SYSTEM"}' \
"https://api.test.1814.love:9443/admin/notification/custom-push"
# 按角色(roleKeys 如 house_lead/house_keeper)
# body: {"channel":"ADMIN_INAPP","targetType":"ADMIN_BY_ROLE","roleKeys":["house_lead"], "title":..,"content":..}
响应:{ totalCount, successCount, failCount }。被推的员工立即在 SSE 收到 message 事件、收件箱多一条。
说明 1:消息分类对内部员工固定 SYSTEM,后端强制写 SYSTEM,前端**「消息分类」下拉对内部员工可隐藏/不传**(传了也会被忽略为 SYSTEM)。 说明 2:按角色(ADMIN_BY_ROLE)当前覆盖已绑企微的同角色活跃员工;要覆盖未绑企微的,用 ADMIN_SPECIFIC 指定人。
6. 数据结构
6.1 AdminMessageRespVO(收件箱条目)
| 字段 | 类型 | 说明 |
|---|---|---|
messageId |
string | 消息 ID(雪花,String 透传防精度丢失) |
categoryCode |
string | 分类编码(SYSTEM/ORDER/TRIP/PROMO …) |
title |
string | 标题 |
content |
string | 正文 |
link |
string | 跳转链接(可空) |
bizId / bizType |
string | 业务关联(可空) |
isRead |
int | 0=未读 1=已读(数字,非布尔) |
createTime |
long | 毫秒时间戳 |
6.2 AdminPushTargetVO(选人)
adminId(string) / name(企微真名优先,回退用户名) / username / roleKey / roleName
6.3 SSE message 信令
adminId(number) / unreadCount(number) / title(string)
9. 业务边界
- ✅ 与 C 端站内信完全隔离:内部员工走
admin_message,C 端客人走user_message,互不影响。 - ✅ DB 为真相源,SSE 仅信令:SSE 丢一条不丢数据(重连拉未读对账)。
- ✅ 多实例安全:经 Redis Pub/Sub 跨实例广播,员工连任意实例都能实时收到。
- ✅ 越权防护:收件箱所有读写按当前 adminId 限定,只能看/操作自己的消息。
- ✅ 房务/车务事件:内部员工类事件(房务组长/房务组/认领人等)开站内信后也会落员工收件箱(与企微并行)。
11. 影响评估
- 破坏性:无(全为新增接口 + 新增 channel/targetType,C 端站内信与原自定义推送 C 端路径零改动)。
- 前端是否必须同步:是(铃铛 + 收件箱页 + EventSource 客户端 + 自定义推送选人加「内部员工」选项)。
- 存量数据:无影响(新表新建)。
12. 注意事项
- SSE token 走 query(
?token=),EventSource 不能带 Authorization 头;后端已处理 nginx 缓冲,前端正常用EventSource即可。 - SSE 是信令不是正文:收到
message用unreadCount刷角标,正文调/admin/message/list拉。 - 重连对账:EventSource 自动重连,重连后调一次
unread-count防漏推。 - isRead 是数字 0/1(不是布尔),
messageId/adminId出参注意是 String 透传防精度。 - 未读角标双来源:登录/刷新页用
unread-count拉一次初值,之后由 SSEmessage事件增量刷新。
13. 关联
- 工单 #3414;PR #3424(主功能)+ #3425(SSE 过 nginx 缓冲修复)
- 后端负责人:@wx
- 前端对接(管理后台):mmg