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,88 @@
import { Kafka, type Consumer, type Producer } from "kafkajs";
import { env } from "../../config/env.js";
import { logger } from "../observability/logger.js";
/**
* msg 服务 Kafka 客户端。
*
* 仲裁依据:
* - producer 配置 idempotent=true + transactionalIdat-least-once + 有序)
* - consumer 使用 KAFKA_CONSUMER_GROUP_ID 隔离消费组
* - Kafka 不可用时服务仍启动降级模式outbox 消息暂存待发)
*
* 使用 lazy initialization连接在首次 connect 时建立,
* 避免模块导入副作用导致测试环境难以 mock。
*/
let kafkaInstance: Kafka | null = null;
let producerInstance: Producer | null = null;
let consumerInstance: Consumer | null = null;
let kafkaHealthy = false;
function getKafka(): Kafka {
if (!kafkaInstance) {
kafkaInstance = new Kafka({
brokers: env.KAFKA_BROKERS.split(","),
clientId: env.KAFKA_CLIENT_ID,
});
}
return kafkaInstance;
}
export function getProducer(): Producer {
if (!producerInstance) {
producerInstance = getKafka().producer({
idempotent: true,
transactionalId: "msg-service-tx",
});
}
return producerInstance;
}
export function getConsumer(): Consumer {
if (!consumerInstance) {
consumerInstance = getKafka().consumer({
groupId: env.KAFKA_CONSUMER_GROUP_ID,
sessionTimeout: 30000,
rebalanceTimeout: 60000,
});
}
return consumerInstance;
}
export async function connectKafka(): Promise<void> {
try {
await getProducer().connect();
await getConsumer().connect();
kafkaHealthy = true;
logger.info(
{ brokers: env.KAFKA_BROKERS, clientId: env.KAFKA_CLIENT_ID },
"Kafka connected",
);
} catch (err) {
kafkaHealthy = false;
logger.warn(
{ err },
"Kafka connect failed, running without Kafka (outbox will buffer)",
);
}
}
/** /readyz 健康检查Kafka producer+consumer 是否已连接 */
export function isKafkaHealthy(): boolean {
return kafkaHealthy;
}
export async function disconnectKafka(): Promise<void> {
try {
if (consumerInstance) {
await consumerInstance.disconnect();
}
if (producerInstance) {
await producerInstance.disconnect();
}
kafkaHealthy = false;
logger.info("Kafka disconnected");
} catch (err) {
logger.warn({ err }, "Kafka disconnect error (ignored on shutdown)");
}
}