v0.2.7: 修复Redis连接 + 启动管理后台

- 修复Redis认证 (配置密码)
- 启动Python管理后台 (端口9531, 15个功能开关)
- 统一版本号 0.2.7
- 更新docker-compose.yml (镜像版本/Redis URL/Admin服务)
This commit is contained in:
2026-05-17 02:22:18 +08:00
commit 83cbfaf03f
164 changed files with 25195 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
import Redis from 'ioredis';
let client: Redis | null = null;
let currentUrl: string = '';
export function getRedis(): Redis | null {
return client;
}
export function getRedisClient(): Redis {
if (client) return client;
return createClient();
}
function createClient(url?: string): Redis {
const redisUrl = url || process.env.REDIS_URL || currentUrl || getSystemConfigRedisUrl();
currentUrl = redisUrl;
if (client) {
try { client.disconnect(); } catch {}
client = null;
}
client = new Redis(redisUrl, {
maxRetriesPerRequest: 3,
retryStrategy(times: number) {
if (times > 3) return null;
return Math.min(times * 200, 2000);
},
lazyConnect: true,
});
client.on('error', (err: Error) => {
console.error('[Redis] Error:', err.message);
});
client.on('connect', () => {
console.log('[Redis] Connected to', currentUrl);
});
return client;
}
function getSystemConfigRedisUrl(): string {
try {
const { getSystemConfig } = require('./admin/system-config.service');
return getSystemConfig('redis_url') || process.env.REDIS_URL || 'redis://redis:6379';
} catch {
return 'redis://redis:6379';
}
}
export async function connectRedis(): Promise<void> {
const redis = createClient();
try {
await redis.connect();
} catch (err) {
console.warn('[Redis] Connection failed, running without cache');
}
}
export async function reconnectRedis(url: string): Promise<boolean> {
try {
if (client) {
await client.quit().catch(() => {});
client = null;
}
const redis = createClient(url);
await redis.connect();
console.log('[Redis] Reconnected with new URL:', url);
return true;
} catch (err) {
console.error('[Redis] Reconnect failed:', err);
return false;
}
}
export async function disconnectRedis(): Promise<void> {
if (client) {
await client.quit();
client = null;
currentUrl = '';
console.log('[Redis] Disconnected');
}
}
/**
* Test a Redis URL without affecting the running client.
* @returns { ok: boolean, latency: number, info?: string }
*/
export async function testRedisConnection(url: string): Promise<{ ok: boolean; latency: number; info?: string }> {
const start = Date.now();
const testClient = new Redis(url, {
maxRetriesPerRequest: 1,
retryStrategy() { return null; },
lazyConnect: true,
connectTimeout: 5000,
});
try {
await testClient.connect();
const pong = await testClient.ping();
const latency = Date.now() - start;
await testClient.quit();
return { ok: pong === 'PONG', latency, info: `响应时间 ${latency}ms` };
} catch (err: any) {
try { await testClient.disconnect(); } catch {}
const latency = Date.now() - start;
return { ok: false, latency, info: err.message || '连接失败' };
}
}
export class RedisClient {
private redis: Redis;
constructor() {
this.redis = getRedisClient();
}
async get(key: string): Promise<string | null> {
try {
return await this.redis.get(key);
} catch {
return null;
}
}
async set(key: string, value: string): Promise<void> {
try {
await this.redis.set(key, value);
} catch {
// silently fail
}
}
async setEx(key: string, ttl: number, value: string): Promise<void> {
try {
await this.redis.setex(key, ttl, value);
} catch {
// silently fail
}
}
async del(key: string): Promise<void> {
try {
await this.redis.del(key);
} catch {
// silently fail
}
}
async exists(key: string): Promise<boolean> {
try {
const result = await this.redis.exists(key);
return result === 1;
} catch {
return false;
}
}
}
export default RedisClient;

View File

@@ -0,0 +1,53 @@
import rateLimit from 'express-rate-limit';
/** 公开搜索接口:较宽松 */
export const searchLimiter = rateLimit({
windowMs: 60 * 1000,
max: 150,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.socket.remoteAddress ?? 'unknown',
message: { error: '搜索请求过于频繁,请稍后再试', code: 429 },
});
/** 管理接口admin/*):较严格 */
export const adminLimiter = rateLimit({
windowMs: 60 * 1000,
max: 30,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.socket.remoteAddress ?? 'unknown',
message: { error: '操作过于频繁,请稍后再试', code: 429 },
});
/** 登录接口:极严格,防暴力破解 */
export const loginLimiter = rateLimit({
windowMs: 60 * 1000,
max: 5,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.socket.remoteAddress ?? 'unknown',
message: { error: '登录尝试次数过多,请一分钟后重试', code: 429 },
});
/** 转存/保存接口:中等等级 */
export const saveLimiter = rateLimit({
windowMs: 60 * 1000,
max: 30,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.socket.remoteAddress ?? 'unknown',
message: { error: '转存操作过于频繁,请稍后再试', code: 429 },
});
/** 默认全局限流(兜底,未匹配上述规则的路由) */
const defaultLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.socket.remoteAddress ?? 'unknown',
message: { error: 'Too many requests, please try again later.', code: 429 },
});
export default defaultLimiter;