feat(push-gateway,msg): redis pubsub backplane for real-time notifications

M7: ADR-040 Redis Pub/Sub as state routing backplane

- push-gateway: remove Kafka consumer, add SSE endpoint

- SSE: subscribe to Redis user:{userId}:notify on connect

- msg: publish notifications to Redis Pub/Sub instead of HTTP push

- docker-compose: remove Kafka env from push-gateway
This commit is contained in:
SpecialX
2026-07-15 01:28:55 +08:00
parent a75527be80
commit 6af1aa0d82
14 changed files with 402 additions and 484 deletions

View File

@@ -8,21 +8,24 @@ import type {
* InAppChannel —— 站内信渠道。
*
* 站内信数据已在 NotificationService 中写入 MySQL + ES
* 此渠道仅负责触发 push-gateway 实时推送给在线用户(软失败)。
* 此渠道仅负责触发实时推送给在线用户(软失败)。
*
* M7 (ADR-040):实时推送通过 Redis Pub/Sub backplane 发布到
* `user:{userId}:notify` 通道realtime-gateway SSE 端点订阅并转发。
*
* 仲裁依据 02-architecture-design.md §2.4
* - in_app 总是发送,保证站内信可见
* - 实时推送通过 push-gateway,不在线时用户下次拉取即可见
* - 实时推送通过 Redis Pub/Sub,不在线时用户下次拉取即可见
*/
import { sendPush } from "../shared/push/push-gateway.client.js";
import { publishToRedisPubSub } from "../shared/push/redis-publisher.js";
import { logger } from "../shared/observability/logger.js";
class InAppChannel implements NotificationChannelStrategy {
readonly name = "in_app" as const;
async send(ctx: ChannelSendContext): Promise<ChannelSendResult> {
// 站内信数据已落库,仅触发实时推送
const pushResult = await sendPush({
// 站内信数据已落库,仅触发实时推送M7Redis Pub/Sub backplane
const pushResult = await publishToRedisPubSub({
userId: ctx.userId,
event: "notification.new",
data: {
@@ -37,7 +40,7 @@ class InAppChannel implements NotificationChannelStrategy {
if (!pushResult.sent) {
logger.debug(
{ userId: ctx.userId, notificationId: ctx.notificationId },
"In-app push not delivered (user offline or gateway unavailable)",
"In-app real-time push not delivered (user offline or Redis unavailable)",
);
}

View File

@@ -1,4 +1,4 @@
import { sendPush } from "../shared/push/push-gateway.client.js";
import { publishToRedisPubSub } from "../shared/push/redis-publisher.js";
import type {
ChannelSendContext,
ChannelSendResult,
@@ -8,16 +8,16 @@ import type {
/**
* PushChannel —— 移动推送渠道。
*
* 通过 push-gateway HTTP /internal/push 推送。
* 仲裁依据 M4HTTP POST豁免 gRPC
* M7 (ADR-040):通过 Redis Pub/Sub backplane 发布到
* `user:{userId}:notify` 通道realtime-gateway SSE 端点订阅并转发
*
* 软失败:push-gateway 不可用或用户离线时返回 sent=false不阻断。
* 软失败:Redis 不可用或用户离线时返回 sent=false不阻断。
*/
class PushChannel implements NotificationChannelStrategy {
readonly name = "push" as const;
async send(ctx: ChannelSendContext): Promise<ChannelSendResult> {
const result = await sendPush({
const result = await publishToRedisPubSub({
userId: ctx.userId,
event: "notification.push",
data: {