From 755a4245301434f5cf6f81390e534ba42229f952 Mon Sep 17 00:00:00 2001 From: jiangrui Date: Mon, 10 Mar 2025 11:21:52 +0800 Subject: [PATCH 01/22] =?UTF-8?q?backend:=E4=BC=98=E5=8C=96=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E5=90=AF=E5=8A=A8=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/app.ts | 188 ++++++++++++++++++++--------- backend/src/utils/axiosInstance.ts | 1 - 2 files changed, 129 insertions(+), 60 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index f3c6d5a..7f2fbc1 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,76 +1,146 @@ // filepath: /d:/code/CloudDiskDown/backend/src/app.ts import "./types/express"; -import express from "express"; +import express, { Application } from "express"; import cors from "cors"; import cookieParser from "cookie-parser"; +import { QueryTypes } from "sequelize"; + +// 路由和中间件导入 import routes from "./routes/api"; import { errorHandler } from "./middleware/errorHandler"; -import sequelize from "./config/database"; import { authMiddleware } from "./middleware/auth"; + +// 数据库和服务相关 +import sequelize from "./config/database"; import GlobalSetting from "./models/GlobalSetting"; import Searcher from "./services/Searcher"; -const app = express(); +// 常量配置 +const PUBLIC_ROUTES = ["/user/login", "/user/register"]; +const IMAGE_PATH = "tele-images"; +const DEFAULT_PORT = 8009; -app.use( - cors({ - origin: "*", - credentials: true, - methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], - allowedHeaders: ["Content-Type", "Authorization", "Cookie"], - }) -); - -app.use(cookieParser()); -app.use(express.json()); - -// 应用 token 验证中间件,排除登录和注册接口 -app.use((req, res, next) => { - if ( - req.path === "/user/login" || - req.path === "/user/register" || - req.path.includes("tele-images") - ) { - return next(); - } - authMiddleware(req, res, next); -}); - -app.use("/", routes); - -const initializeGlobalSettings = async (): Promise => { - const settings = await GlobalSetting.findOne(); - if (!settings) { - await GlobalSetting.create({ - httpProxyHost: "127.0.0.1", - httpProxyPort: 7890, - isProxyEnabled: true, - CommonUserCode: 9527, - AdminUserCode: 230713, - }); - console.log("Global settings initialized with default values."); - } - await Searcher.updateAxiosInstance(); +// 全局设置默认值 +const DEFAULT_GLOBAL_SETTINGS = { + httpProxyHost: "127.0.0.1", + httpProxyPort: 7890, + isProxyEnabled: false, + CommonUserCode: 9527, + AdminUserCode: 230713, }; -// 错误处理 -app.use(errorHandler); +class App { + private app: Application; -const PORT = process.env.PORT || 8009; + constructor() { + this.app = express(); + this.setupMiddlewares(); + this.setupRoutes(); + this.setupErrorHandling(); + } -// 在同步前禁用外键约束,同步后重新启用 -sequelize - .query("PRAGMA foreign_keys = OFF") // 禁用外键 - .then(() => sequelize.sync({ alter: true })) - .then(() => sequelize.query("PRAGMA foreign_keys = ON")) // 重新启用外键 - .then(() => { - app.listen(PORT, async () => { - await initializeGlobalSettings(); - console.log(`Server is running on port ${PORT}`); + private setupMiddlewares(): void { + // CORS 配置 + this.app.use( + cors({ + origin: "*", + credentials: true, + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization", "Cookie"], + }) + ); + + 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); }); - }) - .catch((error) => { - console.error("Database sync failed:", error); - }); + } -export default app; + private setupRoutes(): void { + this.app.use("/", routes); + } + + private setupErrorHandling(): void { + this.app.use(errorHandler); + } + + private async initializeGlobalSettings(): Promise { + 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 { + 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 { + 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"); + + // 启动服务器 + 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"} + `); + }); + } catch (error) { + console.error("❌ Failed to start server:", error); + process.exit(1); + } + } +} + +// 创建并启动应用 +const application = new App(); +application.start().catch((error) => { + console.error("❌ Application failed to start:", error); + process.exit(1); +}); + +export default application; diff --git a/backend/src/utils/axiosInstance.ts b/backend/src/utils/axiosInstance.ts index 3626a26..09f7bba 100644 --- a/backend/src/utils/axiosInstance.ts +++ b/backend/src/utils/axiosInstance.ts @@ -13,7 +13,6 @@ export function createAxiosInstance( proxyConfig?: ProxyConfig ): AxiosInstance { let agent; - console.log(proxyConfig); if (useProxy && proxyConfig) { agent = tunnel.httpsOverHttp({ proxy: proxyConfig, From a78ea7e5bdb5556e50a4b468465deb16fb68fded Mon Sep 17 00:00:00 2001 From: jiangrui Date: Mon, 10 Mar 2025 18:33:47 +0800 Subject: [PATCH 02/22] Refactoring the backend --- backend/package.json | 4 +- backend/src/app.ts | 138 +++----------- backend/src/config/index.ts | 26 ++- backend/src/controllers/BaseController.ts | 17 ++ backend/src/controllers/cloud115.ts | 30 +-- backend/src/controllers/douban.ts | 27 +-- backend/src/controllers/quark.ts | 66 +++---- backend/src/controllers/resource.ts | 30 +-- backend/src/controllers/setting.ts | 79 +++----- backend/src/controllers/teleImages.ts | 85 ++------- backend/src/controllers/user.ts | 76 ++------ backend/src/core/ApiResponse.ts | 21 +++ backend/src/core/ServiceRegistry.ts | 19 ++ backend/src/core/container.ts | 16 ++ backend/src/core/types.ts | 18 ++ backend/src/interfaces/ICloudService.ts | 12 ++ backend/src/inversify.config.ts | 40 ++++ backend/src/middleware/cors.ts | 15 ++ backend/src/middleware/index.ts | 14 ++ backend/src/middleware/rateLimiter.ts | 27 +++ backend/src/middleware/requestLogger.ts | 18 ++ backend/src/routes/api.ts | 54 ++++-- backend/src/services/Cloud115Service.ts | 37 ++-- backend/src/services/DatabaseService.ts | 38 ++++ backend/src/services/DoubanService.ts | 4 +- backend/src/services/ImageService.ts | 42 +++++ backend/src/services/QuarkService.ts | 30 +-- backend/src/services/Searcher.ts | 34 ++-- backend/src/services/SettingService.ts | 46 +++++ backend/src/services/UserService.ts | 64 +++++++ backend/src/types/cloud.ts | 27 +++ backend/src/types/index.ts | 15 ++ backend/src/types/services.ts | 9 + backend/src/utils/logger.ts | 53 ++---- backend/tsconfig.json | 4 +- pnpm-lock.yaml | 213 ++++++++++++++++++++++ 36 files changed, 974 insertions(+), 474 deletions(-) create mode 100644 backend/src/controllers/BaseController.ts create mode 100644 backend/src/core/ApiResponse.ts create mode 100644 backend/src/core/ServiceRegistry.ts create mode 100644 backend/src/core/container.ts create mode 100644 backend/src/core/types.ts create mode 100644 backend/src/interfaces/ICloudService.ts create mode 100644 backend/src/inversify.config.ts create mode 100644 backend/src/middleware/cors.ts create mode 100644 backend/src/middleware/index.ts create mode 100644 backend/src/middleware/rateLimiter.ts create mode 100644 backend/src/middleware/requestLogger.ts create mode 100644 backend/src/services/DatabaseService.ts create mode 100644 backend/src/services/ImageService.ts create mode 100644 backend/src/services/SettingService.ts create mode 100644 backend/src/services/UserService.ts create mode 100644 backend/src/types/cloud.ts create mode 100644 backend/src/types/index.ts create mode 100644 backend/src/types/services.ts diff --git a/backend/package.json b/backend/package.json index 2849e0f..5c97066 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/src/app.ts b/backend/src/app.ts index 7f2fbc1..6676cc1 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -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(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 { - 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 { - 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 { 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); }); diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index e36f251..91fa570 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -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, diff --git a/backend/src/controllers/BaseController.ts b/backend/src/controllers/BaseController.ts new file mode 100644 index 0000000..af3fbb9 --- /dev/null +++ b/backend/src/controllers/BaseController.ts @@ -0,0 +1,17 @@ +import { Request, Response } from "express"; +import { ApiResponse } from "../core/ApiResponse"; + +export abstract class BaseController { + protected async handleRequest( + req: Request, + res: Response, + action: () => Promise + ): Promise { + try { + const result = await action(); + res.json(ApiResponse.success(result)); + } catch (error: any) { + res.status(500).json(ApiResponse.error(error?.message || "未知错误")); + } + } +} diff --git a/backend/src/controllers/cloud115.ts b/backend/src/controllers/cloud115.ts index a70185f..e63c356 100644 --- a/backend/src/controllers/cloud115.ts +++ b/backend/src/controllers/cloud115.ts @@ -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 => { @@ -16,18 +19,19 @@ const setCookie = async (req: Request): Promise => { } }; -export const cloud115Controller = { +@injectable() +export class Cloud115Controller extends BaseController { + constructor(@inject(TYPES.Cloud115Service) private cloud115Service: Cloud115Service) { + super(); + } + async getShareInfo(req: Request, res: Response): Promise { - 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 { try { @@ -38,7 +42,7 @@ export const cloud115Controller = { } catch (error) { sendError(res, { message: (error as Error).message || "获取目录列表失败" }); } - }, + } async saveFile(req: Request, res: Response): Promise { try { @@ -54,7 +58,7 @@ export const cloud115Controller = { } catch (error) { sendError(res, { message: (error as Error).message || "保存文件失败" }); } - }, -}; + } +} export const Cloud115ServiceInstance = cloud115; diff --git a/backend/src/controllers/douban.ts b/backend/src/controllers/douban.ts index 461061a..eca73e4 100644 --- a/backend/src/controllers/douban.ts +++ b/backend/src/controllers/douban.ts @@ -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 { - 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; + }); + } +} diff --git a/backend/src/controllers/quark.ts b/backend/src/controllers/quark.ts index 5673aa0..afa390e 100644 --- a/backend/src/controllers/quark.ts +++ b/backend/src/controllers/quark.ts @@ -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 => { - 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 { - 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 { - 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 { - 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 || "保存文件失败" }); - } - }, -}; + }); + } +} diff --git a/backend/src/controllers/resource.ts b/backend/src/controllers/resource.ts index 2be16e7..c8635a0 100644 --- a/backend/src/controllers/resource.ts +++ b/backend/src/controllers/resource.ts @@ -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 { - 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 || "搜索资源失败", - }); - } - }, -}; + }); + } +} diff --git a/backend/src/controllers/setting.ts b/backend/src/controllers/setting.ts index c70f525..f41b642 100644 --- a/backend/src/controllers/setting.ts +++ b/backend/src/controllers/setting.ts @@ -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 { - 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 { - 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); + }); + } +} diff --git a/backend/src/controllers/teleImages.ts b/backend/src/controllers/teleImages.ts index 6da7ab4..f838631 100644 --- a/backend/src/controllers/teleImages.ts +++ b/backend/src/controllers/teleImages.ts @@ -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 { - 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 { + await this.handleRequest(req, res, async () => { + const url = req.query.url as string; + return await this.imageService.getImages(url); }); } - public async updateProxyConfig(): Promise { - 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 { - 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 => { - const url = req.query.url as string; - iamgesInstance.getImages(req, res, url); - }, -}; diff --git a/backend/src/controllers/user.ts b/backend/src/controllers/user.ts index 84d67d5..27bf0a5 100644 --- a/backend/src/controllers/user.ts +++ b/backend/src/controllers/user.ts @@ -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 { - 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 { - 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, - }, - }); - }, -}; + } +} diff --git a/backend/src/core/ApiResponse.ts b/backend/src/core/ApiResponse.ts new file mode 100644 index 0000000..a8d2c63 --- /dev/null +++ b/backend/src/core/ApiResponse.ts @@ -0,0 +1,21 @@ +export class ApiResponse { + 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(data?: T, message = "操作成功"): ApiResponse { + return new ApiResponse(true, 200, data, message); + } + + static error(message: string, code = 500): ApiResponse { + return new ApiResponse(false, code, null, message); + } +} diff --git a/backend/src/core/ServiceRegistry.ts b/backend/src/core/ServiceRegistry.ts new file mode 100644 index 0000000..0a86372 --- /dev/null +++ b/backend/src/core/ServiceRegistry.ts @@ -0,0 +1,19 @@ +export class ServiceRegistry { + private static instance: ServiceRegistry; + private services: Map = 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(name: string): T { + return this.services.get(name); + } +} diff --git a/backend/src/core/container.ts b/backend/src/core/container.ts new file mode 100644 index 0000000..cf12410 --- /dev/null +++ b/backend/src/core/container.ts @@ -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(TYPES.Cloud115Service).to(Cloud115Service); +container.bind(TYPES.QuarkService).to(QuarkService); +container.bind(TYPES.Searcher).to(Searcher); +container.bind(TYPES.DatabaseService).to(DatabaseService); + +export { container }; diff --git a/backend/src/core/types.ts b/backend/src/core/types.ts new file mode 100644 index 0000000..b33d413 --- /dev/null +++ b/backend/src/core/types.ts @@ -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"), +}; diff --git a/backend/src/interfaces/ICloudService.ts b/backend/src/interfaces/ICloudService.ts new file mode 100644 index 0000000..996c104 --- /dev/null +++ b/backend/src/interfaces/ICloudService.ts @@ -0,0 +1,12 @@ +import { + ShareInfoResponse, + FolderListResponse, + SaveFileParams, + SaveFileResponse, +} from "../types/cloud"; + +export interface ICloudService { + getShareInfo(shareCode: string, receiveCode?: string): Promise; + getFolderList(parentCid?: string): Promise; + saveSharedFile(params: SaveFileParams): Promise; +} diff --git a/backend/src/inversify.config.ts b/backend/src/inversify.config.ts new file mode 100644 index 0000000..41e6368 --- /dev/null +++ b/backend/src/inversify.config.ts @@ -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(TYPES.DatabaseService).to(DatabaseService).inSingletonScope(); +container.bind(TYPES.Cloud115Service).to(Cloud115Service).inSingletonScope(); +container.bind(TYPES.QuarkService).to(QuarkService).inSingletonScope(); +container.bind(TYPES.Searcher).to(Searcher).inSingletonScope(); +container.bind(TYPES.DoubanService).to(DoubanService).inSingletonScope(); +container.bind(TYPES.UserService).to(UserService).inSingletonScope(); + +// Controllers +container.bind(TYPES.Cloud115Controller).to(Cloud115Controller); +container.bind(TYPES.QuarkController).to(QuarkController); +container.bind(TYPES.ResourceController).to(ResourceController); +container.bind(TYPES.DoubanController).to(DoubanController); +container.bind(TYPES.ImageController).to(ImageController); +container.bind(TYPES.SettingController).to(SettingController); +container.bind(TYPES.UserController).to(UserController); + +export { container }; diff --git a/backend/src/middleware/cors.ts b/backend/src/middleware/cors.ts new file mode 100644 index 0000000..ec0b791 --- /dev/null +++ b/backend/src/middleware/cors.ts @@ -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(); + }; +}; diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts new file mode 100644 index 0000000..ca06926 --- /dev/null +++ b/backend/src/middleware/index.ts @@ -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); +}; diff --git a/backend/src/middleware/rateLimiter.ts b/backend/src/middleware/rateLimiter.ts new file mode 100644 index 0000000..27f742d --- /dev/null +++ b/backend/src/middleware/rateLimiter.ts @@ -0,0 +1,27 @@ +import { Request, Response, NextFunction } from "express"; + +const requestCounts = new Map(); +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(); + }; +}; diff --git a/backend/src/middleware/requestLogger.ts b/backend/src/middleware/requestLogger.ts new file mode 100644 index 0000000..4803b6e --- /dev/null +++ b/backend/src/middleware/requestLogger.ts @@ -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(); + }; +}; diff --git a/backend/src/routes/api.ts b/backend/src/routes/api.ts index 73367d4..d39ab75 100644 --- a/backend/src/routes/api.ts +++ b/backend/src/routes/api.ts @@ -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(TYPES.Cloud115Controller); +const quarkController = container.get(TYPES.QuarkController); +const resourceController = container.get(TYPES.ResourceController); +const doubanController = container.get(TYPES.DoubanController); +const imageController = container.get(TYPES.ImageController); +const settingController = container.get(TYPES.SettingController); +const userController = container.get(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; diff --git a/backend/src/services/Cloud115Service.ts b/backend/src/services/Cloud115Service.ts index 4b4543e..89ceff9 100644 --- a/backend/src/services/Cloud115Service.ts +++ b/backend/src/services/Cloud115Service.ts @@ -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 { + 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 { @@ -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); } } diff --git a/backend/src/services/DatabaseService.ts b/backend/src/services/DatabaseService.ts new file mode 100644 index 0000000..dbe01ac --- /dev/null +++ b/backend/src/services/DatabaseService.ts @@ -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 { + 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 { + 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}`); + } + } + } + + // ... 其他数据库相关方法 +} diff --git a/backend/src/services/DoubanService.ts b/backend/src/services/DoubanService.ts index 8f27a7a..3437af2 100644 --- a/backend/src/services/DoubanService.ts +++ b/backend/src/services/DoubanService.ts @@ -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; diff --git a/backend/src/services/ImageService.ts b/backend/src/services/ImageService.ts new file mode 100644 index 0000000..8c454e0 --- /dev/null +++ b/backend/src/services/ImageService.ts @@ -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 { + 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 { + if (!this.axiosInstance) { + throw new Error("Axios instance not initialized"); + } + return await this.axiosInstance.get(url, { responseType: "stream" }); + } +} diff --git a/backend/src/services/QuarkService.ts b/backend/src/services/QuarkService.ts index 2173bdf..cfce52c 100644 --- a/backend/src/services/QuarkService.ts +++ b/backend/src/services/QuarkService.ts @@ -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 { + 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); } } diff --git a/backend/src/services/Searcher.ts b/backend/src/services/Searcher.ts index 3dcca04..cd39921 100644 --- a/backend/src/services/Searcher.ts +++ b/backend/src/services/Searcher.ts @@ -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 { - 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 { + 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: "", diff --git a/backend/src/services/SettingService.ts b/backend/src/services/SettingService.ts new file mode 100644 index 0000000..b1f9cb9 --- /dev/null +++ b/backend/src/services/SettingService.ts @@ -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: "保存成功" }; + } +} diff --git a/backend/src/services/UserService.ts b/backend/src/services/UserService.ts new file mode 100644 index 0000000..fdb9989 --- /dev/null +++ b/backend/src/services/UserService.ts @@ -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, + }, + }; + } +} diff --git a/backend/src/types/cloud.ts b/backend/src/types/cloud.ts new file mode 100644 index 0000000..84776e6 --- /dev/null +++ b/backend/src/types/cloud.ts @@ -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; +} diff --git a/backend/src/types/index.ts b/backend/src/types/index.ts new file mode 100644 index 0000000..dd4bfd3 --- /dev/null +++ b/backend/src/types/index.ts @@ -0,0 +1,15 @@ +export interface Config { + app: { + port: number; + env: string; + }; + database: { + type: string; + path: string; + }; + jwt: { + secret: string; + expiresIn: string; + }; + // ... 其他配置类型 +} diff --git a/backend/src/types/services.ts b/backend/src/types/services.ts new file mode 100644 index 0000000..bcb6fcc --- /dev/null +++ b/backend/src/types/services.ts @@ -0,0 +1,9 @@ +import { Request } from "express"; +import { ShareInfoResponse } from "./cloud115"; + +export interface ICloudService { + setCookie(req: Request): Promise; + getShareInfo(shareCode: string, receiveCode?: string): Promise; + getFolderList(parentCid?: string): Promise; + saveSharedFile(params: any): Promise; +} diff --git a/backend/src/utils/logger.ts b/backend/src/utils/logger.ts index cca5e9a..9923eb7 100644 --- a/backend/src/utils/logger.ts +++ b/backend/src/utils/logger.ts @@ -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 }; diff --git a/backend/tsconfig.json b/backend/tsconfig.json index dd10083..3e0017f 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -15,7 +15,9 @@ "typeRoots": ["./node_modules/@types", "./src/types"], "paths": { "@/*": ["src/*"] - } + }, + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7a1916..a659008 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: express: specifier: ^4.18.3 version: 4.21.2 + inversify: + specifier: ^7.1.0 + version: 7.1.0(reflect-metadata@0.2.2) jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -80,6 +83,9 @@ importers: tunnel: specifier: ^0.0.6 version: 0.0.6 + winston: + specifier: ^3.17.0 + version: 3.17.0 devDependencies: '@types/bcrypt': specifier: ^5.0.2 @@ -673,6 +679,10 @@ packages: resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==} engines: {node: '>=6.9.0'} + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -681,6 +691,9 @@ packages: resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} engines: {node: '>=10'} + '@dabh/diagnostics@2.0.3': + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + '@element-plus/icons-vue@2.3.1': resolution: {integrity: sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==} peerDependencies: @@ -867,6 +880,25 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead + '@inversifyjs/common@1.5.0': + resolution: {integrity: sha512-Qj5BELk11AfI2rgZEAaLPmOftmQRLLmoCXgAjmaF0IngQN5vHomVT5ML7DZ3+CA5fgGcEVMcGbUDAun+Rz+oNg==} + + '@inversifyjs/container@1.5.4': + resolution: {integrity: sha512-mHAaWjAQb8m6TJksm5EJXW/kPcZFVEc1UKkWv5OnLbwbU0QvxM2UbEsuXzusGVHcrNY4TQp9Uh2wkRY6TN2WJg==} + peerDependencies: + reflect-metadata: ~0.2.2 + + '@inversifyjs/core@5.0.0': + resolution: {integrity: sha512-axOl+VZFGVA3nAMbs6RuHhQ8HvgO6/tKjlWJk4Nt0rUqed+1ksak4p5yZNtown1Kdm0GV2Oc57qLqqWd943hgA==} + + '@inversifyjs/prototype-utils@0.1.0': + resolution: {integrity: sha512-lNz1yyajMRDXBHLvJsYYX81FcmeD15e5Qz1zAZ/3zeYTl+u7ZF/GxNRKJzNOloeMPMtuR8BnvzHA1SZxjR+J9w==} + + '@inversifyjs/reflect-metadata-utils@1.1.0': + resolution: {integrity: sha512-jmuAuC3eL1GnFAYfJGJOMKRDL9q1mgzOyrban6zxfM8Yg1FUHsj25h27bW2G7p8X1Amvhg3MLkaOuogszkrofA==} + peerDependencies: + reflect-metadata: 0.2.2 + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1262,6 +1294,9 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -1682,10 +1717,19 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + + colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -1927,6 +1971,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -2134,6 +2181,9 @@ packages: picomatch: optional: true + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -2163,6 +2213,9 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + follow-redirects@1.15.9: resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} engines: {node: '>=4.0'} @@ -2414,6 +2467,11 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + inversify@7.1.0: + resolution: {integrity: sha512-f8SlUTgecMZGr/rsFK36PD84/mH0+sp0/P/TuiGo3CcJywmF5kgoXeE2RW5IjaIt1SlqS5c5V9RuQc1+B8mw4Q==} + peerDependencies: + reflect-metadata: ~0.2.2 + ip-address@9.0.5: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} @@ -2429,6 +2487,9 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} @@ -2631,6 +2692,9 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -2702,6 +2766,10 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2961,6 +3029,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3196,6 +3267,9 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -3292,6 +3366,10 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -3434,6 +3512,9 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + simple-update-notifier@2.0.0: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} @@ -3513,6 +3594,9 @@ packages: resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} engines: {node: '>= 8'} + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -3619,6 +3703,9 @@ packages: engines: {node: '>=10'} hasBin: true + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -3647,6 +3734,10 @@ packages: tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -3973,6 +4064,14 @@ packages: wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.17.0: + resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} + engines: {node: '>= 12.0.0'} + wkx@0.5.0: resolution: {integrity: sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==} @@ -4744,12 +4843,20 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@colors/colors@1.6.0': {} + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 '@ctrl/tinycolor@3.6.1': {} + '@dabh/diagnostics@2.0.3': + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + '@element-plus/icons-vue@2.3.1(vue@3.5.13(typescript@5.8.2))': dependencies: vue: 3.5.13(typescript@5.8.2) @@ -4872,6 +4979,31 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} + '@inversifyjs/common@1.5.0': {} + + '@inversifyjs/container@1.5.4(reflect-metadata@0.2.2)': + dependencies: + '@inversifyjs/common': 1.5.0 + '@inversifyjs/core': 5.0.0(reflect-metadata@0.2.2) + '@inversifyjs/reflect-metadata-utils': 1.1.0(reflect-metadata@0.2.2) + reflect-metadata: 0.2.2 + + '@inversifyjs/core@5.0.0(reflect-metadata@0.2.2)': + dependencies: + '@inversifyjs/common': 1.5.0 + '@inversifyjs/prototype-utils': 0.1.0 + '@inversifyjs/reflect-metadata-utils': 1.1.0(reflect-metadata@0.2.2) + transitivePeerDependencies: + - reflect-metadata + + '@inversifyjs/prototype-utils@0.1.0': + dependencies: + '@inversifyjs/common': 1.5.0 + + '@inversifyjs/reflect-metadata-utils@1.1.0(reflect-metadata@0.2.2)': + dependencies: + reflect-metadata: 0.2.2 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -5221,6 +5353,8 @@ snapshots: '@types/node': 20.17.23 '@types/send': 0.17.4 + '@types/triple-beam@1.3.5': {} + '@types/trusted-types@2.0.7': {} '@types/tunnel@0.0.7': @@ -5773,8 +5907,23 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + color-support@1.1.3: {} + color@3.2.1: + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + + colorspace@1.1.4: + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -5999,6 +6148,8 @@ snapshots: emoji-regex@9.2.2: {} + enabled@2.0.0: {} + encodeurl@1.0.2: {} encodeurl@2.0.0: {} @@ -6343,6 +6494,8 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fecha@4.2.3: {} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -6382,6 +6535,8 @@ snapshots: flatted@3.3.3: {} + fn.name@1.1.0: {} + follow-redirects@1.15.9: {} for-each@0.3.5: @@ -6658,6 +6813,13 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + inversify@7.1.0(reflect-metadata@0.2.2): + dependencies: + '@inversifyjs/common': 1.5.0 + '@inversifyjs/container': 1.5.4(reflect-metadata@0.2.2) + '@inversifyjs/core': 5.0.0(reflect-metadata@0.2.2) + reflect-metadata: 0.2.2 + ip-address@9.0.5: dependencies: jsbn: 1.1.0 @@ -6674,6 +6836,8 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.2: {} + is-async-function@2.1.1: dependencies: async-function: 1.0.0 @@ -6874,6 +7038,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kuler@2.0.0: {} + leven@3.1.0: {} levn@0.4.1: @@ -6935,6 +7101,15 @@ snapshots: lodash@4.17.21: {} + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -7219,6 +7394,10 @@ snapshots: dependencies: wrappy: 1.0.2 + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -7449,6 +7628,8 @@ snapshots: readdirp@4.1.2: {} + reflect-metadata@0.2.2: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -7580,6 +7761,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} sass@1.85.1: @@ -7736,6 +7919,10 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + simple-update-notifier@2.0.0: dependencies: semver: 7.7.1 @@ -7852,6 +8039,8 @@ snapshots: minipass: 3.3.6 optional: true + stack-trace@0.0.10: {} + statuses@2.0.1: {} string-width@4.2.3: @@ -7997,6 +8186,8 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + text-hex@1.0.0: {} + text-table@0.2.0: {} tinyglobby@0.2.12: @@ -8020,6 +8211,8 @@ snapshots: dependencies: punycode: 2.3.1 + triple-beam@1.4.1: {} + ts-api-utils@1.4.3(typescript@5.8.2): dependencies: typescript: 5.8.2 @@ -8373,6 +8566,26 @@ snapshots: dependencies: string-width: 4.2.3 + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.17.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + wkx@0.5.0: dependencies: '@types/node': 20.17.23 From 615149c83f0c3dba76cdb7595a302cc01fe59e09 Mon Sep 17 00:00:00 2001 From: jiangrui Date: Tue, 11 Mar 2025 00:06:10 +0800 Subject: [PATCH 03/22] Refactor the backend --- .gitignore | 1 + backend/src/app.ts | 4 +- backend/src/controllers/BaseController.ts | 13 +- backend/src/controllers/cloud115.ts | 39 +-- backend/src/controllers/quark.ts | 13 +- backend/src/controllers/resource.ts | 4 +- backend/src/controllers/setting.ts | 4 +- backend/src/controllers/user.ts | 4 +- backend/src/core/ApiResponse.ts | 4 +- backend/src/inversify.config.ts | 4 + backend/src/middleware/auth.ts | 3 +- backend/src/middleware/errorHandler.ts | 1 - backend/src/middleware/index.ts | 4 +- backend/src/middleware/rateLimiter.ts | 2 +- backend/src/services/Cloud115Service.ts | 2 +- backend/src/services/Searcher.ts | 2 +- backend/src/services/SettingService.ts | 4 +- backend/src/services/UserService.ts | 1 - backend/src/utils/logger.ts | 6 +- frontend/src/api/quark.ts | 4 +- frontend/src/stores/resource.ts | 17 +- package-lock.json | 286 +++++++++++++++++++++- 22 files changed, 338 insertions(+), 84 deletions(-) diff --git a/.gitignore b/.gitignore index 8c87e52..1d3f01a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ +logs/ dist/ .env .env.local diff --git a/backend/src/app.ts b/backend/src/app.ts index 6676cc1..dafccdd 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -7,7 +7,7 @@ import { DatabaseService } from "./services/DatabaseService"; import { setupMiddlewares } from "./middleware"; import routes from "./routes/api"; import { logger } from "./utils/logger"; - +import { errorHandler } from "./middleware/errorHandler"; class App { private app = express(); private databaseService = container.get(TYPES.DatabaseService); @@ -22,6 +22,8 @@ class App { // 设置路由 this.app.use("/", routes); + // 设置错误处理中间件 + this.app.use(errorHandler); } public async start(): Promise { diff --git a/backend/src/controllers/BaseController.ts b/backend/src/controllers/BaseController.ts index af3fbb9..b40cbdf 100644 --- a/backend/src/controllers/BaseController.ts +++ b/backend/src/controllers/BaseController.ts @@ -1,17 +1,22 @@ import { Request, Response } from "express"; import { ApiResponse } from "../core/ApiResponse"; +interface ApiResponseData { + data?: T; + message?: string; +} export abstract class BaseController { protected async handleRequest( req: Request, res: Response, - action: () => Promise + action: () => Promise> ): Promise { try { const result = await action(); - res.json(ApiResponse.success(result)); - } catch (error: any) { - res.status(500).json(ApiResponse.error(error?.message || "未知错误")); + res.json(ApiResponse.success(result.data, result.message)); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : "未知错误"; + res.status(200).json(ApiResponse.error(errorMessage)); } } } diff --git a/backend/src/controllers/cloud115.ts b/backend/src/controllers/cloud115.ts index e63c356..7ff5c14 100644 --- a/backend/src/controllers/cloud115.ts +++ b/backend/src/controllers/cloud115.ts @@ -1,24 +1,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 => { - const userId = req.user?.userId; - const userSetting = await UserSetting.findOne({ - where: { userId }, - }); - if (userSetting && userSetting.dataValues.cloud115Cookie) { - cloud115.setCookie(userSetting.dataValues.cloud115Cookie); - } else { - throw new Error("请先设置115网盘cookie"); - } -}; - @injectable() export class Cloud115Controller extends BaseController { constructor(@inject(TYPES.Cloud115Service) private cloud115Service: Cloud115Service) { @@ -34,31 +19,23 @@ export class Cloud115Controller extends BaseController { } async getFolderList(req: Request, res: Response): Promise { - try { + await this.handleRequest(req, res, async () => { const { parentCid } = req.query; - await setCookie(req); - const result = await cloud115.getFolderList(parentCid as string); - sendSuccess(res, result); - } catch (error) { - sendError(res, { message: (error as Error).message || "获取目录列表失败" }); - } + await this.cloud115Service.setCookie(req); + return await this.cloud115Service.getFolderList(parentCid as string); + }); } async saveFile(req: Request, res: Response): Promise { - try { + await this.handleRequest(req, res, async () => { const { shareCode, receiveCode, fileId, folderId } = req.body; - await setCookie(req); - const result = await cloud115.saveSharedFile({ + await this.cloud115Service.setCookie(req); + return await this.cloud115Service.saveSharedFile({ shareCode, receiveCode, fileId, cid: folderId, }); - sendSuccess(res, result); - } catch (error) { - sendError(res, { message: (error as Error).message || "保存文件失败" }); - } + }); } } - -export const Cloud115ServiceInstance = cloud115; diff --git a/backend/src/controllers/quark.ts b/backend/src/controllers/quark.ts index afa390e..ec57fa0 100644 --- a/backend/src/controllers/quark.ts +++ b/backend/src/controllers/quark.ts @@ -3,7 +3,6 @@ import { injectable, inject } from "inversify"; import { TYPES } from "../core/types"; import { QuarkService } from "../services/QuarkService"; import { BaseController } from "./BaseController"; -import { sendSuccess } from "../utils/response"; @injectable() export class QuarkController extends BaseController { @@ -15,11 +14,7 @@ export class QuarkController extends BaseController { 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); + return await this.quarkService.getShareInfo(shareCode as string, receiveCode as string); }); } @@ -27,16 +22,14 @@ export class QuarkController extends BaseController { await this.handleRequest(req, res, async () => { const { parentCid } = req.query; await this.quarkService.setCookie(req); - const result = await this.quarkService.getFolderList(parentCid as string); - sendSuccess(res, result); + return await this.quarkService.getFolderList(parentCid as string); }); } async saveFile(req: Request, res: Response): Promise { await this.handleRequest(req, res, async () => { await this.quarkService.setCookie(req); - const result = await this.quarkService.saveSharedFile(req.body); - sendSuccess(res, result); + return await this.quarkService.saveSharedFile(req.body); }); } } diff --git a/backend/src/controllers/resource.ts b/backend/src/controllers/resource.ts index c8635a0..59faeed 100644 --- a/backend/src/controllers/resource.ts +++ b/backend/src/controllers/resource.ts @@ -3,7 +3,6 @@ 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 { @@ -14,12 +13,11 @@ export class ResourceController extends BaseController { async search(req: Request, res: Response): Promise { await this.handleRequest(req, res, async () => { const { keyword, channelId = "", lastMessageId = "" } = req.query; - const result = await this.searcher.searchAll( + return await this.searcher.searchAll( keyword as string, channelId as string, lastMessageId as string ); - sendSuccess(res, result); }); } } diff --git a/backend/src/controllers/setting.ts b/backend/src/controllers/setting.ts index f41b642..722a00d 100644 --- a/backend/src/controllers/setting.ts +++ b/backend/src/controllers/setting.ts @@ -12,7 +12,7 @@ export class SettingController extends BaseController { async get(req: Request, res: Response): Promise { await this.handleRequest(req, res, async () => { - const userId = Number(req.user?.userId); + const userId = req.user?.userId; const role = Number(req.user?.role); return await this.settingService.getSettings(userId, role); }); @@ -20,7 +20,7 @@ export class SettingController extends BaseController { async save(req: Request, res: Response): Promise { await this.handleRequest(req, res, async () => { - const userId = Number(req.user?.userId); + const userId = req.user?.userId; const role = Number(req.user?.role); return await this.settingService.saveSettings(userId, role, req.body); }); diff --git a/backend/src/controllers/user.ts b/backend/src/controllers/user.ts index 27bf0a5..f2ca4f3 100644 --- a/backend/src/controllers/user.ts +++ b/backend/src/controllers/user.ts @@ -12,8 +12,8 @@ export class UserController extends BaseController { async register(req: Request, res: Response): Promise { await this.handleRequest(req, res, async () => { - const { username, password, code } = req.body; - return await this.userService.register(username, password, code); + const { username, password, registerCode } = req.body; + return await this.userService.register(username, password, registerCode); }); } diff --git a/backend/src/core/ApiResponse.ts b/backend/src/core/ApiResponse.ts index a8d2c63..8bd467f 100644 --- a/backend/src/core/ApiResponse.ts +++ b/backend/src/core/ApiResponse.ts @@ -12,10 +12,10 @@ export class ApiResponse { } static success(data?: T, message = "操作成功"): ApiResponse { - return new ApiResponse(true, 200, data, message); + return new ApiResponse(true, 0, data, message); } - static error(message: string, code = 500): ApiResponse { + static error(message: string, code = 10000): ApiResponse { return new ApiResponse(false, code, null, message); } } diff --git a/backend/src/inversify.config.ts b/backend/src/inversify.config.ts index 41e6368..57db955 100644 --- a/backend/src/inversify.config.ts +++ b/backend/src/inversify.config.ts @@ -8,6 +8,8 @@ import { QuarkService } from "./services/QuarkService"; import { Searcher } from "./services/Searcher"; import { DoubanService } from "./services/DoubanService"; import { UserService } from "./services/UserService"; +import { ImageService } from "./services/ImageService"; +import { SettingService } from "./services/SettingService"; // Controllers import { Cloud115Controller } from "./controllers/cloud115"; @@ -25,6 +27,8 @@ container.bind(TYPES.DatabaseService).to(DatabaseService).inSin container.bind(TYPES.Cloud115Service).to(Cloud115Service).inSingletonScope(); container.bind(TYPES.QuarkService).to(QuarkService).inSingletonScope(); container.bind(TYPES.Searcher).to(Searcher).inSingletonScope(); +container.bind(TYPES.ImageService).to(ImageService).inSingletonScope(); +container.bind(TYPES.SettingService).to(SettingService).inSingletonScope(); container.bind(TYPES.DoubanService).to(DoubanService).inSingletonScope(); container.bind(TYPES.UserService).to(UserService).inSingletonScope(); diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index 6a41518..0511ef7 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -16,7 +16,8 @@ export const authMiddleware = async ( res: Response, next: NextFunction ): Promise => { - if (req.path === "/user/login" || req.path === "/user/register") { + console.log(req.path); + if (req.path === "/user/login" || req.path === "/user/register" || req.path === "/tele-images/") { return next(); } diff --git a/backend/src/middleware/errorHandler.ts b/backend/src/middleware/errorHandler.ts index c7ec51e..d7efa7b 100644 --- a/backend/src/middleware/errorHandler.ts +++ b/backend/src/middleware/errorHandler.ts @@ -5,7 +5,6 @@ interface CustomError extends Error { } export const errorHandler = (err: CustomError, req: Request, res: Response): void => { - console.error(err); res.status(err.status || 500).json({ success: false, error: err.message || "服务器内部错误", diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index ca06926..2bda0be 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -1,14 +1,14 @@ import { Application } from "express"; -import { errorHandler } from "./errorHandler"; +import express from "express"; import { authMiddleware } from "./auth"; import { requestLogger } from "./requestLogger"; import { rateLimiter } from "./rateLimiter"; import { cors } from "./cors"; export const setupMiddlewares = (app: Application) => { + app.use(express.json()); app.use(cors()); app.use(requestLogger()); app.use(rateLimiter()); app.use(authMiddleware); - app.use(errorHandler); }; diff --git a/backend/src/middleware/rateLimiter.ts b/backend/src/middleware/rateLimiter.ts index 27f742d..da8be8e 100644 --- a/backend/src/middleware/rateLimiter.ts +++ b/backend/src/middleware/rateLimiter.ts @@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from "express"; const requestCounts = new Map(); const WINDOW_MS = 60 * 1000; // 1分钟窗口 -const MAX_REQUESTS = 60; // 每个IP每分钟最多60个请求 +const MAX_REQUESTS = 600; // 每个IP每分钟最多60个请求 export const rateLimiter = () => { return (req: Request, res: Response, next: NextFunction) => { diff --git a/backend/src/services/Cloud115Service.ts b/backend/src/services/Cloud115Service.ts index 89ceff9..53a06c8 100644 --- a/backend/src/services/Cloud115Service.ts +++ b/backend/src/services/Cloud115Service.ts @@ -5,7 +5,7 @@ import { injectable } from "inversify"; import { Request } from "express"; import UserSetting from "../models/UserSetting"; import { ICloudService } from "../types/services"; -import { logger } from "@/utils/logger"; +import { logger } from "../utils/logger"; interface Cloud115ListItem { cid: string; diff --git a/backend/src/services/Searcher.ts b/backend/src/services/Searcher.ts index cd39921..e99aa67 100644 --- a/backend/src/services/Searcher.ts +++ b/backend/src/services/Searcher.ts @@ -24,7 +24,7 @@ interface sourceItem { @injectable() export class Searcher { private static instance: Searcher; - private api: AxiosInstance; + private api: AxiosInstance | null = null; constructor() { this.initAxiosInstance(); diff --git a/backend/src/services/SettingService.ts b/backend/src/services/SettingService.ts index b1f9cb9..a30e1e8 100644 --- a/backend/src/services/SettingService.ts +++ b/backend/src/services/SettingService.ts @@ -5,7 +5,7 @@ import { Searcher } from "./Searcher"; @injectable() export class SettingService { - async getSettings(userId: number | undefined, role: number | undefined) { + async getSettings(userId: string | undefined, role: number | undefined) { if (!userId) { throw new Error("用户ID无效"); } @@ -28,7 +28,7 @@ export class SettingService { }; } - async saveSettings(userId: number | undefined, role: number | undefined, settings: any) { + async saveSettings(userId: string | undefined, role: number | undefined, settings: any) { if (!userId) { throw new Error("用户ID无效"); } diff --git a/backend/src/services/UserService.ts b/backend/src/services/UserService.ts index fdb9989..06c0eb5 100644 --- a/backend/src/services/UserService.ts +++ b/backend/src/services/UserService.ts @@ -19,7 +19,6 @@ export class UserService { globalSetting?.dataValues.CommonUserCode, globalSetting?.dataValues.AdminUserCode, ]; - if (!registerCode || !registerCodeList.includes(Number(registerCode))) { throw new Error("注册码错误"); } diff --git a/backend/src/utils/logger.ts b/backend/src/utils/logger.ts index 9923eb7..1770bdd 100644 --- a/backend/src/utils/logger.ts +++ b/backend/src/utils/logger.ts @@ -1,8 +1,8 @@ import winston from "winston"; -import { Config } from "../config"; +import { config } from "../config"; const logger = winston.createLogger({ - level: Config.app.env === "development" ? "debug" : "info", + 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" }), @@ -10,7 +10,7 @@ const logger = winston.createLogger({ ], }); -if (Config.app.env !== "production") { +if (config.app.env !== "production") { logger.add( new winston.transports.Console({ format: winston.format.simple(), diff --git a/frontend/src/api/quark.ts b/frontend/src/api/quark.ts index 68bebb5..2e7bb52 100644 --- a/frontend/src/api/quark.ts +++ b/frontend/src/api/quark.ts @@ -2,9 +2,9 @@ import request from "@/utils/request"; import type { ShareInfoResponse, Folder, SaveQuarkFileParams } from "@/types"; export const quarkApi = { - async getShareInfo(pwdId: string, passcode = "") { + async getShareInfo(shareCode: string, receiveCode = "") { const { data } = await request.get("/api/quark/share-info", { - params: { pwdId, passcode }, + params: { shareCode, receiveCode }, }); return data as ShareInfoResponse; }, diff --git a/frontend/src/stores/resource.ts b/frontend/src/stores/resource.ts index 4c5df53..c7b0ee9 100644 --- a/frontend/src/stores/resource.ts +++ b/frontend/src/stores/resource.ts @@ -42,7 +42,7 @@ interface CloudDriveConfig< // 云盘类型配置 export const CLOUD_DRIVES: [ CloudDriveConfig<{ shareCode: string; receiveCode: string }, Save115FileParams>, - CloudDriveConfig<{ pwdId: string }, SaveQuarkFileParams>, + CloudDriveConfig<{ shareCode: string }, SaveQuarkFileParams>, ] = [ { name: "115网盘", @@ -71,19 +71,20 @@ export const CLOUD_DRIVES: [ type: "quark", regex: /pan\.quark\.cn\/s\/([a-zA-Z0-9]+)/, api: { - getShareInfo: (parsedCode: { pwdId: string }) => quarkApi.getShareInfo(parsedCode.pwdId), + getShareInfo: (parsedCode: { shareCode: string }) => + quarkApi.getShareInfo(parsedCode.shareCode), saveFile: async (params: SaveQuarkFileParams) => { return await quarkApi.saveFile(params as SaveQuarkFileParams); }, }, - parseShareCode: (match: RegExpMatchArray) => ({ pwdId: match[1] }), + parseShareCode: (match: RegExpMatchArray) => ({ shareCode: match[1] }), getSaveParams: (shareInfo: ShareInfoResponse, folderId: string) => ({ fid_list: shareInfo.list.map((item: { fileId?: string }) => item.fileId || ""), fid_token_list: shareInfo.list.map( (item: { fileIdToken?: string }) => item.fileIdToken || "" ), to_pdir_fid: folderId, - pwd_id: shareInfo.pwdId || "", + pwd_id: shareInfo.shareCode || "", stoken: shareInfo.stoken || "", pdir_fid: "0", scene: "link", @@ -190,7 +191,7 @@ export const useResourceStore = defineStore("resource", { folderId: string, drive: | CloudDriveConfig<{ shareCode: string; receiveCode: string }, Save115FileParams> - | CloudDriveConfig<{ pwdId: string }, SaveQuarkFileParams> + | CloudDriveConfig<{ shareCode: string }, SaveQuarkFileParams> ): Promise { const link = resource.cloudLinks.find((link) => drive.regex.test(link)); if (!link) return; @@ -240,7 +241,7 @@ export const useResourceStore = defineStore("resource", { ? await matchedDrive.api.getShareInfo( parsedCode as { shareCode: string; receiveCode: string } ) - : await matchedDrive.api.getShareInfo(parsedCode as { pwdId: string }); + : await matchedDrive.api.getShareInfo(parsedCode as { shareCode: string }); if (Array.isArray(shareInfo)) { shareInfo = { @@ -304,7 +305,7 @@ export const useResourceStore = defineStore("resource", { } else { shareInfo = this.is115Drive(drive) ? await drive.api.getShareInfo(parsedCode as { shareCode: string; receiveCode: string }) - : await drive.api.getShareInfo(parsedCode as { pwdId: string }); + : await drive.api.getShareInfo(parsedCode as { shareCode: string }); } this.setLoadTree(false); if (shareInfo) { @@ -337,7 +338,7 @@ export const useResourceStore = defineStore("resource", { is115Drive( drive: | CloudDriveConfig<{ shareCode: string; receiveCode: string }, Save115FileParams> - | CloudDriveConfig<{ pwdId: string }, SaveQuarkFileParams> + | CloudDriveConfig<{ shareCode: string }, SaveQuarkFileParams> ): drive is CloudDriveConfig<{ shareCode: string; receiveCode: string }, Save115FileParams> { return drive.type === "pan115"; }, diff --git a/package-lock.json b/package-lock.json index 4d4e2b5..8d1ebe6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cloud-saver", - "version": "0.2.2", + "version": "0.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cloud-saver", - "version": "0.2.2", + "version": "0.2.3", "workspaces": [ "frontend", "backend" @@ -38,12 +38,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", @@ -1718,6 +1720,15 @@ "node": ">=6.9.0" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmmirror.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1740,6 +1751,17 @@ "node": ">=10" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@element-plus/icons-vue": { "version": "2.3.1", "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz", @@ -2321,6 +2343,55 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@inversifyjs/common": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@inversifyjs/common/-/common-1.5.0.tgz", + "integrity": "sha512-Qj5BELk11AfI2rgZEAaLPmOftmQRLLmoCXgAjmaF0IngQN5vHomVT5ML7DZ3+CA5fgGcEVMcGbUDAun+Rz+oNg==", + "license": "MIT" + }, + "node_modules/@inversifyjs/container": { + "version": "1.5.4", + "resolved": "https://registry.npmmirror.com/@inversifyjs/container/-/container-1.5.4.tgz", + "integrity": "sha512-mHAaWjAQb8m6TJksm5EJXW/kPcZFVEc1UKkWv5OnLbwbU0QvxM2UbEsuXzusGVHcrNY4TQp9Uh2wkRY6TN2WJg==", + "license": "MIT", + "dependencies": { + "@inversifyjs/common": "1.5.0", + "@inversifyjs/core": "5.0.0", + "@inversifyjs/reflect-metadata-utils": "1.1.0" + }, + "peerDependencies": { + "reflect-metadata": "~0.2.2" + } + }, + "node_modules/@inversifyjs/core": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/@inversifyjs/core/-/core-5.0.0.tgz", + "integrity": "sha512-axOl+VZFGVA3nAMbs6RuHhQ8HvgO6/tKjlWJk4Nt0rUqed+1ksak4p5yZNtown1Kdm0GV2Oc57qLqqWd943hgA==", + "license": "MIT", + "dependencies": { + "@inversifyjs/common": "1.5.0", + "@inversifyjs/prototype-utils": "0.1.0", + "@inversifyjs/reflect-metadata-utils": "1.1.0" + } + }, + "node_modules/@inversifyjs/prototype-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/@inversifyjs/prototype-utils/-/prototype-utils-0.1.0.tgz", + "integrity": "sha512-lNz1yyajMRDXBHLvJsYYX81FcmeD15e5Qz1zAZ/3zeYTl+u7ZF/GxNRKJzNOloeMPMtuR8BnvzHA1SZxjR+J9w==", + "license": "MIT", + "dependencies": { + "@inversifyjs/common": "1.5.0" + } + }, + "node_modules/@inversifyjs/reflect-metadata-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@inversifyjs/reflect-metadata-utils/-/reflect-metadata-utils-1.1.0.tgz", + "integrity": "sha512-jmuAuC3eL1GnFAYfJGJOMKRDL9q1mgzOyrban6zxfM8Yg1FUHsj25h27bW2G7p8X1Amvhg3MLkaOuogszkrofA==", + "license": "MIT", + "peerDependencies": { + "reflect-metadata": "0.2.2" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3634,6 +3705,12 @@ "@types/send": "*" } }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmmirror.com/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -4356,7 +4433,6 @@ "version": "3.2.6", "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, "license": "MIT" }, "node_modules/async-function": { @@ -5033,6 +5109,16 @@ "resolved": "frontend", "link": true }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", @@ -5050,9 +5136,18 @@ "version": "1.1.4", "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmmirror.com/color-support/-/color-support-1.1.3.tgz", @@ -5062,6 +5157,31 @@ "color-support": "bin.js" } }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5677,6 +5797,12 @@ "dev": true, "license": "MIT" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", @@ -6458,6 +6584,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -6648,6 +6780,12 @@ "dev": true, "license": "ISC" }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -7428,6 +7566,20 @@ "node": ">= 0.4" } }, + "node_modules/inversify": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/inversify/-/inversify-7.1.0.tgz", + "integrity": "sha512-f8SlUTgecMZGr/rsFK36PD84/mH0+sp0/P/TuiGo3CcJywmF5kgoXeE2RW5IjaIt1SlqS5c5V9RuQc1+B8mw4Q==", + "license": "MIT", + "dependencies": { + "@inversifyjs/common": "1.5.0", + "@inversifyjs/container": "1.5.4", + "@inversifyjs/core": "5.0.0" + }, + "peerDependencies": { + "reflect-metadata": "~0.2.2" + } + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-9.0.5.tgz", @@ -7809,7 +7961,6 @@ "version": "2.0.1", "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8152,6 +8303,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz", @@ -8311,6 +8468,23 @@ "dev": true, "license": "MIT" }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", @@ -9483,6 +9657,15 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", @@ -10197,6 +10380,13 @@ "node": ">=8.10.0" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0", + "peer": true + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -10570,6 +10760,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -11023,6 +11222,21 @@ "simple-concat": "^1.0.0" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -11397,6 +11611,15 @@ "node": ">=8" } }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmmirror.com/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", @@ -11842,6 +12065,12 @@ "node": ">=10" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz", @@ -11938,6 +12167,15 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -12999,6 +13237,42 @@ "node": ">=8" } }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmmirror.com/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmmirror.com/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/wkx": { "version": "0.5.0", "resolved": "https://registry.npmmirror.com/wkx/-/wkx-0.5.0.tgz", From 37c25a930733909a1c97f24e059945e3c65fd624 Mon Sep 17 00:00:00 2001 From: jiangrui Date: Tue, 11 Mar 2025 17:42:59 +0800 Subject: [PATCH 04/22] Refactor the backend --- .../src/controllers/BaseCloudController.ts | 32 ++++ backend/src/controllers/BaseController.ts | 6 +- backend/src/controllers/cloud115.ts | 38 +---- backend/src/controllers/quark.ts | 31 +--- backend/src/controllers/teleImages.ts | 15 +- backend/src/interfaces/ICloudService.ts | 12 -- backend/src/middleware/auth.ts | 1 - backend/src/middleware/requestLogger.ts | 5 + backend/src/routes/setting.ts | 9 -- backend/src/routes/user.ts | 10 -- backend/src/services/Cloud115Service.ts | 46 +++--- backend/src/services/DatabaseService.ts | 32 +++- backend/src/services/ImageService.ts | 70 ++++++--- backend/src/services/QuarkService.ts | 44 +++--- backend/src/services/Searcher.ts | 13 +- backend/src/services/SettingService.ts | 19 ++- backend/src/types/cloud.ts | 72 ++++++++- backend/src/types/services.ts | 8 +- frontend/components.d.ts | 21 --- frontend/src/api/cloud115.ts | 8 +- frontend/src/api/quark.ts | 8 +- frontend/src/stores/resource.ts | 140 ++++++------------ frontend/src/types/index.ts | 30 +++- 23 files changed, 347 insertions(+), 323 deletions(-) create mode 100644 backend/src/controllers/BaseCloudController.ts delete mode 100644 backend/src/interfaces/ICloudService.ts delete mode 100644 backend/src/routes/setting.ts delete mode 100644 backend/src/routes/user.ts diff --git a/backend/src/controllers/BaseCloudController.ts b/backend/src/controllers/BaseCloudController.ts new file mode 100644 index 0000000..21d7ad6 --- /dev/null +++ b/backend/src/controllers/BaseCloudController.ts @@ -0,0 +1,32 @@ +import { Request, Response } from "express"; +import { BaseController } from "./BaseController"; +import { ICloudStorageService } from "@/types/services"; + +export abstract class BaseCloudController extends BaseController { + constructor(protected cloudService: ICloudStorageService) { + super(); + } + + async getShareInfo(req: Request, res: Response): Promise { + await this.handleRequest(req, res, async () => { + const { shareCode, receiveCode } = req.query; + await this.cloudService.setCookie(req); + return await this.cloudService.getShareInfo(shareCode as string, receiveCode as string); + }); + } + + async getFolderList(req: Request, res: Response): Promise { + await this.handleRequest(req, res, async () => { + const { parentCid } = req.query; + await this.cloudService.setCookie(req); + return await this.cloudService.getFolderList(parentCid as string); + }); + } + + async saveFile(req: Request, res: Response): Promise { + await this.handleRequest(req, res, async () => { + await this.cloudService.setCookie(req); + return await this.cloudService.saveSharedFile(req.body); + }); + } +} diff --git a/backend/src/controllers/BaseController.ts b/backend/src/controllers/BaseController.ts index b40cbdf..c17d954 100644 --- a/backend/src/controllers/BaseController.ts +++ b/backend/src/controllers/BaseController.ts @@ -9,11 +9,13 @@ export abstract class BaseController { protected async handleRequest( req: Request, res: Response, - action: () => Promise> + action: () => Promise | void> ): Promise { try { const result = await action(); - res.json(ApiResponse.success(result.data, result.message)); + if (result) { + res.json(ApiResponse.success(result.data, result.message)); + } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "未知错误"; res.status(200).json(ApiResponse.error(errorMessage)); diff --git a/backend/src/controllers/cloud115.ts b/backend/src/controllers/cloud115.ts index 7ff5c14..0b39e99 100644 --- a/backend/src/controllers/cloud115.ts +++ b/backend/src/controllers/cloud115.ts @@ -1,41 +1,11 @@ -import { Request, Response } from "express"; import { Cloud115Service } from "../services/Cloud115Service"; -import { BaseController } from "./BaseController"; import { injectable, inject } from "inversify"; import { TYPES } from "../core/types"; +import { BaseCloudController } from "./BaseCloudController"; @injectable() -export class Cloud115Controller extends BaseController { - constructor(@inject(TYPES.Cloud115Service) private cloud115Service: Cloud115Service) { - super(); - } - - async getShareInfo(req: Request, res: Response): Promise { - await this.handleRequest(req, res, async () => { - const { shareCode, receiveCode } = req.query; - await this.cloud115Service.setCookie(req); - return await this.cloud115Service.getShareInfo(shareCode as string, receiveCode as string); - }); - } - - async getFolderList(req: Request, res: Response): Promise { - await this.handleRequest(req, res, async () => { - const { parentCid } = req.query; - await this.cloud115Service.setCookie(req); - return await this.cloud115Service.getFolderList(parentCid as string); - }); - } - - async saveFile(req: Request, res: Response): Promise { - await this.handleRequest(req, res, async () => { - const { shareCode, receiveCode, fileId, folderId } = req.body; - await this.cloud115Service.setCookie(req); - return await this.cloud115Service.saveSharedFile({ - shareCode, - receiveCode, - fileId, - cid: folderId, - }); - }); +export class Cloud115Controller extends BaseCloudController { + constructor(@inject(TYPES.Cloud115Service) cloud115Service: Cloud115Service) { + super(cloud115Service); } } diff --git a/backend/src/controllers/quark.ts b/backend/src/controllers/quark.ts index ec57fa0..4ef6ddb 100644 --- a/backend/src/controllers/quark.ts +++ b/backend/src/controllers/quark.ts @@ -2,34 +2,11 @@ import { Request, Response } from "express"; import { injectable, inject } from "inversify"; import { TYPES } from "../core/types"; import { QuarkService } from "../services/QuarkService"; -import { BaseController } from "./BaseController"; +import { BaseCloudController } from "./BaseCloudController"; @injectable() -export class QuarkController extends BaseController { - constructor(@inject(TYPES.QuarkService) private quarkService: QuarkService) { - super(); - } - - async getShareInfo(req: Request, res: Response): Promise { - await this.handleRequest(req, res, async () => { - const { shareCode, receiveCode } = req.query; - await this.quarkService.setCookie(req); - return await this.quarkService.getShareInfo(shareCode as string, receiveCode as string); - }); - } - - async getFolderList(req: Request, res: Response): Promise { - await this.handleRequest(req, res, async () => { - const { parentCid } = req.query; - await this.quarkService.setCookie(req); - return await this.quarkService.getFolderList(parentCid as string); - }); - } - - async saveFile(req: Request, res: Response): Promise { - await this.handleRequest(req, res, async () => { - await this.quarkService.setCookie(req); - return await this.quarkService.saveSharedFile(req.body); - }); +export class QuarkController extends BaseCloudController { + constructor(@inject(TYPES.QuarkService) quarkService: QuarkService) { + super(quarkService); } } diff --git a/backend/src/controllers/teleImages.ts b/backend/src/controllers/teleImages.ts index f838631..450503a 100644 --- a/backend/src/controllers/teleImages.ts +++ b/backend/src/controllers/teleImages.ts @@ -12,8 +12,19 @@ export class ImageController extends BaseController { async getImages(req: Request, res: Response): Promise { await this.handleRequest(req, res, async () => { - const url = req.query.url as string; - return await this.imageService.getImages(url); + const url = decodeURIComponent((req.query.url as string) || ""); + const response = await this.imageService.getImages(url); + + // 设置正确的响应头 + res.setHeader("Content-Type", response.headers["content-type"]); + res.setHeader("Cache-Control", "no-cache"); + + // 确保清除任何可能导致304响应的头信息 + res.removeHeader("etag"); + res.removeHeader("last-modified"); + + // 直接传输图片数据 + response.data.pipe(res); }); } } diff --git a/backend/src/interfaces/ICloudService.ts b/backend/src/interfaces/ICloudService.ts deleted file mode 100644 index 996c104..0000000 --- a/backend/src/interfaces/ICloudService.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { - ShareInfoResponse, - FolderListResponse, - SaveFileParams, - SaveFileResponse, -} from "../types/cloud"; - -export interface ICloudService { - getShareInfo(shareCode: string, receiveCode?: string): Promise; - getFolderList(parentCid?: string): Promise; - saveSharedFile(params: SaveFileParams): Promise; -} diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index 0511ef7..684707a 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -16,7 +16,6 @@ export const authMiddleware = async ( res: Response, next: NextFunction ): Promise => { - console.log(req.path); if (req.path === "/user/login" || req.path === "/user/register" || req.path === "/tele-images/") { return next(); } diff --git a/backend/src/middleware/requestLogger.ts b/backend/src/middleware/requestLogger.ts index 4803b6e..4e850b3 100644 --- a/backend/src/middleware/requestLogger.ts +++ b/backend/src/middleware/requestLogger.ts @@ -1,10 +1,15 @@ import { Request, Response, NextFunction } from "express"; import { logger } from "../utils/logger"; +const excludePaths = ["/tele-images/"]; + export const requestLogger = () => { return (req: Request, res: Response, next: NextFunction) => { const start = Date.now(); res.on("finish", () => { + if (excludePaths.includes(req.path)) { + return; + } const duration = Date.now() - start; logger.info({ method: req.method, diff --git a/backend/src/routes/setting.ts b/backend/src/routes/setting.ts deleted file mode 100644 index 474c116..0000000 --- a/backend/src/routes/setting.ts +++ /dev/null @@ -1,9 +0,0 @@ -import express from "express"; -import { settingController } from "../controllers/setting"; - -const router = express.Router(); - -router.get("/get", settingController.get); -router.post("/save", settingController.save); - -export default router; diff --git a/backend/src/routes/user.ts b/backend/src/routes/user.ts deleted file mode 100644 index f0dbc76..0000000 --- a/backend/src/routes/user.ts +++ /dev/null @@ -1,10 +0,0 @@ -// backend/src/routes/user.ts -import express from "express"; -import { userController } from "../controllers/user"; - -const router = express.Router(); - -router.post("/register", userController.register); -router.post("/login", userController.login); - -export default router; diff --git a/backend/src/services/Cloud115Service.ts b/backend/src/services/Cloud115Service.ts index 53a06c8..d4245e4 100644 --- a/backend/src/services/Cloud115Service.ts +++ b/backend/src/services/Cloud115Service.ts @@ -1,10 +1,10 @@ import { AxiosHeaders, AxiosInstance } from "axios"; // 导入 AxiosHeaders import { createAxiosInstance } from "../utils/axiosInstance"; -import { ShareInfoResponse } from "../types/cloud115"; +import { ShareInfoResponse, FolderListResponse, SaveFileParams } from "../types/cloud"; import { injectable } from "inversify"; import { Request } from "express"; import UserSetting from "../models/UserSetting"; -import { ICloudService } from "../types/services"; +import { ICloudStorageService } from "@/types/services"; import { logger } from "../utils/logger"; interface Cloud115ListItem { @@ -19,13 +19,8 @@ interface Cloud115FolderItem { ns: number; } -interface Cloud115PathItem { - cid: string; - name: string; -} - @injectable() -export class Cloud115Service implements ICloudService { +export class Cloud115Service implements ICloudStorageService { private api: AxiosInstance; private cookie: string = ""; @@ -80,19 +75,21 @@ export class Cloud115Service implements ICloudService { }); if (response.data?.state && response.data.data?.list?.length > 0) { return { - data: response.data.data.list.map((item: Cloud115ListItem) => ({ - fileId: item.cid, - fileName: item.n, - fileSize: item.s, - })), + data: { + list: response.data.data.list.map((item: Cloud115ListItem) => ({ + fileId: item.cid, + fileName: item.n, + fileSize: item.s, + })), + }, }; + } else { + logger.error("未找到文件信息:", response.data); + throw new Error("未找到文件信息"); } - throw new Error("未找到文件信息"); } - async getFolderList( - parentCid = "0" - ): Promise<{ data: { cid: string; name: string; path: Cloud115PathItem[] }[] }> { + async getFolderList(parentCid = "0"): Promise { const response = await this.api.get("/files", { params: { aid: 1, @@ -128,17 +125,12 @@ export class Cloud115Service implements ICloudService { } } - async saveSharedFile(params: { - cid: string; - shareCode: string; - receiveCode: string; - fileId: string; - }): Promise<{ message: string; data: unknown }> { + async saveSharedFile(params: SaveFileParams): Promise<{ message: string; data: unknown }> { const param = new URLSearchParams({ - cid: params.cid, - share_code: params.shareCode, - receive_code: params.receiveCode, - file_id: params.fileId, + cid: params.folderId || "", + share_code: params.shareCode || "", + receive_code: params.receiveCode || "", + file_id: params.fids?.[0] || "", }); const response = await this.api.post("/share/receive", param.toString()); logger.info("保存文件:", response.data); diff --git a/backend/src/services/DatabaseService.ts b/backend/src/services/DatabaseService.ts index dbe01ac..ec447b9 100644 --- a/backend/src/services/DatabaseService.ts +++ b/backend/src/services/DatabaseService.ts @@ -1,13 +1,22 @@ import { Sequelize, QueryTypes } from "sequelize"; +import GlobalSetting from "../models/GlobalSetting"; +import { Searcher } from "./Searcher"; +import sequelize from "../config/database"; + +// 全局设置默认值 +const DEFAULT_GLOBAL_SETTINGS = { + httpProxyHost: "127.0.0.1", + httpProxyPort: 7890, + isProxyEnabled: false, + CommonUserCode: 9527, + AdminUserCode: 230713, +}; export class DatabaseService { private sequelize: Sequelize; constructor() { - this.sequelize = new Sequelize({ - dialect: "sqlite", - storage: "./data/database.sqlite", - }); + this.sequelize = sequelize; } async initialize(): Promise { @@ -16,11 +25,26 @@ export class DatabaseService { await this.cleanupBackupTables(); await this.sequelize.sync({ alter: true }); await this.sequelize.query("PRAGMA foreign_keys = ON"); + await this.initializeGlobalSettings(); } catch (error) { throw new Error(`数据库初始化失败: ${(error as Error).message}`); } } + private async initializeGlobalSettings(): Promise { + 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 { const backupTables = await this.sequelize.query<{ name: string }>( "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%\\_backup%' ESCAPE '\\'", diff --git a/backend/src/services/ImageService.ts b/backend/src/services/ImageService.ts index 8c454e0..1f59ff5 100644 --- a/backend/src/services/ImageService.ts +++ b/backend/src/services/ImageService.ts @@ -9,34 +9,60 @@ export class ImageService { private axiosInstance: AxiosInstance | null = null; constructor() { - this.initializeAxiosInstance(); + // 移除构造函数中的初始化,改为懒加载 } - private async initializeAxiosInstance(): Promise { - const settings = await GlobalSetting.findOne(); - const globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes); + private async ensureAxiosInstance(): Promise { + if (!this.axiosInstance) { + 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": "", + this.axiosInstance = axios.create({ + timeout: 30000, + headers: { + Accept: "image/*, */*", + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + }, + withCredentials: false, + maxRedirects: 5, + httpsAgent: globalSetting.isProxyEnabled + ? tunnel.httpsOverHttp({ + proxy: { + host: globalSetting.httpProxyHost, + port: globalSetting.httpProxyPort, + headers: { + "Proxy-Authorization": "", + }, }, - }, - }) - : undefined, - withCredentials: true, - }); + }) + : undefined, + }); + + this.axiosInstance.interceptors.response.use( + (response) => response, + (error) => { + throw error; + } + ); + } + return this.axiosInstance; + } + + async updateAxiosInstance(): Promise { + this.axiosInstance = null; + await this.ensureAxiosInstance(); } async getImages(url: string): Promise { - if (!this.axiosInstance) { - throw new Error("Axios instance not initialized"); - } - return await this.axiosInstance.get(url, { responseType: "stream" }); + const axiosInstance = await this.ensureAxiosInstance(); + + return await axiosInstance.get(url, { + responseType: "stream", + validateStatus: (status) => status >= 200 && status < 300, + headers: { + Referer: new URL(url).origin, + }, + }); } } diff --git a/backend/src/services/QuarkService.ts b/backend/src/services/QuarkService.ts index cfce52c..d5b3f8a 100644 --- a/backend/src/services/QuarkService.ts +++ b/backend/src/services/QuarkService.ts @@ -4,6 +4,13 @@ import { createAxiosInstance } from "../utils/axiosInstance"; import { injectable } from "inversify"; import { Request } from "express"; import UserSetting from "../models/UserSetting"; +import { + ShareInfoResponse, + FolderListResponse, + QuarkFolderItem, + SaveFileParams, +} from "../types/cloud"; +import { ICloudStorageService } from "@/types/services"; interface QuarkShareInfo { stoken?: string; @@ -17,14 +24,8 @@ interface QuarkShareInfo { }[]; } -interface QuarkFolderItem { - fid: string; - file_name: string; - file_type: number; -} - @injectable() -export class QuarkService { +export class QuarkService implements ICloudStorageService { private api: AxiosInstance; private cookie: string = ""; @@ -64,7 +65,7 @@ export class QuarkService { } } - async getShareInfo(pwdId: string, passcode = ""): Promise<{ data: QuarkShareInfo }> { + async getShareInfo(pwdId: string, passcode = ""): Promise { const response = await this.api.post( `/1/clouddrive/share/sharepage/token?pr=ucpro&fr=pc&uc_param_str=&__dt=994&__t=${Date.now()}`, { @@ -84,7 +85,7 @@ export class QuarkService { throw new Error("获取夸克分享信息失败"); } - async getShareList(pwdId: string, stoken: string): Promise { + async getShareList(pwdId: string, stoken: string): Promise { const response = await this.api.get("/1/clouddrive/share/sharepage/detail", { params: { pr: "ucpro", @@ -125,9 +126,7 @@ export class QuarkService { } } - async getFolderList( - parentCid = "0" - ): Promise<{ data: { cid: string; name: string; path: [] }[] }> { + async getFolderList(parentCid = "0"): Promise { const response = await this.api.get("/1/clouddrive/file/sort", { params: { pr: "ucpro", @@ -161,19 +160,20 @@ export class QuarkService { } } - async saveSharedFile(params: { - fid_list: string[]; - fid_token_list: string[]; - to_pdir_fid: string; - pwd_id: string; - stoken: string; - pdir_fid: string; - scene: string; - }): Promise<{ message: string; data: unknown }> { + async saveSharedFile(params: SaveFileParams): Promise<{ message: string; data: unknown }> { + const quarkParams = { + fid_list: params.fids, + fid_token_list: params.fidTokens, + to_pdir_fid: params.folderId, + pwd_id: params.shareCode, + stoken: params.receiveCode, + pdir_fid: "0", + scene: "link", + }; try { const response = await this.api.post( `/1/clouddrive/share/sharepage/save?pr=ucpro&fr=pc&uc_param_str=&__dt=208097&__t=${Date.now()}`, - params + quarkParams ); return { diff --git a/backend/src/services/Searcher.ts b/backend/src/services/Searcher.ts index e99aa67..005e0b8 100644 --- a/backend/src/services/Searcher.ts +++ b/backend/src/services/Searcher.ts @@ -31,9 +31,12 @@ export class Searcher { Searcher.instance = this; } - private async initAxiosInstance() { - const settings = await GlobalSetting.findOne(); - const globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes); + private async initAxiosInstance(isUpdate: boolean = false) { + let globalSetting = {} as GlobalSettingAttributes; + if (isUpdate) { + const settings = await GlobalSetting.findOne(); + globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes); + } this.api = createAxiosInstance( config.telegram.baseUrl, AxiosHeaders.from({ @@ -59,9 +62,7 @@ export class Searcher { } public static async updateAxiosInstance(): Promise { - if (Searcher.instance) { - await Searcher.instance.initAxiosInstance(); - } + await Searcher.instance.initAxiosInstance(true); } private extractCloudLinks(text: string): { links: string[]; cloudType: string } { diff --git a/backend/src/services/SettingService.ts b/backend/src/services/SettingService.ts index a30e1e8..98e75b4 100644 --- a/backend/src/services/SettingService.ts +++ b/backend/src/services/SettingService.ts @@ -1,10 +1,14 @@ -import { injectable } from "inversify"; +import { injectable, inject } from "inversify"; +import { TYPES } from "../core/types"; import UserSetting from "../models/UserSetting"; import GlobalSetting from "../models/GlobalSetting"; import { Searcher } from "./Searcher"; +import { ImageService } from "./ImageService"; @injectable() export class SettingService { + constructor(@inject(TYPES.ImageService) private imageService: ImageService) {} + async getSettings(userId: string | undefined, role: number | undefined) { if (!userId) { throw new Error("用户ID无效"); @@ -39,8 +43,17 @@ export class SettingService { if (role === 1 && globalSetting) { await GlobalSetting.update(globalSetting, { where: {} }); } - - await Searcher.updateAxiosInstance(); + await this.updateSettings(); return { message: "保存成功" }; } + + async updateSettings(/* 参数 */): Promise { + // ... 其他代码 ... + + // 修改这一行,使用注入的实例方法而不是静态方法 + await this.imageService.updateAxiosInstance(); + await Searcher.updateAxiosInstance(); + + // ... 其他代码 ... + } } diff --git a/backend/src/types/cloud.ts b/backend/src/types/cloud.ts index 84776e6..4b428f1 100644 --- a/backend/src/types/cloud.ts +++ b/backend/src/types/cloud.ts @@ -1,11 +1,23 @@ export interface ShareInfoResponse { data: { - fileId: string; - fileName: string; - fileSize: number; - }[]; + list: ShareInfoItem[]; + fileSize?: number; + pwdId?: string; + stoken?: string; + }; } +export interface GetShareInfoParams { + shareCode: string; + receiveCode?: string; +} + +export interface ShareInfoItem { + fileId: string; + fileName: string; + fileSize?: number; + fileIdToken?: string; +} export interface FolderListResponse { data: { cid: string; @@ -15,13 +27,57 @@ export interface FolderListResponse { } export interface SaveFileParams { - shareCode: string; - receiveCode?: string; - fileId: string; - cid?: string; + shareCode: string; // 分享code + receiveCode?: string; // 分享文件的密码 + folderId?: string; // 文件夹id + fids?: string[]; // 存储文件id + fidTokens?: string[]; // 存储文件token } export interface SaveFileResponse { message: string; data: unknown; } + +export interface ShareFileInfo { + shareCode: string; + receiveCode?: string; + fileId: string; + cid?: string; + fid_list?: string[]; + fid_token_list?: string[]; + to_pdir_fid?: string; + pwd_id?: string; + stoken?: string; + pdir_fid?: string; + scene?: string; + [key: string]: any; +} + +export interface QuarkShareFileInfo { + fid_list: string[]; + fid_token_list: string[]; + to_pdir_fid: string; + pwd_id: string; + stoken: string; + pdir_fid: string; + scene: string; +} + +export interface QuarkShareInfo { + stoken?: string; + pwdId?: string; + fileSize?: number; + list: { + fid: string; + file_name: string; + file_type: number; + share_fid_token: string; + }[]; +} + +export interface QuarkFolderItem { + fid: string; + file_name: string; + file_type: number; +} diff --git a/backend/src/types/services.ts b/backend/src/types/services.ts index bcb6fcc..f07fba4 100644 --- a/backend/src/types/services.ts +++ b/backend/src/types/services.ts @@ -1,9 +1,9 @@ import { Request } from "express"; -import { ShareInfoResponse } from "./cloud115"; +import { ShareInfoResponse, FolderListResponse, SaveFileParams } from "./cloud"; -export interface ICloudService { +export interface ICloudStorageService { setCookie(req: Request): Promise; getShareInfo(shareCode: string, receiveCode?: string): Promise; - getFolderList(parentCid?: string): Promise; - saveSharedFile(params: any): Promise; + getFolderList(parentCid?: string): Promise; + saveSharedFile(params: SaveFileParams): Promise; } diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 326a461..609c95a 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -43,27 +43,6 @@ declare module 'vue' { RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] SearchBar: typeof import('./src/components/SearchBar.vue')['default'] - VanBackTop: typeof import('vant/es')['BackTop'] - VanButton: typeof import('vant/es')['Button'] - VanCell: typeof import('vant/es')['Cell'] - VanCellGroup: typeof import('vant/es')['CellGroup'] - VanCheckbox: typeof import('vant/es')['Checkbox'] - VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup'] - VanEmpty: typeof import('vant/es')['Empty'] - VanField: typeof import('vant/es')['Field'] - VanForm: typeof import('vant/es')['Form'] - VanIcon: typeof import('vant/es')['Icon'] - VanImage: typeof import('vant/es')['Image'] - VanLoading: typeof import('vant/es')['Loading'] - VanOverlay: typeof import('vant/es')['Overlay'] - VanPopup: typeof import('vant/es')['Popup'] - VanSearch: typeof import('vant/es')['Search'] - VanSwitch: typeof import('vant/es')['Switch'] - VanTab: typeof import('vant/es')['Tab'] - VanTabbar: typeof import('vant/es')['Tabbar'] - VanTabbarItem: typeof import('vant/es')['TabbarItem'] - VanTabs: typeof import('vant/es')['Tabs'] - VanTag: typeof import('vant/es')['Tag'] } export interface ComponentCustomProperties { vLoading: typeof import('element-plus/es')['ElLoadingDirective'] diff --git a/frontend/src/api/cloud115.ts b/frontend/src/api/cloud115.ts index 73c016f..f8d3168 100644 --- a/frontend/src/api/cloud115.ts +++ b/frontend/src/api/cloud115.ts @@ -1,10 +1,10 @@ import request from "@/utils/request"; -import type { ShareInfoResponse, Folder, Save115FileParams } from "@/types"; +import type { ShareInfoResponse, Folder, SaveFileParams, GetShareInfoParams } from "@/types"; export const cloud115Api = { - async getShareInfo(shareCode: string, receiveCode = "") { + async getShareInfo(params: GetShareInfoParams) { const { data } = await request.get("/api/cloud115/share-info", { - params: { shareCode, receiveCode }, + params, }); return data as ShareInfoResponse; }, @@ -16,7 +16,7 @@ export const cloud115Api = { return res; }, - async saveFile(params: Save115FileParams) { + async saveFile(params: SaveFileParams) { const res = await request.post("/api/cloud115/save", params); return res; }, diff --git a/frontend/src/api/quark.ts b/frontend/src/api/quark.ts index 2e7bb52..ebd3a7a 100644 --- a/frontend/src/api/quark.ts +++ b/frontend/src/api/quark.ts @@ -1,10 +1,10 @@ import request from "@/utils/request"; -import type { ShareInfoResponse, Folder, SaveQuarkFileParams } from "@/types"; +import type { ShareInfoResponse, Folder, SaveFileParams, GetShareInfoParams } from "@/types"; export const quarkApi = { - async getShareInfo(shareCode: string, receiveCode = "") { + async getShareInfo(params: GetShareInfoParams) { const { data } = await request.get("/api/quark/share-info", { - params: { shareCode, receiveCode }, + params, }); return data as ShareInfoResponse; }, @@ -16,7 +16,7 @@ export const quarkApi = { return data; }, - async saveFile(params: SaveQuarkFileParams) { + async saveFile(params: SaveFileParams) { return await request.post("/api/quark/save", params); }, }; diff --git a/frontend/src/stores/resource.ts b/frontend/src/stores/resource.ts index c7b0ee9..24338b1 100644 --- a/frontend/src/stores/resource.ts +++ b/frontend/src/stores/resource.ts @@ -5,10 +5,11 @@ import { quarkApi } from "@/api/quark"; import type { Resource, ShareInfoResponse, - Save115FileParams, - SaveQuarkFileParams, ShareInfo, ResourceItem, + GetShareInfoParams, + SaveFileParams, + ShareFileInfoAndFolder, } from "@/types"; import { ElMessage } from "element-plus"; @@ -24,46 +25,40 @@ const lastResource = ( ) as StorageListObject; // 定义云盘驱动配置类型 -interface CloudDriveConfig< - T extends Record, - P extends Save115FileParams | SaveQuarkFileParams, -> { +interface CloudDriveConfig { name: string; type: string; regex: RegExp; api: { - getShareInfo: (parsedCode: T) => Promise; - saveFile: (params: P) => Promise<{ code: number; message?: string }>; + getShareInfo: (params: GetShareInfoParams) => Promise; + saveFile: (params: SaveFileParams) => Promise<{ code: number; message?: string }>; }; - parseShareCode: (match: RegExpMatchArray) => T; - getSaveParams: (shareInfo: ShareInfoResponse, folderId: string) => P; + parseShareCode: (match: RegExpMatchArray) => GetShareInfoParams; + getSaveParams: (shareInfoAndFolder: ShareFileInfoAndFolder) => SaveFileParams; } // 云盘类型配置 -export const CLOUD_DRIVES: [ - CloudDriveConfig<{ shareCode: string; receiveCode: string }, Save115FileParams>, - CloudDriveConfig<{ shareCode: string }, SaveQuarkFileParams>, -] = [ +export const CLOUD_DRIVES: CloudDriveConfig[] = [ { name: "115网盘", type: "pan115", regex: /(?:115|anxia|115cdn)\.com\/s\/([^?]+)(?:\?password=([^&#]+))?/, api: { - getShareInfo: (parsedCode: { shareCode: string; receiveCode: string }) => - cloud115Api.getShareInfo(parsedCode.shareCode, parsedCode.receiveCode), - saveFile: async (params: Save115FileParams) => { - return await cloud115Api.saveFile(params as Save115FileParams); + getShareInfo: (params: GetShareInfoParams) => cloud115Api.getShareInfo(params), + saveFile: async (params: SaveFileParams) => { + return await cloud115Api.saveFile(params); }, }, parseShareCode: (match: RegExpMatchArray) => ({ shareCode: match[1], receiveCode: match[2] || "", }), - getSaveParams: (shareInfo: ShareInfoResponse, folderId: string) => ({ - shareCode: shareInfo.shareCode || "", - receiveCode: shareInfo.receiveCode || "", - fileId: shareInfo.list[0].fileId, - folderId, + getSaveParams: (shareInfoAndFolder: ShareFileInfoAndFolder) => ({ + shareCode: shareInfoAndFolder.shareCode || "", + receiveCode: shareInfoAndFolder.receiveCode || "", + fileId: shareInfoAndFolder.shareInfo.list[0].fileId, + folderId: shareInfoAndFolder.folderId, + fids: shareInfoAndFolder.shareInfo.list.map((item: { fileId?: string }) => item.fileId || ""), }), }, { @@ -71,23 +66,20 @@ export const CLOUD_DRIVES: [ type: "quark", regex: /pan\.quark\.cn\/s\/([a-zA-Z0-9]+)/, api: { - getShareInfo: (parsedCode: { shareCode: string }) => - quarkApi.getShareInfo(parsedCode.shareCode), - saveFile: async (params: SaveQuarkFileParams) => { - return await quarkApi.saveFile(params as SaveQuarkFileParams); + getShareInfo: (params) => quarkApi.getShareInfo(params), + saveFile: async (params: SaveFileParams) => { + return await quarkApi.saveFile(params); }, }, parseShareCode: (match: RegExpMatchArray) => ({ shareCode: match[1] }), - getSaveParams: (shareInfo: ShareInfoResponse, folderId: string) => ({ - fid_list: shareInfo.list.map((item: { fileId?: string }) => item.fileId || ""), - fid_token_list: shareInfo.list.map( + getSaveParams: (shareInfoAndFolder: ShareFileInfoAndFolder) => ({ + fids: shareInfoAndFolder.shareInfo.list.map((item: { fileId?: string }) => item.fileId || ""), + fidTokens: shareInfoAndFolder.shareInfo.list.map( (item: { fileIdToken?: string }) => item.fileIdToken || "" ), - to_pdir_fid: folderId, - pwd_id: shareInfo.shareCode || "", - stoken: shareInfo.stoken || "", - pdir_fid: "0", - scene: "link", + folderId: shareInfoAndFolder.folderId, + shareCode: shareInfoAndFolder.shareInfo.pwdId || "", + receiveCode: shareInfoAndFolder.shareInfo.stoken || "", }), }, ]; @@ -189,39 +181,32 @@ export const useResourceStore = defineStore("resource", { async saveResourceToDrive( resource: ResourceItem, folderId: string, - drive: - | CloudDriveConfig<{ shareCode: string; receiveCode: string }, Save115FileParams> - | CloudDriveConfig<{ shareCode: string }, SaveQuarkFileParams> + drive: CloudDriveConfig ): Promise { const link = resource.cloudLinks.find((link) => drive.regex.test(link)); if (!link) return; const match = link.match(drive.regex); if (!match) throw new Error("链接解析失败"); + const parsedCode = drive.parseShareCode(match); const shareInfo = { ...this.shareInfo, list: this.resourceSelect.filter((x) => x.isChecked), }; + console.log(shareInfo); - if (this.is115Drive(drive)) { - const params = drive.getSaveParams(shareInfo, folderId); - const result = await drive.api.saveFile(params); + const params = drive.getSaveParams({ + shareInfo, + ...parsedCode, + folderId, + }); + const result = await drive.api.saveFile(params); - if (result.code === 0) { - ElMessage.success(`${drive.name} 转存成功`); - } else { - ElMessage.error(result.message); - } + if (result.code === 0) { + ElMessage.success(`${drive.name} 转存成功`); } else { - const params = drive.getSaveParams(shareInfo, folderId); - const result = await drive.api.saveFile(params); - - if (result.code === 0) { - ElMessage.success(`${drive.name} 转存成功`); - } else { - ElMessage.error(result.message); - } + ElMessage.error(result.message); } }, @@ -237,18 +222,7 @@ export const useResourceStore = defineStore("resource", { if (!match) throw new Error("链接解析失败"); const parsedCode = matchedDrive.parseShareCode(match); - let shareInfo = this.is115Drive(matchedDrive) - ? await matchedDrive.api.getShareInfo( - parsedCode as { shareCode: string; receiveCode: string } - ) - : await matchedDrive.api.getShareInfo(parsedCode as { shareCode: string }); - - if (Array.isArray(shareInfo)) { - shareInfo = { - list: shareInfo, - }; - } - + const shareInfo = await matchedDrive.api.getShareInfo(parsedCode); if (shareInfo?.list?.length) { this.resources = [ { @@ -296,30 +270,14 @@ export const useResourceStore = defineStore("resource", { if (!match) throw new Error("链接解析失败"); const parsedCode = drive.parseShareCode(match); - let shareInfo = {} as ShareInfoResponse; this.setLoadTree(true); - if (this.is115Drive(drive)) { - shareInfo = await drive.api.getShareInfo( - parsedCode as { shareCode: string; receiveCode: string } - ); - } else { - shareInfo = this.is115Drive(drive) - ? await drive.api.getShareInfo(parsedCode as { shareCode: string; receiveCode: string }) - : await drive.api.getShareInfo(parsedCode as { shareCode: string }); - } + let shareInfo = await drive.api.getShareInfo(parsedCode); this.setLoadTree(false); if (shareInfo) { - if (Array.isArray(shareInfo)) { - shareInfo = { - list: shareInfo, - ...parsedCode, - }; - } else { - shareInfo = { - ...shareInfo, - ...parsedCode, - }; - } + shareInfo = { + ...shareInfo, + ...parsedCode, + }; this.shareInfo = shareInfo; this.setSelectedResource(this.shareInfo.list.map((x) => ({ ...x, isChecked: true }))); return true; @@ -334,13 +292,5 @@ export const useResourceStore = defineStore("resource", { console.error(message, error); ElMessage.error(error instanceof Error ? error.message : message); }, - - is115Drive( - drive: - | CloudDriveConfig<{ shareCode: string; receiveCode: string }, Save115FileParams> - | CloudDriveConfig<{ shareCode: string }, SaveQuarkFileParams> - ): drive is CloudDriveConfig<{ shareCode: string; receiveCode: string }, Save115FileParams> { - return drive.type === "pan115"; - }, }, }); diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index cb1fb1d..f1d1e56 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -33,13 +33,25 @@ export interface ShareInfo { isChecked?: boolean; } +export interface ShareInfoItem { + fileId: string; + fileName: string; + fileSize?: number; + fileIdToken?: string; +} + export interface ShareInfoResponse { - list: ShareInfo[]; + list: ShareInfoItem[]; + fileSize?: number; pwdId?: string; stoken?: string; - shareCode?: string; +} + +export interface ShareFileInfoAndFolder { + shareInfo: ShareInfoResponse; + folderId: string; + shareCode: string; receiveCode?: string; - fileSize?: number; } export interface Folder { @@ -49,10 +61,16 @@ export interface Folder { } export interface SaveFileParams { + shareCode: string; // 分享code + receiveCode?: string; // 分享文件的密码 + folderId: string; // 文件夹id + fids: string[]; // 存储文件id + fidTokens?: string[]; // 存储文件token +} + +export interface GetShareInfoParams { shareCode: string; - receiveCode: string; - fileId: string; - folderId: string; + receiveCode?: string; } export interface ApiResponse { From 5a625e5c0e1827f3284983495069f4d9eaa9f76e Mon Sep 17 00:00:00 2001 From: jiangrui Date: Tue, 11 Mar 2025 17:59:00 +0800 Subject: [PATCH 05/22] Remove useless code --- backend/src/app.ts | 3 +-- backend/src/core/ServiceRegistry.ts | 19 ------------------- backend/src/core/container.ts | 16 ---------------- 3 files changed, 1 insertion(+), 37 deletions(-) delete mode 100644 backend/src/core/ServiceRegistry.ts delete mode 100644 backend/src/core/container.ts diff --git a/backend/src/app.ts b/backend/src/app.ts index dafccdd..d000f51 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,7 +1,7 @@ // filepath: /d:/code/CloudDiskDown/backend/src/app.ts import "./types/express"; import express from "express"; -import { container } from "./core/container"; +import { container } from "./inversify.config"; import { TYPES } from "./core/types"; import { DatabaseService } from "./services/DatabaseService"; import { setupMiddlewares } from "./middleware"; @@ -22,7 +22,6 @@ class App { // 设置路由 this.app.use("/", routes); - // 设置错误处理中间件 this.app.use(errorHandler); } diff --git a/backend/src/core/ServiceRegistry.ts b/backend/src/core/ServiceRegistry.ts deleted file mode 100644 index 0a86372..0000000 --- a/backend/src/core/ServiceRegistry.ts +++ /dev/null @@ -1,19 +0,0 @@ -export class ServiceRegistry { - private static instance: ServiceRegistry; - private services: Map = 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(name: string): T { - return this.services.get(name); - } -} diff --git a/backend/src/core/container.ts b/backend/src/core/container.ts deleted file mode 100644 index cf12410..0000000 --- a/backend/src/core/container.ts +++ /dev/null @@ -1,16 +0,0 @@ -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(TYPES.Cloud115Service).to(Cloud115Service); -container.bind(TYPES.QuarkService).to(QuarkService); -container.bind(TYPES.Searcher).to(Searcher); -container.bind(TYPES.DatabaseService).to(DatabaseService); - -export { container }; From e6171fb34c094ab70165db244d8c611b3a2affa2 Mon Sep 17 00:00:00 2001 From: jiangrui Date: Tue, 11 Mar 2025 18:08:29 +0800 Subject: [PATCH 06/22] update version --- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- package-lock.json | 2 +- package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 28b3e1c..ffdbeb7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "cloud-disk-web", - "version": "0.2.3", + "version": "0.2.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cloud-disk-web", - "version": "0.2.3", + "version": "0.2.4", "dependencies": { "axios": "^1.6.7", "element-plus": "^2.6.1", diff --git a/frontend/package.json b/frontend/package.json index 9dd137a..6744a07 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "cloud-saver-web", "private": true, - "version": "0.2.3", + "version": "0.2.4", "type": "module", "scripts": { "dev": "vite --host", diff --git a/package-lock.json b/package-lock.json index 8d1ebe6..2cc577f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,7 +62,7 @@ }, "frontend": { "name": "cloud-saver-web", - "version": "0.2.3", + "version": "0.2.4", "dependencies": { "@element-plus/icons-vue": "^2.3.1", "axios": "^1.6.7", diff --git a/package.json b/package.json index 577b3fa..2c30fdf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cloud-saver", - "version": "0.2.3", + "version": "0.2.4", "private": true, "workspaces": [ "frontend", From dca0f1f0c1826b687ac36b46e5833b96b66e1bc4 Mon Sep 17 00:00:00 2001 From: jiangrui Date: Tue, 11 Mar 2025 21:57:45 +0800 Subject: [PATCH 07/22] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E8=B7=B3=E8=BD=AC?= =?UTF-8?q?=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components.d.ts | 18 ++++++++++ frontend/src/components/Home/ResourceCard.vue | 35 +++++++++++++++---- .../src/components/Home/ResourceTable.vue | 10 ++++-- .../src/components/mobile/ResourceCard.vue | 16 +++++++-- frontend/src/stores/resource.ts | 11 +++++- frontend/src/types/index.ts | 1 + frontend/src/views/ResourceList.vue | 5 +++ frontend/src/views/mobile/ResourceList.vue | 5 +++ 8 files changed, 89 insertions(+), 12 deletions(-) diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 609c95a..d85c927 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -43,6 +43,24 @@ declare module 'vue' { RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] SearchBar: typeof import('./src/components/SearchBar.vue')['default'] + VanBackTop: typeof import('vant/es')['BackTop'] + VanButton: typeof import('vant/es')['Button'] + VanCell: typeof import('vant/es')['Cell'] + VanCellGroup: typeof import('vant/es')['CellGroup'] + VanCheckbox: typeof import('vant/es')['Checkbox'] + VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup'] + VanEmpty: typeof import('vant/es')['Empty'] + VanIcon: typeof import('vant/es')['Icon'] + VanImage: typeof import('vant/es')['Image'] + VanLoading: typeof import('vant/es')['Loading'] + VanOverlay: typeof import('vant/es')['Overlay'] + VanPopup: typeof import('vant/es')['Popup'] + VanSearch: typeof import('vant/es')['Search'] + VanTab: typeof import('vant/es')['Tab'] + VanTabbar: typeof import('vant/es')['Tabbar'] + VanTabbarItem: typeof import('vant/es')['TabbarItem'] + VanTabs: typeof import('vant/es')['Tabs'] + VanTag: typeof import('vant/es')['Tag'] } export interface ComponentCustomProperties { vLoading: typeof import('element-plus/es')['ElLoadingDirective'] diff --git a/frontend/src/components/Home/ResourceCard.vue b/frontend/src/components/Home/ResourceCard.vue index fa90887..6ae45bf 100644 --- a/frontend/src/components/Home/ResourceCard.vue +++ b/frontend/src/components/Home/ResourceCard.vue @@ -52,7 +52,13 @@ @@ -77,7 +153,7 @@ import { ref, onMounted } from "vue"; import { useRouter } from "vue-router"; import { showNotify } from "vant"; -import type { FieldInstance } from "vant"; +import type { FieldInstance, FieldRule } from "vant"; import { userApi } from "@/api/user"; import logo from "@/assets/images/logo.png"; import { STORAGE_KEYS } from "@/constants/storage"; @@ -88,21 +164,42 @@ interface LoginForm { password: string; } +interface RegisterForm { + username: string; + password: string; + confirmPassword: string; + registerCode: string; +} + // 响应式数据 -const formData = ref({ +const activeTab = ref("login"); +const isLoading = ref(false); +const loginPasswordRef = ref(); +const rememberPassword = ref(false); + +const loginForm = ref({ username: "", password: "", }); -const isLoading = ref(false); -const passwordRef = ref(); -const rememberPassword = ref(false); + +const registerForm = ref({ + username: "", + password: "", + confirmPassword: "", + registerCode: "", +}); // 工具函数 const router = useRouter(); // 方法定义 -const focusPassword = () => { - passwordRef.value?.focus(); +const focusLoginPassword = () => { + loginPasswordRef.value?.focus(); +}; + +// 表单验证 +const validateConfirmPassword = (value: string) => { + return value === registerForm.value.password; }; // 在组件加载时检查是否有保存的账号密码 @@ -110,22 +207,22 @@ onMounted(() => { const savedUsername = localStorage.getItem(STORAGE_KEYS.USERNAME); const savedPassword = localStorage.getItem(STORAGE_KEYS.PASSWORD); if (savedUsername && savedPassword) { - formData.value.username = savedUsername; - formData.value.password = savedPassword; + loginForm.value.username = savedUsername; + loginForm.value.password = savedPassword; rememberPassword.value = true; } }); -const handleSubmit = async () => { +// 登录处理 +const handleLogin = async () => { try { isLoading.value = true; - const res = await userApi.login(formData.value); + const res = await userApi.login(loginForm.value); if (res.code === 0) { - // 处理记住密码 if (rememberPassword.value) { - localStorage.setItem(STORAGE_KEYS.USERNAME, formData.value.username); - localStorage.setItem(STORAGE_KEYS.PASSWORD, formData.value.password); + localStorage.setItem(STORAGE_KEYS.USERNAME, loginForm.value.username); + localStorage.setItem(STORAGE_KEYS.PASSWORD, loginForm.value.password); } else { localStorage.removeItem(STORAGE_KEYS.USERNAME); localStorage.removeItem(STORAGE_KEYS.PASSWORD); @@ -134,22 +231,65 @@ const handleSubmit = async () => { localStorage.setItem(STORAGE_KEYS.TOKEN, res.data.token); await router.push("/"); } else { - showNotify({ - type: "danger", - message: res.message || "登录失败", - duration: 2000, - }); + showNotify({ type: "danger", message: res.message || "登录失败" }); } } catch (error) { - showNotify({ - type: "danger", - message: "登录失败", - duration: 2000, - }); + showNotify({ type: "danger", message: "登录失败" }); } finally { isLoading.value = false; } }; + +// 注册处理 +const handleRegister = async () => { + try { + isLoading.value = true; + const res = await userApi.register({ + username: registerForm.value.username, + password: registerForm.value.password, + registerCode: registerForm.value.registerCode, + }); + + if (res.code === 0) { + showNotify({ type: "success", message: "注册成功" }); + // 自动填充登录表单 + loginForm.value.username = registerForm.value.username; + loginForm.value.password = registerForm.value.password; + activeTab.value = "login"; + // 清空注册表单 + registerForm.value = { + username: "", + password: "", + confirmPassword: "", + registerCode: "", + }; + } else { + showNotify({ type: "danger", message: res.message || "注册失败" }); + } + } catch (error) { + showNotify({ type: "danger", message: "注册失败" }); + } finally { + isLoading.value = false; + } +}; + +// 定义验证规则 +const usernameRules: FieldRule[] = [ + { required: true, message: "请填写用户名" }, + { pattern: /.{3,}/, message: "用户名至少3个字符" }, +]; + +const passwordRules: FieldRule[] = [ + { required: true, message: "请填写密码" }, + { pattern: /.{6,}/, message: "密码至少6个字符" }, +]; + +const confirmPasswordRules: FieldRule[] = [ + { required: true, message: "请确认密码" }, + { validator: validateConfirmPassword, message: "两次密码不一致" }, +]; + +const registerCodeRules: FieldRule[] = [{ required: true, message: "请填写注册码" }]; From 28f894f98818ef20eb28a6eadb3e6da83d403154 Mon Sep 17 00:00:00 2001 From: jiangrui Date: Wed, 12 Mar 2025 13:04:33 +0800 Subject: [PATCH 10/22] =?UTF-8?q?fix:=E9=BB=98=E8=AE=A4=E5=8F=AA=E5=8F=96?= =?UTF-8?q?=E7=AC=AC=E4=B8=80=E4=B8=AA=E7=BD=91=E7=9B=98=E9=93=BE=E6=8E=A5?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E4=B8=BA=E6=A0=87=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/services/Searcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/Searcher.ts b/backend/src/services/Searcher.ts index 005e0b8..91c9bdc 100644 --- a/backend/src/services/Searcher.ts +++ b/backend/src/services/Searcher.ts @@ -72,7 +72,7 @@ export class Searcher { const matches = text.match(pattern); if (matches) { links.push(...matches); - cloudType = Object.keys(config.cloudPatterns)[index]; + if (!cloudType) cloudType = Object.keys(config.cloudPatterns)[index]; } }); return { From f32b6cd5d0092aa3bcd2311adc237f41d1ba6ca9 Mon Sep 17 00:00:00 2001 From: jiangrui Date: Wed, 12 Mar 2025 14:03:53 +0800 Subject: [PATCH 11/22] =?UTF-8?q?fix:=E4=BC=98=E5=8C=96=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E7=AB=AF=E7=99=BB=E5=BD=95=E6=B3=A8=E5=86=8C=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/views/mobile/Login.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/mobile/Login.vue b/frontend/src/views/mobile/Login.vue index 608b967..ea0818a 100644 --- a/frontend/src/views/mobile/Login.vue +++ b/frontend/src/views/mobile/Login.vue @@ -304,8 +304,8 @@ const registerCodeRules: FieldRule[] = [{ required: true, message: "请填写注 position: absolute; inset: 0; background: url("@/assets/images/mobile-login-bg.png") no-repeat; - background-size: cover; - background-position: center; + background-size: 100% auto; + filter: blur(1px); } // 主内容区 @@ -347,6 +347,7 @@ const registerCodeRules: FieldRule[] = [{ required: true, message: "请填写注 // 表单 &__form { padding: 0; + margin-top: 20px; } &__form-group { From bc38acded3185b62fb234192667ce72f926a8636 Mon Sep 17 00:00:00 2001 From: jiangrui Date: Wed, 12 Mar 2025 15:22:30 +0800 Subject: [PATCH 12/22] =?UTF-8?q?feat:=E5=8E=BB=E9=99=A4=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E4=BF=A1=E6=81=AF=E9=9C=80=E8=A6=81cookie?= =?UTF-8?q?=E7=9A=84=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/controllers/BaseCloudController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/controllers/BaseCloudController.ts b/backend/src/controllers/BaseCloudController.ts index 21d7ad6..cf8cff8 100644 --- a/backend/src/controllers/BaseCloudController.ts +++ b/backend/src/controllers/BaseCloudController.ts @@ -10,7 +10,7 @@ export abstract class BaseCloudController extends BaseController { async getShareInfo(req: Request, res: Response): Promise { await this.handleRequest(req, res, async () => { const { shareCode, receiveCode } = req.query; - await this.cloudService.setCookie(req); + // await this.cloudService.setCookie(req); return await this.cloudService.getShareInfo(shareCode as string, receiveCode as string); }); } From bf2d7c70c73402bb8c073bfa64c3a1d629c7ad6a Mon Sep 17 00:00:00 2001 From: jiangrui Date: Wed, 12 Mar 2025 15:23:11 +0800 Subject: [PATCH 13/22] =?UTF-8?q?fix:=E4=BC=98=E5=8C=96=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=B1=95=E7=A4=BA=E6=B7=BB=E5=8A=A0=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/assets/images/default.png | Bin 0 -> 6199 bytes frontend/src/components/Home/ResourceCard.vue | 29 +++++------------- .../src/components/Home/ResourceTable.vue | 27 +++------------- .../src/components/mobile/ResourceCard.vue | 5 +-- frontend/src/main.ts | 5 ++- frontend/src/stores/resource.ts | 2 ++ frontend/src/utils/image.ts | 10 ++++++ 7 files changed, 32 insertions(+), 46 deletions(-) create mode 100644 frontend/src/assets/images/default.png create mode 100644 frontend/src/utils/image.ts diff --git a/frontend/src/assets/images/default.png b/frontend/src/assets/images/default.png new file mode 100644 index 0000000000000000000000000000000000000000..583c09f678b6beabb5c19c4a41969faef88dec6c GIT binary patch literal 6199 zcmd6rWfvp_aWTkM0cj9WkX%YZK{}*dn+6Gy5C$=@01G4qrIApSMpi*l zT9%Slq?M2bY3_J`|G>Sk?tL+znKS3iInQ(EiSKi6o18ex!OF`D02~JTx@G_f_J{xr z5xd!YXI=Q$@-aKA4f)Od697Bs40N?Dt~mZqy6e|h%zg9Alit?)9O26%krJ;Q41ONl zYklUn$3DqVMWUVdEM#I7GPmS+qPeLFN1i=9dr9t!7ym3XA%#-Hdau8jpW_zJgbF}q zGD$g005non$mkXhzztY^UjnpV8hbHe0)TQ*!XkM9RHlO!1u#Ji5wX@+IH1%eMP=w< z(jEeWiWR_k>HJq;F#sAdg;IwN6Y$rF-%0@J2GY0<0x84IN9n1UHhd^fKoY>WZ`nzO zge{K$UoC#yeW)%gwuPt=i! zy}g-CQM%hq$hR?h)9z`8Zmh|*%vUFWFbrB|_jeOj8Bf754rsg)i($^BkVQp76lTH{%NS6xN%xGY$YPdK% zThe5ov&_5%wB+39!hd?s{}1$}m_tg|+IO!d#mIem4Fz4J<}c~~ZlV=R4(_>Do2%|_+Wg#=$|#xi8XTZoRjb~M;vp=__8>SqI%-4> zFNqZX{@fXjs~i66)ZlxX4R?Jpk_f!KOzKJ>na~b*qtf`MGyFhld6AJ?tO`_@>>kVR zg$_5M=!Bi`=TxsYK2wa@RUy0g#XN;#|5q@6I4S17-}xOEaQ}Kj0$=%cMWlK_JrYe|B@LeFck*QoPQ(s(NITmZO4 zC4no$&a>>AZ2xCJM?A177y#L@B-vZ9#m}d&w-68UHmpn#$Xk!N8@=rlx$3Xu0ZWFr zaOYZL2=H0d?<(i9gG-hTA~T+i9O&s?g0|hf?9i?x@PPq`l zn_-(g)(vySx@b~m^H!ha(l-uZ623x@Vs9W~I~ z*GE?rsjgR70NsTz&KH4l;;m#rjPLcJ_>55V8B!xVJec?@IEuip)WGEy(!tptr1cm? zH*$eksNA%vjxz{vCfc3g#GJH7n4{(K9kHElmHjynPP3XEgfv`Qkgu{Ds%{~>7PlKr ze7FkZfSdt0IBNxRcUEnN=1$WUo3eXjTv^E+b?xP)_2JAzW?A$>b5FdL{IR4hp9!c{ zG30JwkwakU-i5oR<=m3c9%Bzq;t(MYXGy+^KbL(~t{RpNI=Jklo`5$bU;-7v{>F^` z3%Im_jonDJ1?L5!2+64i$=fVw)ZF4-t$Z5N`r}_SkP{37Kbo*ff55GaW(YcoK|e(d{W1^dRZY?l6X!kdqMk!5XqmpHts#G&Hg zW8(8@L~t#~5k>jI))C7lrTY?MPbiL$*&t`>8lEr(_XbIfi&37daH z9ac7L4D;9!A2r57wui3F5n&xYF1&uLYEaomU1W>6dl{)rZIO zOK-1GUOx%TE1?>2MFV9Dp%@~2nKuhYO7@OLWD`Z1J+3+4vYye0lm#He(D^f-+%vok zA4_%b^Qgzl)zYe}tKRCjLSzvf4#c>p6E|iJ!o|@j+uS#aC1CCj2`jM`^XIB*JND>T~*vq}}u9YP!GX&0McJ2Vbi zw{S32)`GMe9`J%X6MRRO5*)Tot-tFSZTxD^zF|^OQ=*>1$VOUqfD2~dR6Y)`o&Wmv zONkXx_)w)GT~~ytrE|dC^2p1Z+|Dt}{c3++!DMavC}Mg+tl3YvM?2%?|2IoE&lE zn;vgnR3FEI#=Up-k9dx)t_H0rXD^+Kyh1q!KjB5`cq20n-@PLzp^Qv-y!LP;f;o+e zA}a|Ih+Be>(u1KC)8BChD>b59kX9bXcf^Lh%(83Hr%%*qx0f)?ZXTu!<1UzA(CQBbC9M8F zywp)-euBV_q~u~hz0hXw==$n+YP@JUjiKYlVFoEjRcWM@)NqYg;Z9L&*%>%-XnnD> zTO9&_G1Q4GoeJiL1Z-3Z4?3L*>gB=d92=HuWEY8OfPMm0Fw+Y7Twf>Nl=N%mu`0+i zPOQVFbcqaMV{dTaEcTm(*#N_rnPz(i$gM-sc~XQkNUN(93y3vDnkG>Gwax<90hDr9 zr}z&kf{9X6LHUk6zSxjuisBx?_^eNEFE~boObeLjjcO(yoVHKqdp-m#R zJ;A8-p5r&Luy#7hYC%4Q!MwjlSTA3IBqum}I{0p3uQbhw2{CsND+_=B`9)y{7vGh*0U!e zf^~D>7Z(@5v&(!`D|76TIYldM(f8i4J|!YdyGscY z0=EdFiNBwp+e7F~sEWzPYrW@z+94$dGmy`vi5)7yG2QT=8K`*P`IQJLIrn zR8)YgMI9Ybm6a`TujBS&ziaR7=O$t%SRFtSSif$ojcmhG(6*DNVx%k#_ERE5w53zg z2n+@zUW$=e+fy-uJF1x)V?xnIfTC+fVkJysHAB{mXC}wR$j<@DdnH+K;QX+{p#7_H zmI#T<6oBgi&kgOcfAl{&zQ3KDH3I96_7J9x+d`{&m9&rv=PvB#;&S?w2kHY zv0DMjns#?gJsnr#`a*=dHcytF6~7;tJSOY!?|-T!UfqaZT_g8$N-*2(;V#^q~$*%p?qgMYH?*AId$u|?5^5Ub!c8lbVcd5Qnjr%S;bGu4~P zW%D0z!t7e^dPR%#SU^U=>Umon2=j}a8+N`sFX_$3mGkxslw2UWYypKAZKaJNBLQQlrYdK2z597kBcUJfRDHA8;-}9)DqG6 z4aasA65!nLJTVymRKVOHGuU#+i&@VdS~)^?-F%J|)?WHNWg6<{hRLs_Kc3`MZ% zvJ@{5@$u1VxW=ekWOrePiz9m~R||Yr0^#O+&FEOYSC3j%CaBDy{p9C}BoS8nPClDj zz(IeK3=i0b&c3|=;Mm|V0?6s!)i+}ThSKdX<3R*UbAOaQCv}3YCp9j(Y&%5kYC=xl z1$0luIotVDp6!=@Zz)aO-CBQnWLRI{$NtsG0f)P7P&hZrdHChR1^f=69eD5Se2hSG zW0MV22s>YPZktDg#OJ_O1%hhP!h;69T+_0(`)u1sit0@V zXVh-dE&Ax&#YUSRFJ{dSQ&tdX+>zdQF}TjEYcJ5Y$?qk4ojlp?o$2?Yae36CB(!CH zweRxs@7vA&b|JkClEpVOAbUnDP=x8f)HgGWd6+?7RAi;Xe@13=V`XKf ztDfdGT^6ZQFqS&=ge|9xo}fCZjSR8eGw3n)`c_<$@6K9C87gvJsQEci_sKL33cE(xZ~u;y_d^O=&Kv{^Hlc+u=1u$m5@|nfj9Vur_50k>%gU{e(K;3$7qWWdub8*T z9!J*~8R7NA$uYgOrc38wTD|cdnYyR~& zkqf3cXg@-h|AkpozhdFri3Zzi3nBN(ax4g~Bq6x1=*$TX!m$hjNKKC7(9`0+SEDB; z8}x&Si2IRUUaxX(`wufO+b3&lLz)KZ!XqZo*OFTIZY7sXoP6^l#L@Jd`bh1E#)+E)2d0WXnWdfvm z+7jW+fped$1<82<(8qSb42N3rb|&~3I(+)JrPO;lJ0gXa995PEDC3|jpvF=1^kPtH=WkBa7PB{O#X-Hjc?*p zzkVGg?F{r#Pr7CuOO3SPYL!D@4l3&tpnte9z`_t-hC4S7R0j8T1U;ijfnhEwdvym4 z#fKd&?_5N#_eDHng}#>iWkBt&I;vj(t=DrZ;H@!D<%vGP)qUn|Kt9M0v%9+c(xegU zZsl_kE8O2k>)WZd#bnd?!Y??-gi>Q3iDfviBHEYcL3Q+OJ~Ct z87du#P+v0OrrR}MG`=n}>vSVxLIc9`7bLusFZNsrE(^Yn2lF-kKl}t3>PWAweJhRc zdA^q08ORaxq=lP(MCNw<;)vbU9>1ge3hDx~?V%8snq-a-(63|zB%g51b~#C)_T9Z$ zdHk8=&GRx1P5GI<{48(rv>iSVf>$5WfRc6C<7X}b>=qc8x=MumY>qHY2g<)Yjkwzr z*c9B}{b=8;zx(YE1Z%2(3Q*q90W{Mu3isc!>tC!>zdL(u!}Xck?w%1