mirror of
https://github.com/jiangrui1994/CloudSaver.git
synced 2026-01-10 15:18:46 +08:00
Refactoring the backend
This commit is contained in:
@@ -15,12 +15,14 @@
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.18.3",
|
||||
"inversify": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"rss-parser": "^3.13.0",
|
||||
"sequelize": "^6.37.5",
|
||||
"socket.io": "^4.8.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"tunnel": "^0.0.6"
|
||||
"tunnel": "^0.0.6",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
|
||||
@@ -1,136 +1,46 @@
|
||||
// filepath: /d:/code/CloudDiskDown/backend/src/app.ts
|
||||
import "./types/express";
|
||||
import express, { Application } from "express";
|
||||
import cors from "cors";
|
||||
import cookieParser from "cookie-parser";
|
||||
import { QueryTypes } from "sequelize";
|
||||
|
||||
// 路由和中间件导入
|
||||
import express from "express";
|
||||
import { container } from "./core/container";
|
||||
import { TYPES } from "./core/types";
|
||||
import { DatabaseService } from "./services/DatabaseService";
|
||||
import { setupMiddlewares } from "./middleware";
|
||||
import routes from "./routes/api";
|
||||
import { errorHandler } from "./middleware/errorHandler";
|
||||
import { authMiddleware } from "./middleware/auth";
|
||||
|
||||
// 数据库和服务相关
|
||||
import sequelize from "./config/database";
|
||||
import GlobalSetting from "./models/GlobalSetting";
|
||||
import Searcher from "./services/Searcher";
|
||||
|
||||
// 常量配置
|
||||
const PUBLIC_ROUTES = ["/user/login", "/user/register"];
|
||||
const IMAGE_PATH = "tele-images";
|
||||
const DEFAULT_PORT = 8009;
|
||||
|
||||
// 全局设置默认值
|
||||
const DEFAULT_GLOBAL_SETTINGS = {
|
||||
httpProxyHost: "127.0.0.1",
|
||||
httpProxyPort: 7890,
|
||||
isProxyEnabled: false,
|
||||
CommonUserCode: 9527,
|
||||
AdminUserCode: 230713,
|
||||
};
|
||||
import { logger } from "./utils/logger";
|
||||
|
||||
class App {
|
||||
private app: Application;
|
||||
private app = express();
|
||||
private databaseService = container.get<DatabaseService>(TYPES.DatabaseService);
|
||||
|
||||
constructor() {
|
||||
this.app = express();
|
||||
this.setupMiddlewares();
|
||||
this.setupRoutes();
|
||||
this.setupErrorHandling();
|
||||
this.setupExpress();
|
||||
}
|
||||
|
||||
private setupMiddlewares(): void {
|
||||
// CORS 配置
|
||||
this.app.use(
|
||||
cors({
|
||||
origin: "*",
|
||||
credentials: true,
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allowedHeaders: ["Content-Type", "Authorization", "Cookie"],
|
||||
})
|
||||
);
|
||||
private setupExpress(): void {
|
||||
// 设置中间件
|
||||
setupMiddlewares(this.app);
|
||||
|
||||
this.app.use(cookieParser());
|
||||
this.app.use(express.json());
|
||||
|
||||
// 身份验证中间件
|
||||
this.app.use((req, res, next) => {
|
||||
if (PUBLIC_ROUTES.includes(req.path) || req.path.includes(IMAGE_PATH)) {
|
||||
return next();
|
||||
}
|
||||
authMiddleware(req, res, next);
|
||||
});
|
||||
}
|
||||
|
||||
private setupRoutes(): void {
|
||||
// 设置路由
|
||||
this.app.use("/", routes);
|
||||
}
|
||||
|
||||
private setupErrorHandling(): void {
|
||||
this.app.use(errorHandler);
|
||||
}
|
||||
|
||||
private async initializeGlobalSettings(): Promise<void> {
|
||||
try {
|
||||
const settings = await GlobalSetting.findOne();
|
||||
if (!settings) {
|
||||
await GlobalSetting.create(DEFAULT_GLOBAL_SETTINGS);
|
||||
console.log("✅ Global settings initialized with default values.");
|
||||
}
|
||||
await Searcher.updateAxiosInstance();
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to initialize global settings:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanupBackupTables(): Promise<void> {
|
||||
try {
|
||||
// 查询所有以 '_backup' 结尾的备份表
|
||||
const backupTables = await sequelize.query<{ name: string }>(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%\\_backup%' ESCAPE '\\'",
|
||||
{ type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
// 逐个删除备份表
|
||||
for (const table of backupTables) {
|
||||
if (table?.name) {
|
||||
await sequelize.query(`DROP TABLE IF EXISTS ${table.name}`);
|
||||
console.log(`✅ Cleaned up backup table: ${table.name}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to cleanup backup tables:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
try {
|
||||
// 数据库初始化流程
|
||||
await sequelize.query("PRAGMA foreign_keys = OFF");
|
||||
console.log("📝 Foreign keys disabled for initialization...");
|
||||
|
||||
await this.cleanupBackupTables();
|
||||
console.log("🧹 Backup tables cleaned up");
|
||||
|
||||
await sequelize.sync({ alter: true });
|
||||
console.log("📚 Database schema synchronized");
|
||||
|
||||
await sequelize.query("PRAGMA foreign_keys = ON");
|
||||
console.log("🔐 Foreign keys re-enabled");
|
||||
// 初始化数据库
|
||||
await this.databaseService.initialize();
|
||||
logger.info("数据库初始化成功");
|
||||
|
||||
// 启动服务器
|
||||
const port = process.env.PORT || DEFAULT_PORT;
|
||||
this.app.listen(port, async () => {
|
||||
await this.initializeGlobalSettings();
|
||||
console.log(`
|
||||
🚀 Server is running on port ${port}
|
||||
🔧 Environment: ${process.env.NODE_ENV || "development"}
|
||||
const port = process.env.PORT || 8009;
|
||||
this.app.listen(port, () => {
|
||||
logger.info(`
|
||||
🚀 服务器启动成功
|
||||
🌍 监听端口: ${port}
|
||||
🔧 运行环境: ${process.env.NODE_ENV || "development"}
|
||||
`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to start server:", error);
|
||||
logger.error("服务器启动失败:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -139,7 +49,7 @@ class App {
|
||||
// 创建并启动应用
|
||||
const application = new App();
|
||||
application.start().catch((error) => {
|
||||
console.error("❌ Application failed to start:", error);
|
||||
logger.error("应用程序启动失败:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -25,6 +25,18 @@ interface Config {
|
||||
channels: Channel[];
|
||||
};
|
||||
cloudPatterns: CloudPatterns;
|
||||
app: {
|
||||
port: number;
|
||||
env: string;
|
||||
};
|
||||
database: {
|
||||
type: string;
|
||||
path: string;
|
||||
};
|
||||
jwt: {
|
||||
secret: string;
|
||||
expiresIn: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 从环境变量读取频道配置
|
||||
@@ -56,13 +68,24 @@ const getTeleChannels = (): Channel[] => {
|
||||
};
|
||||
|
||||
export const config: Config = {
|
||||
app: {
|
||||
port: parseInt(process.env.PORT || "8009"),
|
||||
env: process.env.NODE_ENV || "development",
|
||||
},
|
||||
database: {
|
||||
type: "sqlite",
|
||||
path: "./data/database.sqlite",
|
||||
},
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET || "your-secret-key",
|
||||
expiresIn: "6h",
|
||||
},
|
||||
jwtSecret: process.env.JWT_SECRET || "uV7Y$k92#LkF^q1b!",
|
||||
|
||||
telegram: {
|
||||
baseUrl: process.env.TELEGRAM_BASE_URL || "https://t.me/s",
|
||||
channels: getTeleChannels(),
|
||||
},
|
||||
|
||||
cloudPatterns: {
|
||||
baiduPan: /https?:\/\/(?:pan|yun)\.baidu\.com\/[^\s<>"]+/g,
|
||||
tianyi: /https?:\/\/cloud\.189\.cn\/[^\s<>"]+/g,
|
||||
@@ -70,6 +93,7 @@ export const config: Config = {
|
||||
// pan115有两个域名 115.com 和 anxia.com 和 115cdn.com
|
||||
pan115: /https?:\/\/(?:115|anxia|115cdn)\.com\/s\/[^\s<>"]+/g,
|
||||
// 修改为匹配所有以123开头的域名
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
pan123: /https?:\/\/(?:www\.)?123[^\/\s<>"]+\.com\/s\/[^\s<>"]+/g,
|
||||
quark: /https?:\/\/pan\.quark\.cn\/[^\s<>"]+/g,
|
||||
yidong: /https?:\/\/caiyun\.139\.com\/[^\s<>"]+/g,
|
||||
|
||||
17
backend/src/controllers/BaseController.ts
Normal file
17
backend/src/controllers/BaseController.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Request, Response } from "express";
|
||||
import { ApiResponse } from "../core/ApiResponse";
|
||||
|
||||
export abstract class BaseController {
|
||||
protected async handleRequest<T>(
|
||||
req: Request,
|
||||
res: Response,
|
||||
action: () => Promise<T>
|
||||
): Promise<void> {
|
||||
try {
|
||||
const result = await action();
|
||||
res.json(ApiResponse.success(result));
|
||||
} catch (error: any) {
|
||||
res.status(500).json(ApiResponse.error(error?.message || "未知错误"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@ import { Request, Response } from "express";
|
||||
import { Cloud115Service } from "../services/Cloud115Service";
|
||||
import { sendSuccess, sendError } from "../utils/response";
|
||||
import UserSetting from "../models/UserSetting";
|
||||
import { BaseController } from "./BaseController";
|
||||
import { injectable, inject } from "inversify";
|
||||
import { TYPES } from "../core/types";
|
||||
|
||||
const cloud115 = new Cloud115Service();
|
||||
const setCookie = async (req: Request): Promise<void> => {
|
||||
@@ -16,18 +19,19 @@ const setCookie = async (req: Request): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
export const cloud115Controller = {
|
||||
@injectable()
|
||||
export class Cloud115Controller extends BaseController {
|
||||
constructor(@inject(TYPES.Cloud115Service) private cloud115Service: Cloud115Service) {
|
||||
super();
|
||||
}
|
||||
|
||||
async getShareInfo(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await this.handleRequest(req, res, async () => {
|
||||
const { shareCode, receiveCode } = req.query;
|
||||
await setCookie(req);
|
||||
const result = await cloud115.getShareInfo(shareCode as string, receiveCode as string);
|
||||
sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
sendError(res, { message: (error as Error).message || "获取分享信息失败" });
|
||||
}
|
||||
},
|
||||
await this.cloud115Service.setCookie(req);
|
||||
return await this.cloud115Service.getShareInfo(shareCode as string, receiveCode as string);
|
||||
});
|
||||
}
|
||||
|
||||
async getFolderList(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
@@ -38,7 +42,7 @@ export const cloud115Controller = {
|
||||
} catch (error) {
|
||||
sendError(res, { message: (error as Error).message || "获取目录列表失败" });
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
async saveFile(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
@@ -54,7 +58,7 @@ export const cloud115Controller = {
|
||||
} catch (error) {
|
||||
sendError(res, { message: (error as Error).message || "保存文件失败" });
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const Cloud115ServiceInstance = cloud115;
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { Request, Response } from "express";
|
||||
import DoubanService from "../services/DoubanService";
|
||||
import { sendSuccess, sendError } from "../utils/response";
|
||||
import { injectable, inject } from "inversify";
|
||||
import { TYPES } from "../core/types";
|
||||
import { DoubanService } from "../services/DoubanService";
|
||||
import { BaseController } from "./BaseController";
|
||||
|
||||
const doubanService = new DoubanService();
|
||||
@injectable()
|
||||
export class DoubanController extends BaseController {
|
||||
constructor(@inject(TYPES.DoubanService) private doubanService: DoubanService) {
|
||||
super();
|
||||
}
|
||||
|
||||
export const doubanController = {
|
||||
async getDoubanHotList(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await this.handleRequest(req, res, async () => {
|
||||
const { type = "movie", tag = "热门", page_limit = "50", page_start = "0" } = req.query;
|
||||
const result = await doubanService.getHotList({
|
||||
const result = await this.doubanService.getHotList({
|
||||
type: type as string,
|
||||
tag: tag as string,
|
||||
page_limit: page_limit as string,
|
||||
page_start: page_start as string,
|
||||
});
|
||||
sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
sendError(res, { message: "获取热门列表失败" });
|
||||
}
|
||||
},
|
||||
};
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,42 @@
|
||||
import { Request, Response } from "express";
|
||||
import { injectable, inject } from "inversify";
|
||||
import { TYPES } from "../core/types";
|
||||
import { QuarkService } from "../services/QuarkService";
|
||||
import { sendSuccess, sendError } from "../utils/response";
|
||||
import UserSetting from "../models/UserSetting";
|
||||
import { BaseController } from "./BaseController";
|
||||
import { sendSuccess } from "../utils/response";
|
||||
|
||||
const quark = new QuarkService();
|
||||
|
||||
const setCookie = async (req: Request): Promise<void> => {
|
||||
const userId = req.user?.userId;
|
||||
const userSetting = await UserSetting.findOne({
|
||||
where: { userId },
|
||||
});
|
||||
if (userSetting && userSetting.dataValues.quarkCookie) {
|
||||
quark.setCookie(userSetting.dataValues.quarkCookie);
|
||||
} else {
|
||||
throw new Error("请先设置夸克网盘cookie");
|
||||
@injectable()
|
||||
export class QuarkController extends BaseController {
|
||||
constructor(@inject(TYPES.QuarkService) private quarkService: QuarkService) {
|
||||
super();
|
||||
}
|
||||
};
|
||||
|
||||
export const quarkController = {
|
||||
async getShareInfo(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { pwdId, passcode } = req.query;
|
||||
await setCookie(req);
|
||||
const result = await quark.getShareInfo(pwdId as string, passcode as string);
|
||||
await this.handleRequest(req, res, async () => {
|
||||
const { shareCode, receiveCode } = req.query;
|
||||
await this.quarkService.setCookie(req);
|
||||
const result = await this.quarkService.getShareInfo(
|
||||
shareCode as string,
|
||||
receiveCode as string
|
||||
);
|
||||
sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
sendError(res, { message: "获取分享信息失败" });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getFolderList(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await this.handleRequest(req, res, async () => {
|
||||
const { parentCid } = req.query;
|
||||
await setCookie(req);
|
||||
const result = await quark.getFolderList(parentCid as string);
|
||||
await this.quarkService.setCookie(req);
|
||||
const result = await this.quarkService.getFolderList(parentCid as string);
|
||||
sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
sendError(res, { message: (error as Error).message || "获取目录列表失败" });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async saveFile(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await setCookie(req);
|
||||
const result = await quark.saveSharedFile(req.body);
|
||||
await this.handleRequest(req, res, async () => {
|
||||
await this.quarkService.setCookie(req);
|
||||
const result = await this.quarkService.saveSharedFile(req.body);
|
||||
sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
sendError(res, { message: (error as Error).message || "保存文件失败" });
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { Request, Response } from "express";
|
||||
import Searcher from "../services/Searcher";
|
||||
import { sendSuccess, sendError } from "../utils/response";
|
||||
import { injectable, inject } from "inversify";
|
||||
import { TYPES } from "../core/types";
|
||||
import { Searcher } from "../services/Searcher";
|
||||
import { BaseController } from "./BaseController";
|
||||
import { sendSuccess } from "../utils/response";
|
||||
|
||||
@injectable()
|
||||
export class ResourceController extends BaseController {
|
||||
constructor(@inject(TYPES.Searcher) private searcher: Searcher) {
|
||||
super();
|
||||
}
|
||||
|
||||
export const resourceController = {
|
||||
async search(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { keyword, channelId = "", lastMessageId = "" } = req.query; // Remove `: string` from here
|
||||
const result = await Searcher.searchAll(
|
||||
await this.handleRequest(req, res, async () => {
|
||||
const { keyword, channelId = "", lastMessageId = "" } = req.query;
|
||||
const result = await this.searcher.searchAll(
|
||||
keyword as string,
|
||||
channelId as string,
|
||||
lastMessageId as string
|
||||
);
|
||||
sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
sendError(res, {
|
||||
message: (error as Error).message || "搜索资源失败",
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +1,28 @@
|
||||
import { Request, Response } from "express";
|
||||
import { sendSuccess, sendError } from "../utils/response";
|
||||
import Searcher from "../services/Searcher";
|
||||
import UserSetting from "../models/UserSetting";
|
||||
import GlobalSetting from "../models/GlobalSetting";
|
||||
import { iamgesInstance } from "./teleImages";
|
||||
import { injectable, inject } from "inversify";
|
||||
import { TYPES } from "../core/types";
|
||||
import { SettingService } from "../services/SettingService";
|
||||
import { BaseController } from "./BaseController";
|
||||
|
||||
@injectable()
|
||||
export class SettingController extends BaseController {
|
||||
constructor(@inject(TYPES.SettingService) private settingService: SettingService) {
|
||||
super();
|
||||
}
|
||||
|
||||
export const settingController = {
|
||||
async get(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const userId = req.user?.userId;
|
||||
const role = req.user?.role;
|
||||
if (userId !== null) {
|
||||
let userSettings = await UserSetting.findOne({ where: { userId } });
|
||||
if (!userSettings) {
|
||||
userSettings = {
|
||||
userId: userId,
|
||||
cloud115Cookie: "",
|
||||
quarkCookie: "",
|
||||
} as UserSetting;
|
||||
if (userSettings) {
|
||||
await UserSetting.create(userSettings);
|
||||
}
|
||||
}
|
||||
const globalSetting = await GlobalSetting.findOne();
|
||||
sendSuccess(res, {
|
||||
data: {
|
||||
userSettings,
|
||||
globalSetting: role === 1 ? globalSetting : null,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
sendError(res, { message: "用户ID无效" });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("获取设置失败:" + error);
|
||||
sendError(res, { message: (error as Error).message || "获取设置失败" });
|
||||
}
|
||||
},
|
||||
await this.handleRequest(req, res, async () => {
|
||||
const userId = Number(req.user?.userId);
|
||||
const role = Number(req.user?.role);
|
||||
return await this.settingService.getSettings(userId, role);
|
||||
});
|
||||
}
|
||||
|
||||
async save(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const userId = req.user?.userId;
|
||||
const role = req.user?.role;
|
||||
if (userId !== null) {
|
||||
const { userSettings, globalSetting } = req.body;
|
||||
await UserSetting.update(userSettings, { where: { userId } });
|
||||
if (role === 1 && globalSetting) await GlobalSetting.update(globalSetting, { where: {} });
|
||||
Searcher.updateAxiosInstance();
|
||||
iamgesInstance.updateProxyConfig();
|
||||
sendSuccess(res, {
|
||||
message: "保存成功",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("保存设置失败:" + error);
|
||||
sendError(res, { message: (error as Error).message || "保存设置失败" });
|
||||
}
|
||||
},
|
||||
};
|
||||
await this.handleRequest(req, res, async () => {
|
||||
const userId = Number(req.user?.userId);
|
||||
const role = Number(req.user?.role);
|
||||
return await this.settingService.saveSettings(userId, role, req.body);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +1,19 @@
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
import e, { Request, Response } from "express";
|
||||
import tunnel from "tunnel";
|
||||
import GlobalSetting from "../models/GlobalSetting";
|
||||
import { GlobalSettingAttributes } from "../models/GlobalSetting";
|
||||
import { Request, Response } from "express";
|
||||
import { injectable, inject } from "inversify";
|
||||
import { TYPES } from "../core/types";
|
||||
import { ImageService } from "../services/ImageService";
|
||||
import { BaseController } from "./BaseController";
|
||||
|
||||
export class ImageControll {
|
||||
private axiosInstance: AxiosInstance | null = null;
|
||||
private settings: GlobalSetting | null = null;
|
||||
|
||||
constructor() {
|
||||
this.initializeAxiosInstance();
|
||||
@injectable()
|
||||
export class ImageController extends BaseController {
|
||||
constructor(@inject(TYPES.ImageService) private imageService: ImageService) {
|
||||
super();
|
||||
}
|
||||
|
||||
private async initializeAxiosInstance(): Promise<void> {
|
||||
try {
|
||||
this.settings = await GlobalSetting.findOne();
|
||||
} catch (error) {
|
||||
console.error("Error fetching global settings:", error);
|
||||
}
|
||||
const globalSetting = this.settings?.dataValues || ({} as GlobalSettingAttributes);
|
||||
this.axiosInstance = axios.create({
|
||||
timeout: 3000,
|
||||
httpsAgent: globalSetting.isProxyEnabled
|
||||
? tunnel.httpsOverHttp({
|
||||
proxy: {
|
||||
host: globalSetting.httpProxyHost,
|
||||
port: globalSetting.httpProxyPort,
|
||||
headers: {
|
||||
"Proxy-Authorization": "",
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined,
|
||||
withCredentials: true,
|
||||
async getImages(req: Request, res: Response): Promise<void> {
|
||||
await this.handleRequest(req, res, async () => {
|
||||
const url = req.query.url as string;
|
||||
return await this.imageService.getImages(url);
|
||||
});
|
||||
}
|
||||
public async updateProxyConfig(): Promise<void> {
|
||||
try {
|
||||
this.settings = await GlobalSetting.findOne();
|
||||
const globalSetting = this.settings?.dataValues || ({} as GlobalSettingAttributes);
|
||||
if (this.axiosInstance) {
|
||||
this.axiosInstance.defaults.httpsAgent = globalSetting.isProxyEnabled
|
||||
? tunnel.httpsOverHttp({
|
||||
proxy: {
|
||||
host: globalSetting.httpProxyHost,
|
||||
port: globalSetting.httpProxyPort,
|
||||
headers: {
|
||||
"Proxy-Authorization": "",
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating proxy config:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async getImages(req: Request, res: Response, url: string): Promise<void> {
|
||||
try {
|
||||
const response = await this.axiosInstance?.get(url, { responseType: "stream" });
|
||||
res.set("Content-Type", response?.headers["content-type"]);
|
||||
response?.data.pipe(res);
|
||||
} catch (error) {
|
||||
res.status(500).send("Image fetch error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const iamgesInstance = new ImageControll();
|
||||
|
||||
export const imageControll = {
|
||||
getImages: async (req: Request, res: Response): Promise<void> => {
|
||||
const url = req.query.url as string;
|
||||
iamgesInstance.getImages(req, res, url);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,62 +1,26 @@
|
||||
import { Request, Response } from "express";
|
||||
import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import GlobalSetting from "../models/GlobalSetting";
|
||||
import User from "../models/User";
|
||||
import { config } from "../config";
|
||||
import { sendSuccess, sendError } from "../utils/response";
|
||||
import { injectable, inject } from "inversify";
|
||||
import { TYPES } from "../core/types";
|
||||
import { UserService } from "../services/UserService";
|
||||
import { BaseController } from "./BaseController";
|
||||
|
||||
@injectable()
|
||||
export class UserController extends BaseController {
|
||||
constructor(@inject(TYPES.UserService) private userService: UserService) {
|
||||
super();
|
||||
}
|
||||
|
||||
const isValidInput = (input: string): boolean => {
|
||||
// 检查是否包含空格或汉字
|
||||
const regex = /^[^\s\u4e00-\u9fa5]+$/;
|
||||
return regex.test(input);
|
||||
};
|
||||
export const userController = {
|
||||
async register(req: Request, res: Response): Promise<void> {
|
||||
const { username, password, registerCode } = req.body;
|
||||
const globalSetting = await GlobalSetting.findOne();
|
||||
const registerCodeList = [
|
||||
globalSetting?.dataValues.CommonUserCode,
|
||||
globalSetting?.dataValues.AdminUserCode,
|
||||
];
|
||||
if (!registerCode || !registerCodeList.includes(Number(registerCode))) {
|
||||
return sendError(res, { message: "注册码错误" });
|
||||
}
|
||||
// 验证输入
|
||||
if (!isValidInput(username) || !isValidInput(password)) {
|
||||
return sendError(res, { message: "用户名、密码或注册码不能包含空格或汉字" });
|
||||
}
|
||||
// 检查用户名是否已存在
|
||||
const existingUser = await User.findOne({ where: { username } });
|
||||
if (existingUser) {
|
||||
return sendError(res, { message: "用户名已存在" });
|
||||
}
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
try {
|
||||
const role = registerCodeList.findIndex((x) => x === Number(registerCode));
|
||||
const user = await User.create({ username, password: hashedPassword, role });
|
||||
sendSuccess(res, {
|
||||
data: user,
|
||||
message: "用户注册成功",
|
||||
});
|
||||
} catch (error) {
|
||||
sendError(res, { message: (error as Error).message || "用户注册失败" });
|
||||
}
|
||||
},
|
||||
await this.handleRequest(req, res, async () => {
|
||||
const { username, password, code } = req.body;
|
||||
return await this.userService.register(username, password, code);
|
||||
});
|
||||
}
|
||||
|
||||
async login(req: Request, res: Response): Promise<void> {
|
||||
const { username, password } = req.body;
|
||||
const user = await User.findOne({ where: { username } });
|
||||
if (!user || !(await bcrypt.compare(password, user.password))) {
|
||||
return sendError(res, { message: "用户名或密码错误" });
|
||||
}
|
||||
const token = jwt.sign({ userId: user.userId, role: user.role }, config.jwtSecret, {
|
||||
expiresIn: "6h",
|
||||
await this.handleRequest(req, res, async () => {
|
||||
const { username, password } = req.body;
|
||||
return await this.userService.login(username, password);
|
||||
});
|
||||
sendSuccess(res, {
|
||||
data: {
|
||||
token,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
21
backend/src/core/ApiResponse.ts
Normal file
21
backend/src/core/ApiResponse.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export class ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
code: number;
|
||||
|
||||
private constructor(success: boolean, code: number, data?: T, message?: string) {
|
||||
this.success = success;
|
||||
this.code = code;
|
||||
this.data = data;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
static success<T>(data?: T, message = "操作成功"): ApiResponse<T> {
|
||||
return new ApiResponse(true, 200, data, message);
|
||||
}
|
||||
|
||||
static error(message: string, code = 500): ApiResponse<null> {
|
||||
return new ApiResponse(false, code, null, message);
|
||||
}
|
||||
}
|
||||
19
backend/src/core/ServiceRegistry.ts
Normal file
19
backend/src/core/ServiceRegistry.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export class ServiceRegistry {
|
||||
private static instance: ServiceRegistry;
|
||||
private services: Map<string, any> = new Map();
|
||||
|
||||
static getInstance(): ServiceRegistry {
|
||||
if (!ServiceRegistry.instance) {
|
||||
ServiceRegistry.instance = new ServiceRegistry();
|
||||
}
|
||||
return ServiceRegistry.instance;
|
||||
}
|
||||
|
||||
register(name: string, service: any): void {
|
||||
this.services.set(name, service);
|
||||
}
|
||||
|
||||
get<T>(name: string): T {
|
||||
return this.services.get(name);
|
||||
}
|
||||
}
|
||||
16
backend/src/core/container.ts
Normal file
16
backend/src/core/container.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Container } from "inversify";
|
||||
import { TYPES } from "./types";
|
||||
import { Cloud115Service } from "../services/Cloud115Service";
|
||||
import { QuarkService } from "../services/QuarkService";
|
||||
import { Searcher } from "../services/Searcher";
|
||||
import { DatabaseService } from "../services/DatabaseService";
|
||||
|
||||
const container = new Container();
|
||||
|
||||
// 注册服务
|
||||
container.bind<Cloud115Service>(TYPES.Cloud115Service).to(Cloud115Service);
|
||||
container.bind<QuarkService>(TYPES.QuarkService).to(QuarkService);
|
||||
container.bind<Searcher>(TYPES.Searcher).to(Searcher);
|
||||
container.bind<DatabaseService>(TYPES.DatabaseService).to(DatabaseService);
|
||||
|
||||
export { container };
|
||||
18
backend/src/core/types.ts
Normal file
18
backend/src/core/types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export const TYPES = {
|
||||
DatabaseService: Symbol.for("DatabaseService"),
|
||||
Cloud115Service: Symbol.for("Cloud115Service"),
|
||||
QuarkService: Symbol.for("QuarkService"),
|
||||
Searcher: Symbol.for("Searcher"),
|
||||
DoubanService: Symbol.for("DoubanService"),
|
||||
ImageService: Symbol.for("ImageService"),
|
||||
SettingService: Symbol.for("SettingService"),
|
||||
UserService: Symbol.for("UserService"),
|
||||
|
||||
Cloud115Controller: Symbol.for("Cloud115Controller"),
|
||||
QuarkController: Symbol.for("QuarkController"),
|
||||
ResourceController: Symbol.for("ResourceController"),
|
||||
DoubanController: Symbol.for("DoubanController"),
|
||||
ImageController: Symbol.for("ImageController"),
|
||||
SettingController: Symbol.for("SettingController"),
|
||||
UserController: Symbol.for("UserController"),
|
||||
};
|
||||
12
backend/src/interfaces/ICloudService.ts
Normal file
12
backend/src/interfaces/ICloudService.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {
|
||||
ShareInfoResponse,
|
||||
FolderListResponse,
|
||||
SaveFileParams,
|
||||
SaveFileResponse,
|
||||
} from "../types/cloud";
|
||||
|
||||
export interface ICloudService {
|
||||
getShareInfo(shareCode: string, receiveCode?: string): Promise<ShareInfoResponse>;
|
||||
getFolderList(parentCid?: string): Promise<FolderListResponse>;
|
||||
saveSharedFile(params: SaveFileParams): Promise<SaveFileResponse>;
|
||||
}
|
||||
40
backend/src/inversify.config.ts
Normal file
40
backend/src/inversify.config.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Container } from "inversify";
|
||||
import { TYPES } from "./core/types";
|
||||
|
||||
// Services
|
||||
import { DatabaseService } from "./services/DatabaseService";
|
||||
import { Cloud115Service } from "./services/Cloud115Service";
|
||||
import { QuarkService } from "./services/QuarkService";
|
||||
import { Searcher } from "./services/Searcher";
|
||||
import { DoubanService } from "./services/DoubanService";
|
||||
import { UserService } from "./services/UserService";
|
||||
|
||||
// Controllers
|
||||
import { Cloud115Controller } from "./controllers/cloud115";
|
||||
import { QuarkController } from "./controllers/quark";
|
||||
import { ResourceController } from "./controllers/resource";
|
||||
import { DoubanController } from "./controllers/douban";
|
||||
import { ImageController } from "./controllers/teleImages";
|
||||
import { SettingController } from "./controllers/setting";
|
||||
import { UserController } from "./controllers/user";
|
||||
|
||||
const container = new Container();
|
||||
|
||||
// Services
|
||||
container.bind<DatabaseService>(TYPES.DatabaseService).to(DatabaseService).inSingletonScope();
|
||||
container.bind<Cloud115Service>(TYPES.Cloud115Service).to(Cloud115Service).inSingletonScope();
|
||||
container.bind<QuarkService>(TYPES.QuarkService).to(QuarkService).inSingletonScope();
|
||||
container.bind<Searcher>(TYPES.Searcher).to(Searcher).inSingletonScope();
|
||||
container.bind<DoubanService>(TYPES.DoubanService).to(DoubanService).inSingletonScope();
|
||||
container.bind<UserService>(TYPES.UserService).to(UserService).inSingletonScope();
|
||||
|
||||
// Controllers
|
||||
container.bind<Cloud115Controller>(TYPES.Cloud115Controller).to(Cloud115Controller);
|
||||
container.bind<QuarkController>(TYPES.QuarkController).to(QuarkController);
|
||||
container.bind<ResourceController>(TYPES.ResourceController).to(ResourceController);
|
||||
container.bind<DoubanController>(TYPES.DoubanController).to(DoubanController);
|
||||
container.bind<ImageController>(TYPES.ImageController).to(ImageController);
|
||||
container.bind<SettingController>(TYPES.SettingController).to(SettingController);
|
||||
container.bind<UserController>(TYPES.UserController).to(UserController);
|
||||
|
||||
export { container };
|
||||
15
backend/src/middleware/cors.ts
Normal file
15
backend/src/middleware/cors.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
export const cors = () => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, Cookie");
|
||||
res.header("Access-Control-Allow-Credentials", "true");
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
14
backend/src/middleware/index.ts
Normal file
14
backend/src/middleware/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Application } from "express";
|
||||
import { errorHandler } from "./errorHandler";
|
||||
import { authMiddleware } from "./auth";
|
||||
import { requestLogger } from "./requestLogger";
|
||||
import { rateLimiter } from "./rateLimiter";
|
||||
import { cors } from "./cors";
|
||||
|
||||
export const setupMiddlewares = (app: Application) => {
|
||||
app.use(cors());
|
||||
app.use(requestLogger());
|
||||
app.use(rateLimiter());
|
||||
app.use(authMiddleware);
|
||||
app.use(errorHandler);
|
||||
};
|
||||
27
backend/src/middleware/rateLimiter.ts
Normal file
27
backend/src/middleware/rateLimiter.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
const requestCounts = new Map<string, { count: number; timestamp: number }>();
|
||||
const WINDOW_MS = 60 * 1000; // 1分钟窗口
|
||||
const MAX_REQUESTS = 60; // 每个IP每分钟最多60个请求
|
||||
|
||||
export const rateLimiter = () => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
const ip = req.ip || req.socket.remoteAddress || "unknown";
|
||||
const now = Date.now();
|
||||
const record = requestCounts.get(ip) || { count: 0, timestamp: now };
|
||||
|
||||
if (now - record.timestamp > WINDOW_MS) {
|
||||
record.count = 0;
|
||||
record.timestamp = now;
|
||||
}
|
||||
|
||||
record.count++;
|
||||
requestCounts.set(ip, record);
|
||||
|
||||
if (record.count > MAX_REQUESTS) {
|
||||
return res.status(429).json({ message: "请求过于频繁,请稍后再试" });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
18
backend/src/middleware/requestLogger.ts
Normal file
18
backend/src/middleware/requestLogger.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export const requestLogger = () => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
const start = Date.now();
|
||||
res.on("finish", () => {
|
||||
const duration = Date.now() - start;
|
||||
logger.info({
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
status: res.statusCode,
|
||||
duration: `${duration}ms`,
|
||||
});
|
||||
});
|
||||
next();
|
||||
};
|
||||
};
|
||||
@@ -1,36 +1,50 @@
|
||||
import express from "express";
|
||||
import { cloud115Controller } from "../controllers/cloud115";
|
||||
import { quarkController } from "../controllers/quark";
|
||||
import { resourceController } from "../controllers/resource";
|
||||
import { doubanController } from "../controllers/douban";
|
||||
import { imageControll } from "../controllers/teleImages";
|
||||
import settingRoutes from "./setting";
|
||||
import userRoutes from "./user";
|
||||
import { Router } from "express";
|
||||
import { container } from "../inversify.config";
|
||||
import { TYPES } from "../core/types";
|
||||
import { Cloud115Controller } from "../controllers/cloud115";
|
||||
import { QuarkController } from "../controllers/quark";
|
||||
import { ResourceController } from "../controllers/resource";
|
||||
import { DoubanController } from "../controllers/douban";
|
||||
import { ImageController } from "../controllers/teleImages";
|
||||
import { SettingController } from "../controllers/setting";
|
||||
import { UserController } from "../controllers/user";
|
||||
|
||||
const router = express.Router();
|
||||
const router = Router();
|
||||
|
||||
// 获取控制器实例
|
||||
const cloud115Controller = container.get<Cloud115Controller>(TYPES.Cloud115Controller);
|
||||
const quarkController = container.get<QuarkController>(TYPES.QuarkController);
|
||||
const resourceController = container.get<ResourceController>(TYPES.ResourceController);
|
||||
const doubanController = container.get<DoubanController>(TYPES.DoubanController);
|
||||
const imageController = container.get<ImageController>(TYPES.ImageController);
|
||||
const settingController = container.get<SettingController>(TYPES.SettingController);
|
||||
const userController = container.get<UserController>(TYPES.UserController);
|
||||
|
||||
// 用户相关路由
|
||||
router.use("/user", userRoutes);
|
||||
router.post("/user/login", (req, res) => userController.login(req, res));
|
||||
router.post("/user/register", (req, res) => userController.register(req, res));
|
||||
|
||||
router.use("/tele-images", imageControll.getImages);
|
||||
// 图片相关路由
|
||||
router.get("/tele-images", (req, res) => imageController.getImages(req, res));
|
||||
|
||||
// 设置相关路由
|
||||
router.use("/setting", settingRoutes);
|
||||
router.get("/setting/get", (req, res) => settingController.get(req, res));
|
||||
router.post("/setting/save", (req, res) => settingController.save(req, res));
|
||||
|
||||
// 资源搜索
|
||||
router.get("/search", resourceController.search);
|
||||
router.get("/search", (req, res) => resourceController.search(req, res));
|
||||
|
||||
// 115网盘相关
|
||||
router.get("/cloud115/share-info", cloud115Controller.getShareInfo);
|
||||
router.get("/cloud115/folders", cloud115Controller.getFolderList);
|
||||
router.post("/cloud115/save", cloud115Controller.saveFile);
|
||||
router.get("/cloud115/share-info", (req, res) => cloud115Controller.getShareInfo(req, res));
|
||||
router.get("/cloud115/folders", (req, res) => cloud115Controller.getFolderList(req, res));
|
||||
router.post("/cloud115/save", (req, res) => cloud115Controller.saveFile(req, res));
|
||||
|
||||
// 夸克网盘相关
|
||||
router.get("/quark/share-info", quarkController.getShareInfo);
|
||||
router.get("/quark/folders", quarkController.getFolderList);
|
||||
router.post("/quark/save", quarkController.saveFile);
|
||||
router.get("/quark/share-info", (req, res) => quarkController.getShareInfo(req, res));
|
||||
router.get("/quark/folders", (req, res) => quarkController.getFolderList(req, res));
|
||||
router.post("/quark/save", (req, res) => quarkController.saveFile(req, res));
|
||||
|
||||
// 获取豆瓣热门列表
|
||||
router.get("/douban/hot", doubanController.getDoubanHotList);
|
||||
router.get("/douban/hot", (req, res) => doubanController.getDoubanHotList(req, res));
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { AxiosHeaders, AxiosInstance } from "axios"; // 导入 AxiosHeaders
|
||||
import { createAxiosInstance } from "../utils/axiosInstance";
|
||||
import { Logger } from "../utils/logger";
|
||||
import { ShareInfoResponse } from "../types/cloud115";
|
||||
import { injectable } from "inversify";
|
||||
import { Request } from "express";
|
||||
import UserSetting from "../models/UserSetting";
|
||||
import { ICloudService } from "../types/services";
|
||||
import { logger } from "@/utils/logger";
|
||||
|
||||
interface Cloud115ListItem {
|
||||
cid: string;
|
||||
@@ -20,11 +24,12 @@ interface Cloud115PathItem {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class Cloud115Service {
|
||||
@injectable()
|
||||
export class Cloud115Service implements ICloudService {
|
||||
private api: AxiosInstance;
|
||||
private cookie: string = "";
|
||||
|
||||
constructor(cookie?: string) {
|
||||
constructor() {
|
||||
this.api = createAxiosInstance(
|
||||
"https://webapi.115.com",
|
||||
AxiosHeaders.from({
|
||||
@@ -44,19 +49,23 @@ export class Cloud115Service {
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
})
|
||||
);
|
||||
if (cookie) {
|
||||
this.setCookie(cookie);
|
||||
} else {
|
||||
console.log("请注意:115网盘需要提供cookie进行身份验证");
|
||||
}
|
||||
|
||||
this.api.interceptors.request.use((config) => {
|
||||
config.headers.cookie = cookie || this.cookie;
|
||||
config.headers.cookie = this.cookie;
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
||||
public setCookie(cookie: string): void {
|
||||
this.cookie = cookie;
|
||||
async setCookie(req: Request): Promise<void> {
|
||||
const userId = req.user?.userId;
|
||||
const userSetting = await UserSetting.findOne({
|
||||
where: { userId },
|
||||
});
|
||||
if (userSetting && userSetting.dataValues.cloud115Cookie) {
|
||||
this.cookie = userSetting.dataValues.cloud115Cookie;
|
||||
} else {
|
||||
throw new Error("请先设置115网盘cookie");
|
||||
}
|
||||
}
|
||||
|
||||
async getShareInfo(shareCode: string, receiveCode = ""): Promise<ShareInfoResponse> {
|
||||
@@ -114,7 +123,7 @@ export class Cloud115Service {
|
||||
})),
|
||||
};
|
||||
} else {
|
||||
Logger.error("获取目录列表失败:", response.data.error);
|
||||
logger.error("获取目录列表失败:", response.data.error);
|
||||
throw new Error("获取115pan目录列表失败:" + response.data.error);
|
||||
}
|
||||
}
|
||||
@@ -132,14 +141,14 @@ export class Cloud115Service {
|
||||
file_id: params.fileId,
|
||||
});
|
||||
const response = await this.api.post("/share/receive", param.toString());
|
||||
Logger.info("保存文件:", response.data);
|
||||
logger.info("保存文件:", response.data);
|
||||
if (response.data.state) {
|
||||
return {
|
||||
message: response.data.error,
|
||||
data: response.data.data,
|
||||
};
|
||||
} else {
|
||||
Logger.error("保存文件失败:", response.data.error);
|
||||
logger.error("保存文件失败:", response.data.error);
|
||||
throw new Error("保存115pan文件失败:" + response.data.error);
|
||||
}
|
||||
}
|
||||
|
||||
38
backend/src/services/DatabaseService.ts
Normal file
38
backend/src/services/DatabaseService.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Sequelize, QueryTypes } from "sequelize";
|
||||
|
||||
export class DatabaseService {
|
||||
private sequelize: Sequelize;
|
||||
|
||||
constructor() {
|
||||
this.sequelize = new Sequelize({
|
||||
dialect: "sqlite",
|
||||
storage: "./data/database.sqlite",
|
||||
});
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
try {
|
||||
await this.sequelize.query("PRAGMA foreign_keys = OFF");
|
||||
await this.cleanupBackupTables();
|
||||
await this.sequelize.sync({ alter: true });
|
||||
await this.sequelize.query("PRAGMA foreign_keys = ON");
|
||||
} catch (error) {
|
||||
throw new Error(`数据库初始化失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanupBackupTables(): Promise<void> {
|
||||
const backupTables = await this.sequelize.query<{ name: string }>(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%\\_backup%' ESCAPE '\\'",
|
||||
{ type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
for (const table of backupTables) {
|
||||
if (table?.name) {
|
||||
await this.sequelize.query(`DROP TABLE IF EXISTS ${table.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ... 其他数据库相关方法
|
||||
}
|
||||
@@ -10,7 +10,7 @@ interface DoubanSubject {
|
||||
is_new: boolean;
|
||||
}
|
||||
|
||||
class DoubanService {
|
||||
export class DoubanService {
|
||||
private baseUrl: string;
|
||||
private api: AxiosInstance;
|
||||
|
||||
@@ -62,5 +62,3 @@ class DoubanService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DoubanService;
|
||||
|
||||
42
backend/src/services/ImageService.ts
Normal file
42
backend/src/services/ImageService.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { injectable } from "inversify";
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
import tunnel from "tunnel";
|
||||
import GlobalSetting from "../models/GlobalSetting";
|
||||
import { GlobalSettingAttributes } from "../models/GlobalSetting";
|
||||
|
||||
@injectable()
|
||||
export class ImageService {
|
||||
private axiosInstance: AxiosInstance | null = null;
|
||||
|
||||
constructor() {
|
||||
this.initializeAxiosInstance();
|
||||
}
|
||||
|
||||
private async initializeAxiosInstance(): Promise<void> {
|
||||
const settings = await GlobalSetting.findOne();
|
||||
const globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes);
|
||||
|
||||
this.axiosInstance = axios.create({
|
||||
timeout: 3000,
|
||||
httpsAgent: globalSetting.isProxyEnabled
|
||||
? tunnel.httpsOverHttp({
|
||||
proxy: {
|
||||
host: globalSetting.httpProxyHost,
|
||||
port: globalSetting.httpProxyPort,
|
||||
headers: {
|
||||
"Proxy-Authorization": "",
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined,
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
async getImages(url: string): Promise<any> {
|
||||
if (!this.axiosInstance) {
|
||||
throw new Error("Axios instance not initialized");
|
||||
}
|
||||
return await this.axiosInstance.get(url, { responseType: "stream" });
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { AxiosInstance, AxiosHeaders } from "axios";
|
||||
import { Logger } from "../utils/logger";
|
||||
import { logger } from "../utils/logger";
|
||||
import { createAxiosInstance } from "../utils/axiosInstance";
|
||||
import { injectable } from "inversify";
|
||||
import { Request } from "express";
|
||||
import UserSetting from "../models/UserSetting";
|
||||
|
||||
interface QuarkShareInfo {
|
||||
stoken?: string;
|
||||
@@ -20,11 +23,12 @@ interface QuarkFolderItem {
|
||||
file_type: number;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class QuarkService {
|
||||
private api: AxiosInstance;
|
||||
private cookie: string = "";
|
||||
|
||||
constructor(cookie?: string) {
|
||||
constructor() {
|
||||
this.api = createAxiosInstance(
|
||||
"https://drive-h.quark.cn",
|
||||
AxiosHeaders.from({
|
||||
@@ -41,19 +45,23 @@ export class QuarkService {
|
||||
"sec-fetch-site": "same-site",
|
||||
})
|
||||
);
|
||||
if (cookie) {
|
||||
this.setCookie(cookie);
|
||||
} else {
|
||||
console.log("请注意:夸克网盘需要提供cookie进行身份验证");
|
||||
}
|
||||
|
||||
this.api.interceptors.request.use((config) => {
|
||||
config.headers.cookie = cookie || this.cookie;
|
||||
config.headers.cookie = this.cookie;
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
||||
public setCookie(cookie: string): void {
|
||||
this.cookie = cookie;
|
||||
async setCookie(req: Request): Promise<void> {
|
||||
const userId = req.user?.userId;
|
||||
const userSetting = await UserSetting.findOne({
|
||||
where: { userId },
|
||||
});
|
||||
if (userSetting && userSetting.dataValues.quarkCookie) {
|
||||
this.cookie = userSetting.dataValues.quarkCookie;
|
||||
} else {
|
||||
throw new Error("请先设置夸克网盘cookie");
|
||||
}
|
||||
}
|
||||
|
||||
async getShareInfo(pwdId: string, passcode = ""): Promise<{ data: QuarkShareInfo }> {
|
||||
@@ -148,7 +156,7 @@ export class QuarkService {
|
||||
};
|
||||
} else {
|
||||
const message = "获取夸克目录列表失败:" + response.data.error;
|
||||
Logger.error(message);
|
||||
logger.error(message);
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import GlobalSetting from "../models/GlobalSetting";
|
||||
import { GlobalSettingAttributes } from "../models/GlobalSetting";
|
||||
import * as cheerio from "cheerio";
|
||||
import { config } from "../config";
|
||||
import { Logger } from "../utils/logger";
|
||||
import { logger } from "../utils/logger";
|
||||
import { injectable } from "inversify";
|
||||
|
||||
interface sourceItem {
|
||||
messageId?: string;
|
||||
@@ -20,20 +21,20 @@ interface sourceItem {
|
||||
cloudType?: string;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class Searcher {
|
||||
private axiosInstance: AxiosInstance | null = null;
|
||||
private static instance: Searcher;
|
||||
private api: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.initializeAxiosInstance();
|
||||
this.initAxiosInstance();
|
||||
Searcher.instance = this;
|
||||
}
|
||||
|
||||
private async initializeAxiosInstance(isUpdate = false): Promise<void> {
|
||||
let settings = null;
|
||||
if (isUpdate) {
|
||||
settings = await GlobalSetting.findOne();
|
||||
}
|
||||
private async initAxiosInstance() {
|
||||
const settings = await GlobalSetting.findOne();
|
||||
const globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes);
|
||||
this.axiosInstance = createAxiosInstance(
|
||||
this.api = createAxiosInstance(
|
||||
config.telegram.baseUrl,
|
||||
AxiosHeaders.from({
|
||||
accept:
|
||||
@@ -56,8 +57,11 @@ export class Searcher {
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
public async updateAxiosInstance() {
|
||||
await this.initializeAxiosInstance(true);
|
||||
|
||||
public static async updateAxiosInstance(): Promise<void> {
|
||||
if (Searcher.instance) {
|
||||
await Searcher.instance.initAxiosInstance();
|
||||
}
|
||||
}
|
||||
|
||||
private extractCloudLinks(text: string): { links: string[]; cloudType: string } {
|
||||
@@ -111,7 +115,7 @@ export class Searcher {
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.error(`搜索频道 ${channel.name} 失败:`, error);
|
||||
logger.error(`搜索频道 ${channel.name} 失败:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -125,10 +129,10 @@ export class Searcher {
|
||||
|
||||
async searchInWeb(url: string) {
|
||||
try {
|
||||
if (!this.axiosInstance) {
|
||||
if (!this.api) {
|
||||
throw new Error("Axios instance is not initialized");
|
||||
}
|
||||
const response = await this.axiosInstance.get(url);
|
||||
const response = await this.api.get(url);
|
||||
const html = response.data;
|
||||
const $ = cheerio.load(html);
|
||||
const items: sourceItem[] = [];
|
||||
@@ -205,7 +209,7 @@ export class Searcher {
|
||||
});
|
||||
return { items: items, channelLogo };
|
||||
} catch (error) {
|
||||
Logger.error(`搜索错误: ${url}`, error);
|
||||
logger.error(`搜索错误: ${url}`, error);
|
||||
return {
|
||||
items: [],
|
||||
channelLogo: "",
|
||||
|
||||
46
backend/src/services/SettingService.ts
Normal file
46
backend/src/services/SettingService.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { injectable } from "inversify";
|
||||
import UserSetting from "../models/UserSetting";
|
||||
import GlobalSetting from "../models/GlobalSetting";
|
||||
import { Searcher } from "./Searcher";
|
||||
|
||||
@injectable()
|
||||
export class SettingService {
|
||||
async getSettings(userId: number | undefined, role: number | undefined) {
|
||||
if (!userId) {
|
||||
throw new Error("用户ID无效");
|
||||
}
|
||||
|
||||
let userSettings = await UserSetting.findOne({ where: { userId: userId.toString() } });
|
||||
if (!userSettings) {
|
||||
userSettings = await UserSetting.create({
|
||||
userId: userId.toString(),
|
||||
cloud115Cookie: "",
|
||||
quarkCookie: "",
|
||||
});
|
||||
}
|
||||
|
||||
const globalSetting = await GlobalSetting.findOne();
|
||||
return {
|
||||
data: {
|
||||
userSettings,
|
||||
globalSetting: role === 1 ? globalSetting : null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async saveSettings(userId: number | undefined, role: number | undefined, settings: any) {
|
||||
if (!userId) {
|
||||
throw new Error("用户ID无效");
|
||||
}
|
||||
|
||||
const { userSettings, globalSetting } = settings;
|
||||
await UserSetting.update(userSettings, { where: { userId: userId.toString() } });
|
||||
|
||||
if (role === 1 && globalSetting) {
|
||||
await GlobalSetting.update(globalSetting, { where: {} });
|
||||
}
|
||||
|
||||
await Searcher.updateAxiosInstance();
|
||||
return { message: "保存成功" };
|
||||
}
|
||||
}
|
||||
64
backend/src/services/UserService.ts
Normal file
64
backend/src/services/UserService.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { injectable } from "inversify";
|
||||
import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { config } from "../config";
|
||||
import User from "../models/User";
|
||||
import GlobalSetting from "../models/GlobalSetting";
|
||||
|
||||
@injectable()
|
||||
export class UserService {
|
||||
private isValidInput(input: string): boolean {
|
||||
// 检查是否包含空格或汉字
|
||||
const regex = /^[^\s\u4e00-\u9fa5]+$/;
|
||||
return regex.test(input);
|
||||
}
|
||||
|
||||
async register(username: string, password: string, registerCode: string) {
|
||||
const globalSetting = await GlobalSetting.findOne();
|
||||
const registerCodeList = [
|
||||
globalSetting?.dataValues.CommonUserCode,
|
||||
globalSetting?.dataValues.AdminUserCode,
|
||||
];
|
||||
|
||||
if (!registerCode || !registerCodeList.includes(Number(registerCode))) {
|
||||
throw new Error("注册码错误");
|
||||
}
|
||||
|
||||
// 验证输入
|
||||
if (!this.isValidInput(username) || !this.isValidInput(password)) {
|
||||
throw new Error("用户名、密码或注册码不能包含空格或汉字");
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
const existingUser = await User.findOne({ where: { username } });
|
||||
if (existingUser) {
|
||||
throw new Error("用户名已存在");
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const role = registerCodeList.findIndex((x) => x === Number(registerCode));
|
||||
const user = await User.create({ username, password: hashedPassword, role });
|
||||
|
||||
return {
|
||||
data: user,
|
||||
message: "用户注册成功",
|
||||
};
|
||||
}
|
||||
|
||||
async login(username: string, password: string) {
|
||||
const user = await User.findOne({ where: { username } });
|
||||
if (!user || !(await bcrypt.compare(password, user.password))) {
|
||||
throw new Error("用户名或密码错误");
|
||||
}
|
||||
|
||||
const token = jwt.sign({ userId: user.userId, role: user.role }, config.jwtSecret, {
|
||||
expiresIn: "6h",
|
||||
});
|
||||
|
||||
return {
|
||||
data: {
|
||||
token,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
27
backend/src/types/cloud.ts
Normal file
27
backend/src/types/cloud.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export interface ShareInfoResponse {
|
||||
data: {
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface FolderListResponse {
|
||||
data: {
|
||||
cid: string;
|
||||
name: string;
|
||||
path: { cid: string; name: string }[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface SaveFileParams {
|
||||
shareCode: string;
|
||||
receiveCode?: string;
|
||||
fileId: string;
|
||||
cid?: string;
|
||||
}
|
||||
|
||||
export interface SaveFileResponse {
|
||||
message: string;
|
||||
data: unknown;
|
||||
}
|
||||
15
backend/src/types/index.ts
Normal file
15
backend/src/types/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface Config {
|
||||
app: {
|
||||
port: number;
|
||||
env: string;
|
||||
};
|
||||
database: {
|
||||
type: string;
|
||||
path: string;
|
||||
};
|
||||
jwt: {
|
||||
secret: string;
|
||||
expiresIn: string;
|
||||
};
|
||||
// ... 其他配置类型
|
||||
}
|
||||
9
backend/src/types/services.ts
Normal file
9
backend/src/types/services.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Request } from "express";
|
||||
import { ShareInfoResponse } from "./cloud115";
|
||||
|
||||
export interface ICloudService {
|
||||
setCookie(req: Request): Promise<void>;
|
||||
getShareInfo(shareCode: string, receiveCode?: string): Promise<ShareInfoResponse>;
|
||||
getFolderList(parentCid?: string): Promise<any>;
|
||||
saveSharedFile(params: any): Promise<any>;
|
||||
}
|
||||
@@ -1,38 +1,21 @@
|
||||
type LogLevel = "info" | "success" | "warn" | "error";
|
||||
import winston from "winston";
|
||||
import { Config } from "../config";
|
||||
|
||||
export const Logger = {
|
||||
info(...args: any[]) {
|
||||
this.log("info", ...args);
|
||||
},
|
||||
const logger = winston.createLogger({
|
||||
level: Config.app.env === "development" ? "debug" : "info",
|
||||
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
|
||||
transports: [
|
||||
new winston.transports.File({ filename: "logs/error.log", level: "error" }),
|
||||
new winston.transports.File({ filename: "logs/combined.log" }),
|
||||
],
|
||||
});
|
||||
|
||||
success(...args: any[]) {
|
||||
this.log("success", ...args);
|
||||
},
|
||||
if (Config.app.env !== "production") {
|
||||
logger.add(
|
||||
new winston.transports.Console({
|
||||
format: winston.format.simple(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
warn(...args: any[]) {
|
||||
this.log("warn", ...args);
|
||||
},
|
||||
|
||||
error(...args: any[]) {
|
||||
this.log("error", ...args);
|
||||
},
|
||||
|
||||
log(level: LogLevel, ...args: any[]) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
|
||||
|
||||
switch (level) {
|
||||
case "success":
|
||||
console.log(prefix, ...args);
|
||||
break;
|
||||
case "warn":
|
||||
console.warn(prefix, ...args);
|
||||
break;
|
||||
case "error":
|
||||
console.error(prefix, ...args);
|
||||
break;
|
||||
default:
|
||||
console.log(prefix, ...args);
|
||||
}
|
||||
},
|
||||
};
|
||||
export { logger };
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
"typeRoots": ["./node_modules/@types", "./src/types"],
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
||||
Reference in New Issue
Block a user