feat(msg): 完整实现 msg 消息服务

包含 channels/preferences/templates/grpc/kafka/outbox/push/redis 等完整实现
This commit is contained in:
SpecialX
2026-07-10 19:09:52 +08:00
parent 21530dc7f6
commit 7b7abbb309
51 changed files with 5204 additions and 371 deletions

View File

@@ -0,0 +1,99 @@
import { env } from "../../config/env.js";
import { logger } from "../observability/logger.js";
/**
* Push Gateway HTTP 客户端。
*
* 仲裁依据:
* - M4HTTP POST /internal/push豁免 gRPC
* - PushGateway 软失败:不可用时返回 false不阻断主流程
* - 鉴权头X-Internal-Key = PUSH_INTERNAL_TOKEN
*
* 请求体对齐 push-gateway handler.go PushHandler
* { userId, event, data }
*/
export interface PushRequest {
userId: string;
event: string;
data: Record<string, unknown>;
}
export interface PushResult {
sent: boolean;
error?: string;
}
/**
* 向 push-gateway 发送实时推送。
*
* 软失败语义:
* - PUSH_GATEWAY_URL 未配置 → 返回 { sent: false },不报错
* - 网络错误/非 2xx → 返回 { sent: false, error }logger.warn
* - 成功 → 返回 { sent: true }
*/
export async function sendPush(req: PushRequest): Promise<PushResult> {
if (!env.PUSH_GATEWAY_URL) {
return { sent: false };
}
const url = `${env.PUSH_GATEWAY_URL}/internal/push`;
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (env.PUSH_INTERNAL_TOKEN) {
headers["X-Internal-Key"] = env.PUSH_INTERNAL_TOKEN;
}
try {
const res = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify({
userId: req.userId,
event: req.event,
data: req.data,
}),
});
if (!res.ok) {
const error = `push-gateway returned ${res.status}`;
logger.warn({ status: res.status, userId: req.userId }, error);
return { sent: false, error };
}
return { sent: true };
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
logger.warn({ err, userId: req.userId }, "Push gateway unavailable");
return { sent: false, error };
}
}
/**
* 批量推送逐条发送push-gateway 无批量 API
* 任一失败不影响其他,返回每条结果。
*/
export async function sendPushBatch(
requests: PushRequest[],
): Promise<PushResult[]> {
const results: PushResult[] = [];
for (const req of requests) {
results.push(await sendPush(req));
}
return results;
}
/**
* 健康检查:探测 push-gateway /readyz。
* 软失败:不可用返回 false不阻断 /readyz仲裁 M2
*/
export async function checkPushGateway(): Promise<boolean> {
if (!env.PUSH_GATEWAY_URL) return false;
try {
const res = await fetch(`${env.PUSH_GATEWAY_URL}/readyz`, {
signal: AbortSignal.timeout(2000),
});
return res.ok;
} catch {
return false;
}
}