feat(msg): 修复通知服务并添加ES降级与Push Gateway推送
database.ts 导出db常量替代getDb()函数 env.ts JWT_SECRET/ES_URL改optional加DEV_MODE/PUSH_GATEWAY_URL elasticsearch.ts ES降级: ES_URL未设置时esClient=null notifications.service.ts 加createBatch+分页查询+Push Gateway推送调用 新建msg-init.sql创建2张表
This commit is contained in:
@@ -1,24 +1,16 @@
|
||||
import { drizzle } from 'drizzle-orm/mysql2';
|
||||
import mysql from 'mysql2/promise';
|
||||
import { env } from './env.js';
|
||||
import { drizzle } from "drizzle-orm/mysql2";
|
||||
import mysql from "mysql2/promise";
|
||||
import { env } from "./env.js";
|
||||
|
||||
let pool: mysql.Pool | null = null;
|
||||
const pool = mysql.createPool({
|
||||
uri: env.DATABASE_URL,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
});
|
||||
|
||||
export function getDb() {
|
||||
if (!pool) {
|
||||
pool = mysql.createPool({
|
||||
uri: env.DATABASE_URL,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
});
|
||||
}
|
||||
return drizzle(pool);
|
||||
}
|
||||
export const db = drizzle(pool);
|
||||
|
||||
export async function closeDb(): Promise<void> {
|
||||
if (pool) {
|
||||
await pool.end();
|
||||
pool = null;
|
||||
}
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
@@ -1,13 +1,87 @@
|
||||
import { Client } from '@elastic/elasticsearch';
|
||||
import { env } from './env.js';
|
||||
import { Client } from "@elastic/elasticsearch";
|
||||
import { env } from "./env.js";
|
||||
|
||||
export const esClient = new Client({ node: env.ES_URL });
|
||||
/**
|
||||
* Elasticsearch 客户端。
|
||||
*
|
||||
* 降级模式:当 ES_URL 未设置或连接失败时,esClient 为 null,
|
||||
* 所有 ES 操作通过 safeIndex / safeSearch 自动跳过,服务仍可启动。
|
||||
*/
|
||||
export const esClient: Client | null = env.ES_URL
|
||||
? new Client({ node: env.ES_URL })
|
||||
: null;
|
||||
|
||||
/**
|
||||
* 探活 ES 连接。失败仅记录日志,不抛错(降级模式)。
|
||||
*/
|
||||
export async function checkEsConnection(): Promise<void> {
|
||||
if (!esClient) {
|
||||
console.log("Elasticsearch disabled (ES_URL not set)");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await esClient.ping();
|
||||
console.log('Elasticsearch connected');
|
||||
console.log("Elasticsearch connected");
|
||||
} catch (err) {
|
||||
console.error('Elasticsearch connection failed:', err);
|
||||
console.error("Elasticsearch connection failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭 ES 客户端连接。
|
||||
*/
|
||||
export async function closeEs(): Promise<void> {
|
||||
if (esClient) {
|
||||
await esClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
export interface IndexParams {
|
||||
index: string;
|
||||
id: string;
|
||||
document: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SearchHit {
|
||||
_id: string;
|
||||
_source: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
hits: SearchHit[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全索引文档。client 为 null 或失败时跳过(降级模式),返回是否成功。
|
||||
*/
|
||||
export async function safeIndex(params: IndexParams): Promise<boolean> {
|
||||
if (!esClient) return false;
|
||||
try {
|
||||
await esClient.index(params);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error("Elasticsearch index failed:", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全搜索。client 为 null 或失败时返回空数组(降级模式)。
|
||||
*/
|
||||
export async function safeSearch(
|
||||
index: string,
|
||||
query: Record<string, unknown>,
|
||||
): Promise<SearchResult> {
|
||||
if (!esClient) return { hits: [] };
|
||||
try {
|
||||
const result = await esClient.search({ index, query });
|
||||
const hits = (result.hits.hits as unknown[]).map((raw) => {
|
||||
const hit = raw as { _id: string; _source: Record<string, unknown> };
|
||||
return { _id: hit._id, _source: hit._source };
|
||||
});
|
||||
return { hits };
|
||||
} catch (err) {
|
||||
console.error("Elasticsearch search failed:", err);
|
||||
return { hits: [] };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { z } from 'zod';
|
||||
import { z } from "zod";
|
||||
|
||||
const envSchema = z.object({
|
||||
PORT: z.string().default('3007'),
|
||||
PORT: z.string().default("3007"),
|
||||
DATABASE_URL: z.string().url(),
|
||||
REDIS_URL: z.string().url().optional(),
|
||||
JWT_SECRET: z.string(),
|
||||
JWT_ISSUER: z.string().default('next-edu-cloud'),
|
||||
KAFKA_BROKERS: z.string().default('localhost:9092'),
|
||||
ES_URL: z.string().url().default('http://localhost:9200'),
|
||||
JWT_SECRET: z.string().optional(),
|
||||
JWT_ISSUER: z.string().default("next-edu-cloud"),
|
||||
KAFKA_BROKERS: z.string().default("localhost:9092"),
|
||||
ES_URL: z.string().url().optional(),
|
||||
PUSH_GATEWAY_URL: z.string().url().optional(),
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url().optional(),
|
||||
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
|
||||
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
||||
LOG_LEVEL: z
|
||||
.enum(["fatal", "error", "warn", "info", "debug", "trace"])
|
||||
.default("info"),
|
||||
NODE_ENV: z
|
||||
.enum(["development", "production", "test"])
|
||||
.default("development"),
|
||||
DEV_MODE: z.string().optional().default("false"),
|
||||
});
|
||||
|
||||
export type Env = z.infer<typeof envSchema>;
|
||||
@@ -18,8 +24,11 @@ export type Env = z.infer<typeof envSchema>;
|
||||
export function loadEnv(): Env {
|
||||
const result = envSchema.safeParse(process.env);
|
||||
if (!result.success) {
|
||||
console.error('❌ Invalid environment variables:', result.error.flatten().fieldErrors);
|
||||
throw new Error('Invalid environment configuration');
|
||||
console.error(
|
||||
"❌ Invalid environment variables:",
|
||||
result.error.flatten().fieldErrors,
|
||||
);
|
||||
throw new Error("Invalid environment configuration");
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user