flnxi-Wu/license-server/lib/db-manager.ts
2026-05-25 14:31:58 +08:00

206 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { PrismaClient } from '.prisma/main-client';
import type { Project } from '.prisma/main-client';
import * as crypto from 'crypto';
// 主库客户端(单例)
let mainClient: PrismaClient | null = null;
// 项目库客户端缓存(LRU,最多缓存 10 个连接)
const projectClients = new Map<string, { client: PrismaClient; lastUsed: number }>();
const MAX_CACHED_CLIENTS = 10;
const CLIENT_EXPIRY_MS = 30 * 60 * 1000; // 30 分钟未使用则释放
// 加密密钥(从环境变量获取)
const getEncryptionKey = () => {
const key = process.env.MAIN_DB_ENCRYPTION_KEY;
if (!key || key.length < 32) {
throw new Error('MAIN_DB_ENCRYPTION_KEY 环境变量未设置或长度不足 32 字节');
}
return key.substring(0, 32);
};
/**
* 加密数据库密码
*/
export function encryptPassword(password: string): string {
const key = getEncryptionKey();
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
let encrypted = cipher.update(password, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
/**
* 解密数据库密码
*/
export function decryptPassword(encryptedPassword: string): string {
const key = getEncryptionKey();
const [ivHex, encrypted] = encryptedPassword.split(':');
if (!ivHex || !encrypted) {
throw new Error('密码格式无效');
}
const iv = Buffer.from(ivHex, 'hex');
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
/**
* 获取主库客户端
*/
export function getMainClient(): PrismaClient {
if (!mainClient) {
mainClient = new PrismaClient();
}
return mainClient;
}
/**
* 构建项目数据库连接 URL
*/
function buildProjectDbUrl(project: Project): string {
const password = decryptPassword(project.dbPassword);
return `postgresql://${project.dbUser}:${encodeURIComponent(password)}@${project.dbHost}:${project.dbPort}/${project.dbName}?schema=${project.dbSchema}`;
}
/**
* 获取项目库客户端(动态创建,带缓存)
*/
export async function getProjectClient(projectCode: string): Promise<PrismaClient> {
// 清理过期连接
cleanupExpiredClients();
// 检查缓存
const cached = projectClients.get(projectCode);
if (cached) {
cached.lastUsed = Date.now();
return cached.client;
}
// 从主库获取项目配置
const mainPrisma = getMainClient();
const project = await mainPrisma.project.findUnique({
where: { code: projectCode },
});
if (!project) {
throw new Error(`项目不存在: ${projectCode}`);
}
if (!project.isActive) {
throw new Error(`项目已禁用: ${projectCode}`);
}
// 创建新的项目库客户端
const dbUrl = buildProjectDbUrl(project);
const client = new PrismaClient({
datasourceUrl: dbUrl,
});
// 缓存客户端
if (projectClients.size >= MAX_CACHED_CLIENTS) {
// 移除最久未使用的客户端
let oldestKey: string | null = null;
let oldestTime = Infinity;
projectClients.forEach((value, key) => {
if (value.lastUsed < oldestTime) {
oldestTime = value.lastUsed;
oldestKey = key;
}
});
if (oldestKey) {
const oldClient = projectClients.get(oldestKey);
if (oldClient) {
await oldClient.client.$disconnect();
}
projectClients.delete(oldestKey);
}
}
projectClients.set(projectCode, { client, lastUsed: Date.now() });
return client;
}
/**
* 根据项目配置直接创建客户端(用于测试连接)
*/
export async function createProjectClient(project: Project): Promise<PrismaClient> {
const dbUrl = buildProjectDbUrl(project);
const client = new PrismaClient({
datasourceUrl: dbUrl,
});
return client;
}
/**
* 测试项目数据库连接
*/
export async function testProjectConnection(project: Project): Promise<{ success: boolean; error?: string }> {
let client: PrismaClient | null = null;
try {
const dbUrl = buildProjectDbUrl(project);
client = new PrismaClient({
datasourceUrl: dbUrl,
});
await client.$connect();
await client.$queryRaw`SELECT 1`;
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '连接失败',
};
} finally {
if (client) {
await client.$disconnect();
}
}
}
/**
* 清理过期的客户端连接
*/
function cleanupExpiredClients(): void {
const now = Date.now();
projectClients.forEach((value, key) => {
if (now - value.lastUsed > CLIENT_EXPIRY_MS) {
value.client.$disconnect().catch(console.error);
projectClients.delete(key);
}
});
}
/**
* 断开所有连接(应用关闭时调用)
*/
export async function disconnectAll(): Promise<void> {
if (mainClient) {
await mainClient.$disconnect();
mainClient = null;
}
for (const value of Array.from(projectClients.values())) {
await value.client.$disconnect();
}
projectClients.clear();
}
// 导出类型
export type { Project } from '.prisma/main-client';