feat(core-edu): 完整实现 core-edu 教学核心服务
包含 classes/exams/homework/grades/attendance/scheduling 域、outbox、iam-consumer、redis 配置等完整实现
This commit is contained in:
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
|
||||
const envSchema = z.object({
|
||||
PORT: z.string().default("3004"),
|
||||
GRPC_PORT: z.string().default("50053"),
|
||||
DATABASE_URL: z.string().url(),
|
||||
REDIS_URL: z.string().url().optional(),
|
||||
JWT_SECRET: z.string().optional(),
|
||||
@@ -23,7 +24,7 @@ export function loadEnv(): Env {
|
||||
const result = envSchema.safeParse(process.env);
|
||||
if (!result.success) {
|
||||
console.error(
|
||||
"❌ Invalid environment variables:",
|
||||
"Invalid environment variables:",
|
||||
result.error.flatten().fieldErrors,
|
||||
);
|
||||
throw new Error("Invalid environment configuration");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Kafka } from "kafkajs";
|
||||
import { env } from "./env.js";
|
||||
import { logger } from "../shared/observability/logger.js";
|
||||
|
||||
export const kafka = new Kafka({
|
||||
brokers: env.KAFKA_BROKERS.split(","),
|
||||
@@ -13,15 +14,35 @@ export const producer = kafka.producer({
|
||||
|
||||
export const consumer = kafka.consumer({ groupId: "core-edu-group" });
|
||||
|
||||
let producerConnected = false;
|
||||
let consumerConnected = false;
|
||||
|
||||
producer.on("producer.connect", () => {
|
||||
producerConnected = true;
|
||||
});
|
||||
producer.on("producer.disconnect", () => {
|
||||
producerConnected = false;
|
||||
});
|
||||
consumer.on("consumer.connect", () => {
|
||||
consumerConnected = true;
|
||||
});
|
||||
consumer.on("consumer.disconnect", () => {
|
||||
consumerConnected = false;
|
||||
});
|
||||
|
||||
export function isKafkaConnected(): boolean {
|
||||
return producerConnected && consumerConnected;
|
||||
}
|
||||
|
||||
export async function connectKafka(): Promise<void> {
|
||||
try {
|
||||
await producer.connect();
|
||||
await consumer.connect();
|
||||
console.log("Kafka connected");
|
||||
logger.info("Kafka connected");
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"Kafka connect failed, running without Kafka:",
|
||||
err instanceof Error ? err.message : String(err),
|
||||
logger.warn(
|
||||
{ err: err instanceof Error ? err.message : String(err) },
|
||||
"Kafka connect failed, running without Kafka",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
97
services/core-edu/src/config/redis.ts
Normal file
97
services/core-edu/src/config/redis.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { createClient, type RedisClientType } from "redis";
|
||||
import { env } from "./env.js";
|
||||
import { logger } from "../shared/observability/logger.js";
|
||||
|
||||
let client: RedisClientType | null = null;
|
||||
|
||||
/**
|
||||
* 获取 Redis 客户端(单例)。
|
||||
* REDIS_URL 未配置时返回 null,调用方需做 null 检查。
|
||||
*/
|
||||
export function getRedisClient(): RedisClientType | null {
|
||||
if (!env.REDIS_URL) return null;
|
||||
if (!client) {
|
||||
client = createClient({ url: env.REDIS_URL });
|
||||
client.on("error", (err) => {
|
||||
logger.error({ err: err.message }, "Redis client error");
|
||||
});
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接 Redis(非阻塞:连接失败不影响服务启动)。
|
||||
*/
|
||||
export async function connectRedis(): Promise<void> {
|
||||
const c = getRedisClient();
|
||||
if (!c) {
|
||||
logger.warn("REDIS_URL not set, Redis disabled");
|
||||
return;
|
||||
}
|
||||
if (c.isOpen) return;
|
||||
try {
|
||||
await c.connect();
|
||||
logger.info("Redis connected");
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
{ err: err instanceof Error ? err.message : String(err) },
|
||||
"Redis connect failed, running without Redis",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭 Redis 连接。
|
||||
*/
|
||||
export async function disconnectRedis(): Promise<void> {
|
||||
if (client && client.isOpen) {
|
||||
await client.quit();
|
||||
logger.info("Redis disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis 健康检查(用于 /readyz)。
|
||||
*/
|
||||
export async function isRedisHealthy(): Promise<boolean> {
|
||||
if (!client || !client.isOpen) return false;
|
||||
try {
|
||||
const pong = await client.ping();
|
||||
return pong === "PONG";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分布式锁(用于作业提交幂等性保护)。
|
||||
*
|
||||
* 使用 SET NX EX 实现:
|
||||
* - 成功获取锁返回 true
|
||||
* - 锁已被持有返回 false
|
||||
*
|
||||
* 锁自动过期(TTL),避免死锁。
|
||||
*/
|
||||
export async function acquireLock(
|
||||
key: string,
|
||||
ttlSeconds: number,
|
||||
): Promise<boolean> {
|
||||
const c = getRedisClient();
|
||||
if (!c || !c.isOpen) return false;
|
||||
try {
|
||||
const result = await c.set(key, "1", { NX: true, EX: ttlSeconds });
|
||||
return result === "OK";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function releaseLock(key: string): Promise<void> {
|
||||
const c = getRedisClient();
|
||||
if (!c || !c.isOpen) return;
|
||||
try {
|
||||
await c.del(key);
|
||||
} catch {
|
||||
// 释放失败不影响业务(锁会自动过期)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user