feat: 支持 Redis

This commit is contained in:
imsyy
2024-12-06 18:15:44 +08:00
parent 161b6b612b
commit 51f27af0d6
16 changed files with 296 additions and 120 deletions

View File

@@ -12,6 +12,9 @@ export type Config = {
ALLOWED_HOST: string;
USE_LOG_FILE: boolean;
RSS_MODE: boolean;
REDIS_HOST: string;
REDIS_PORT: number;
REDIS_PASSWORD: string;
};
// 验证并提取环境变量
@@ -45,4 +48,7 @@ export const config: Config = {
ALLOWED_HOST: getEnvVariable("ALLOWED_HOST") || "imsyy.top",
USE_LOG_FILE: getBooleanEnvVariable("USE_LOG_FILE", true),
RSS_MODE: getBooleanEnvVariable("RSS_MODE", false),
REDIS_HOST: getEnvVariable("REDIS_HOST") || "127.0.0.1",
REDIS_PORT: getNumericEnvVariable("REDIS_PORT", 6379),
REDIS_PASSWORD: getEnvVariable("REDIS_PASSWORD") || "",
};

View File

@@ -48,7 +48,7 @@ const findTsFiles = (dirPath: string, allFiles: string[] = [], basePath: string
if (fs.existsSync(routersDirPath) && fs.statSync(routersDirPath).isDirectory()) {
allRoutePath = findTsFiles(routersDirPath);
} else {
console.error(`目录 ${routersDirPath} 不存在或不是目录`);
console.error(`📂 The directory ${routersDirPath} does not exist or is not a directory`);
}
// 注册全部路由
@@ -82,30 +82,13 @@ for (let index = 0; index < allRoutePath.length; index++) {
c.header("Content-Type", "application/xml; charset=utf-8");
return c.body(rss);
} else {
return c.json(
{
code: 500,
message: "RSS 生成失败",
},
500,
);
return c.json({ code: 500, message: "RSS generation failed" }, 500);
}
}
return c.json({
code: 200,
...listData,
});
return c.json({ code: 200, ...listData });
});
// 请求方式错误
listApp.all("*", (c) =>
c.json(
{
code: 405,
message: "Method Not Allowed",
},
405,
),
);
listApp.all("*", (c) => c.json({ code: 405, message: "Method Not Allowed" }, 405));
}
// 获取全部路由
@@ -120,13 +103,10 @@ app.get("/all", (c) =>
return {
name: path,
path: undefined,
message: "该接口暂时下线",
message: "This interface is temporarily offline",
};
}
return {
name: path,
path: `/${path}`,
};
return { name: path, path: `/${path}` };
}),
},
200,

View File

@@ -43,6 +43,7 @@ const getList = async (options: Options, noCache: boolean): Promise<RouterResTyp
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36",
},
});
// 转码
const utf8Data = iconv.decode(result.data, "gbk");
const list = await parseRSS(utf8Data);

View File

@@ -21,7 +21,7 @@ const getList = async (noCache: boolean) => {
const result = await get({
url,
noCache,
headers: await genHeaders(),
headers: genHeaders(),
});
const list = result.data.data;
return {

View File

@@ -3,28 +3,31 @@ import type { RouterType } from "../router.types.js";
import { parseChineseNumber } from "../utils/getNum.js";
import { get } from "../utils/getData.js";
const typeMap: Record<string, string> = {
all: "新浪热榜",
hotcmnt: "热议榜",
minivideo: "视频热榜",
ent: "娱乐热榜",
ai: "AI热榜",
auto: "汽车热榜",
mother: "育儿热榜",
fashion: "时尚热榜",
travel: "旅游热榜",
esg: "ESG热榜",
};
export const handleRoute = async (c: ListContext, noCache: boolean) => {
const type = c.req.query("type") || "1";
const type = c.req.query("type") || "all";
const listData = await getList({ type }, noCache);
const routeData: RouterData = {
name: "sina",
title: "新浪网",
type: "热榜太多,一个就够",
type: typeMap[type],
description: "热榜太多,一个就够",
params: {
type: {
name: "榜单分类",
type: {
all: "新浪热榜",
hotcmnt: "热议榜",
minivideo: "视频热榜",
ent: "娱乐热榜",
ai: "AI热榜",
auto: "汽车热榜",
mother: "育儿热榜",
fashion: "时尚热榜",
travel: "旅游热榜",
esg: "ESG热榜",
},
type: typeMap,
},
},
link: "https://sinanews.sina.cn/",

View File

@@ -1,8 +1,14 @@
import { config } from "../config.js";
import NodeCache from "node-cache";
import logger from "./logger.js";
import NodeCache from "node-cache";
import Redis from "ioredis";
// init
interface CacheData {
updateTime: string;
data: unknown;
}
// init NodeCache
const cache = new NodeCache({
// 缓存过期时间( 秒
stdTTL: config.CACHE_TTL,
@@ -14,24 +20,119 @@ const cache = new NodeCache({
maxKeys: 100,
});
interface GetCache<T> {
updateTime: string;
data: T;
}
// init Redis client
const redis = new Redis({
host: config.REDIS_HOST,
port: config.REDIS_PORT,
password: config.REDIS_PASSWORD,
// 仅在第一次建立连接
lazyConnect: true,
});
// 从缓存中获取数据
export const getCache = <T>(key: string): GetCache<T> | undefined => {
// Redis 是否可用
let isRedisAvailable: boolean = false;
let isRedisTried: boolean = false;
// Redis 连接错误
redis.on("error", (err) => {
if (!isRedisTried) {
isRedisAvailable = false;
isRedisTried = true;
logger.error(
`📦 [Redis] connection failed: ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
});
// Redis 连接状态
const ensureRedisConnection = async () => {
if (!isRedisTried) {
try {
await redis.connect();
isRedisAvailable = true;
isRedisTried = true;
logger.info("📦 [Redis] connected successfully.");
} catch (error) {
isRedisAvailable = false;
isRedisTried = true;
logger.error(
`📦 [Redis] connection failed: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}
};
/**
* 从缓存中获取数据
* @param key 缓存键
* @returns 缓存数据
*/
export const getCache = async (key: string): Promise<CacheData | undefined> => {
await ensureRedisConnection();
if (isRedisAvailable) {
try {
const redisResult = await redis.get(key);
if (redisResult) {
const data = JSON.parse(redisResult);
return data;
}
} catch (error) {
logger.error(
`📦 [Redis] get error: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}
return cache.get(key);
};
// 将数据写入缓存
export const setCache = <T>(key: string, value: T, ttl: number = config.CACHE_TTL) => {
/**
* 将数据写入缓存
* @param key 缓存键
* @param value 缓存值
* @param ttl 缓存过期时间( 秒
* @returns 是否写入成功
*/
export const setCache = async (
key: string,
value: CacheData,
ttl: number = config.CACHE_TTL,
): Promise<boolean> => {
// 尝试写入 Redis
if (isRedisAvailable && !Buffer.isBuffer(value?.data)) {
try {
await redis.set(key, JSON.stringify(value), "EX", ttl);
if (logger) logger.info(`💾 [REDIS] ${key} has been cached`);
} catch (error) {
logger.error(
`📦 [Redis] set error: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}
const success = cache.set(key, value, ttl);
if (logger) logger.info(`💾 [CHCHE] ${key} has been cached`);
if (logger) logger.info(`💾 [NodeCache] ${key} has been cached`);
return success;
};
// 从缓存中删除数据
export const delCache = (key: string) => {
return cache.del(key);
/**
* 从缓存中删除数据
* @param key 缓存键
* @returns 是否删除成功
*/
export const delCache = async (key: string): Promise<boolean> => {
let redisSuccess = true;
if (isRedisAvailable) {
try {
await redis.del(key);
if (logger) logger.info(`🗑️ [REDIS] ${key} has been deleted from Redis`);
} catch (error) {
logger.error(
`📦 [Redis] del error: ${error instanceof Error ? error.message : "Unknown error"}`,
);
redisSuccess = false;
}
}
// 尝试删除 NodeCache
const nodeCacheSuccess = cache.del(key) > 0;
if (logger) logger.info(`🗑️ [CACHE] ${key} has been deleted from NodeCache`);
return redisSuccess && nodeCacheSuccess;
};

View File

@@ -49,9 +49,9 @@ export const get = async (options: Get) => {
logger.info(`🌐 [GET] ${url}`);
try {
// 检查缓存
if (noCache) delCache(url);
if (noCache) await delCache(url);
else {
const cachedData = getCache(url);
const cachedData = await getCache(url);
if (cachedData) {
logger.info("💾 [CHCHE] The request is cached");
return { fromCache: true, data: cachedData.data, updateTime: cachedData.updateTime };
@@ -63,9 +63,9 @@ export const get = async (options: Get) => {
// 存储新获取的数据到缓存
const updateTime = new Date().toISOString();
const data = originaInfo ? response : responseData;
setCache(url, { data, updateTime }, ttl);
await setCache(url, { data, updateTime }, ttl);
// 返回数据
logger.info(`✅ [${response?.statusText}] request was successful`);
logger.info(`✅ [${response?.status}] request was successful`);
return { fromCache: false, data, updateTime };
} catch (error) {
logger.error("❌ [ERROR] request failed");
@@ -79,9 +79,9 @@ export const post = async (options: Post) => {
logger.info(`🌐 [POST] ${url}`);
try {
// 检查缓存
if (noCache) delCache(url);
if (noCache) await delCache(url);
else {
const cachedData = getCache(url);
const cachedData = await getCache(url);
if (cachedData) {
logger.info("💾 [CHCHE] The request is cached");
return { fromCache: true, data: cachedData.data, updateTime: cachedData.updateTime };
@@ -94,10 +94,10 @@ export const post = async (options: Post) => {
const updateTime = new Date().toISOString();
const data = originaInfo ? response : responseData;
if (!noCache) {
setCache(url, { data, updateTime }, ttl);
await setCache(url, { data, updateTime }, ttl);
}
// 返回数据
logger.info(`✅ [${response?.statusText}] request was successful`);
logger.info(`✅ [${response?.status}] request was successful`);
return { fromCache: false, data, updateTime };
} catch (error) {
logger.error("❌ [ERROR] request failed");

View File

@@ -3,7 +3,7 @@ import { get } from "../getData.js";
import md5 from "md5";
export const getToken = async () => {
const cachedData = getCache("51cto-token");
const cachedData = await getCache("51cto-token");
if (cachedData && typeof cachedData === "object" && "token" in cachedData) {
const { token } = cachedData as { token: string };
return token;
@@ -12,7 +12,7 @@ export const getToken = async () => {
url: "https://api-media.51cto.com/api/token-get",
});
const token = result.data.data.data.token;
setCache("51cto-token", { token });
await setCache("51cto-token", { token });
return token;
};

View File

@@ -67,7 +67,7 @@ const getWbiKeys = async (): Promise<EncodedKeys> => {
};
const getBiliWbi = async (): Promise<string> => {
const cachedData = getCache("bilibili-wbi");
const cachedData = await getCache("bilibili-wbi");
console.log(cachedData);
if (cachedData && typeof cachedData === "object" && "wbi" in cachedData) {
const { wbi } = cachedData as { wbi: string };
@@ -78,7 +78,7 @@ const getBiliWbi = async (): Promise<string> => {
const img_key = web_keys.img_key;
const sub_key = web_keys.sub_key;
const query = encWbi(params, img_key, sub_key);
setCache("bilibili-wbi", { wbi: query });
await setCache("bilibili-wbi", { wbi: query });
return query;
};

View File

@@ -13,18 +13,18 @@ const getRandomDEVICE_ID = () => {
* 获取APP_TOKEN
* @returns APP_TOKEN
*/
const get_app_token = async () => {
const get_app_token = () => {
const DEVICE_ID = getRandomDEVICE_ID();
const now = Math.round(Date.now() / 1000);
const hex_now = "0x" + now.toString(16);
const md5_now = await md5(now.toString());
const md5_now = md5(now.toString());
const s =
"token://com.coolapk.market/c67ef5943784d09750dcfbb31020f0ab?" +
md5_now +
"$" +
DEVICE_ID +
"&com.coolapk.market";
const md5_s = await md5(Buffer.from(s).toString("base64"));
const md5_s = md5(Buffer.from(s).toString("base64"));
const token = md5_s + DEVICE_ID + hex_now;
return token;
};
@@ -33,11 +33,11 @@ const get_app_token = async () => {
* 获取请求头
* @returns 请求头
*/
export const genHeaders = async () => {
export const genHeaders = () => {
return {
"X-Requested-With": "XMLHttpRequest",
"X-App-Id": "com.coolapk.market",
"X-App-Token": await get_app_token(),
"X-App-Token": get_app_token(),
"X-Sdk-Int": "29",
"X-Sdk-Locale": "zh-CN",
"X-App-Version": "11.0",

View File

@@ -47,7 +47,7 @@ export const parseRSS = async (rssContent: string) => {
// 返回解析数据
return items;
} catch (error) {
logger.error("❌ [ERROR] An error occurred while parsing RSS content");
logger.error("❌ [RSS] An error occurred while parsing RSS content");
throw error;
}
};

View File

@@ -72,6 +72,7 @@ const Layout: FC = (props) => {
font-size: 28px;
font-weight: bold;
margin-bottom: 12px;
text-align: center;
}
.title .title-tip {
font-size: 20px;
@@ -83,6 +84,7 @@ const Layout: FC = (props) => {
padding: 20px;
border-radius: 12px;
border: 1px dashed var(--text-color);
user-select: text;
}
.control {
display: flex;