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/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 f3c6d5a..d000f51 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,76 +1,57 @@ // filepath: /d:/code/CloudDiskDown/backend/src/app.ts import "./types/express"; import express from "express"; -import cors from "cors"; -import cookieParser from "cookie-parser"; +import { container } from "./inversify.config"; +import { TYPES } from "./core/types"; +import { DatabaseService } from "./services/DatabaseService"; +import { setupMiddlewares } from "./middleware"; import routes from "./routes/api"; +import { logger } from "./utils/logger"; import { errorHandler } from "./middleware/errorHandler"; -import sequelize from "./config/database"; -import { authMiddleware } from "./middleware/auth"; -import GlobalSetting from "./models/GlobalSetting"; -import Searcher from "./services/Searcher"; +class App { + private app = express(); + private databaseService = container.get(TYPES.DatabaseService); -const app = express(); - -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(); + constructor() { + this.setupExpress(); } - authMiddleware(req, res, next); + + private setupExpress(): void { + // 设置中间件 + setupMiddlewares(this.app); + + // 设置路由 + this.app.use("/", routes); + this.app.use(errorHandler); + } + + public async start(): Promise { + try { + // 初始化数据库 + await this.databaseService.initialize(); + logger.info("数据库初始化成功"); + + // 启动服务器 + const port = process.env.PORT || 8009; + this.app.listen(port, () => { + logger.info(` +🚀 服务器启动成功 +🌍 监听端口: ${port} +🔧 运行环境: ${process.env.NODE_ENV || "development"} + `); + }); + } catch (error) { + logger.error("服务器启动失败:", error); + process.exit(1); + } + } +} + +// 创建并启动应用 +const application = new App(); +application.start().catch((error) => { + logger.error("应用程序启动失败:", error); + process.exit(1); }); -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(); -}; - -// 错误处理 -app.use(errorHandler); - -const PORT = process.env.PORT || 8009; - -// 在同步前禁用外键约束,同步后重新启用 -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}`); - }); - }) - .catch((error) => { - console.error("Database sync failed:", error); - }); - -export default app; +export default application; 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/BaseCloudController.ts b/backend/src/controllers/BaseCloudController.ts new file mode 100644 index 0000000..cf8cff8 --- /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 new file mode 100644 index 0000000..c17d954 --- /dev/null +++ b/backend/src/controllers/BaseController.ts @@ -0,0 +1,24 @@ +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 | void> + ): Promise { + try { + const result = await action(); + 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 a70185f..0b39e99 100644 --- a/backend/src/controllers/cloud115.ts +++ b/backend/src/controllers/cloud115.ts @@ -1,60 +1,11 @@ -import { Request, Response } from "express"; import { Cloud115Service } from "../services/Cloud115Service"; -import { sendSuccess, sendError } from "../utils/response"; -import UserSetting from "../models/UserSetting"; +import { injectable, inject } from "inversify"; +import { TYPES } from "../core/types"; +import { BaseCloudController } from "./BaseCloudController"; -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 BaseCloudController { + constructor(@inject(TYPES.Cloud115Service) cloud115Service: Cloud115Service) { + super(cloud115Service); } -}; - -export const cloud115Controller = { - async getShareInfo(req: Request, res: Response): Promise { - try { - 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 || "获取分享信息失败" }); - } - }, - - async getFolderList(req: Request, res: Response): Promise { - try { - 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 || "获取目录列表失败" }); - } - }, - - async saveFile(req: Request, res: Response): Promise { - try { - const { shareCode, receiveCode, fileId, folderId } = req.body; - await setCookie(req); - const result = await cloud115.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/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..4ef6ddb 100644 --- a/backend/src/controllers/quark.ts +++ b/backend/src/controllers/quark.ts @@ -1,52 +1,12 @@ 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 { BaseCloudController } from "./BaseCloudController"; -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 BaseCloudController { + constructor(@inject(TYPES.QuarkService) quarkService: QuarkService) { + super(quarkService); } -}; - -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); - sendSuccess(res, result); - } catch (error) { - sendError(res, { message: "获取分享信息失败" }); - } - }, - - async getFolderList(req: Request, res: Response): Promise { - try { - const { parentCid } = req.query; - await setCookie(req); - const result = await quark.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); - 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..59faeed 100644 --- a/backend/src/controllers/resource.ts +++ b/backend/src/controllers/resource.ts @@ -1,21 +1,23 @@ 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"; + +@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; + return 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..722a00d 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 { + await this.handleRequest(req, res, async () => { 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 || "获取设置失败" }); - } - }, + const role = Number(req.user?.role); + return await this.settingService.getSettings(userId, role); + }); + } + async save(req: Request, res: Response): Promise { - try { + await this.handleRequest(req, res, async () => { 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 || "保存设置失败" }); - } - }, -}; + const role = Number(req.user?.role); + return await this.settingService.saveSettings(userId, role, req.body); + }); + } +} diff --git a/backend/src/controllers/sponsors.ts b/backend/src/controllers/sponsors.ts new file mode 100644 index 0000000..9a7a807 --- /dev/null +++ b/backend/src/controllers/sponsors.ts @@ -0,0 +1,18 @@ +import { Request, Response } from "express"; +import { injectable, inject } from "inversify"; +import { TYPES } from "../core/types"; +import { SponsorsService } from "../services/SponsorsService"; +import { BaseController } from "./BaseController"; + +@injectable() +export class SponsorsController extends BaseController { + constructor(@inject(TYPES.SponsorsService) private sponsorsService: SponsorsService) { + super(); + } + + async get(req: Request, res: Response): Promise { + await this.handleRequest(req, res, async () => { + return await this.sponsorsService.getSponsors(); + }); + } +} diff --git a/backend/src/controllers/teleImages.ts b/backend/src/controllers/teleImages.ts index 6da7ab4..450503a 100644 --- a/backend/src/controllers/teleImages.ts +++ b/backend/src/controllers/teleImages.ts @@ -1,78 +1,30 @@ -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 = 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); }); } - 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..f2ca4f3 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, registerCode } = req.body; + return await this.userService.register(username, password, registerCode); + }); + } 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..8bd467f --- /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, 0, data, message); + } + + static error(message: string, code = 10000): ApiResponse { + return new ApiResponse(false, code, null, message); + } +} diff --git a/backend/src/core/types.ts b/backend/src/core/types.ts new file mode 100644 index 0000000..9244212 --- /dev/null +++ b/backend/src/core/types.ts @@ -0,0 +1,20 @@ +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"), + SponsorsService: Symbol.for("SponsorsService"), + + 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"), + SponsorsController: Symbol.for("SponsorsController"), +}; diff --git a/backend/src/inversify.config.ts b/backend/src/inversify.config.ts new file mode 100644 index 0000000..e4e9dc5 --- /dev/null +++ b/backend/src/inversify.config.ts @@ -0,0 +1,45 @@ +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"; +import { ImageService } from "./services/ImageService"; +import { SettingService } from "./services/SettingService"; +import { SponsorsService } from "./services/SponsorsService"; +// 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"; +import { SponsorsController } from "./controllers/sponsors"; +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.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(); +container.bind(TYPES.SponsorsService).to(SponsorsService).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); +container.bind(TYPES.SponsorsController).to(SponsorsController); + +export { container }; diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index 6a41518..684707a 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -16,7 +16,7 @@ export const authMiddleware = async ( res: Response, next: NextFunction ): Promise => { - if (req.path === "/user/login" || req.path === "/user/register") { + if (req.path === "/user/login" || req.path === "/user/register" || req.path === "/tele-images/") { return next(); } 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/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 new file mode 100644 index 0000000..2bda0be --- /dev/null +++ b/backend/src/middleware/index.ts @@ -0,0 +1,14 @@ +import { Application } from "express"; +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); +}; diff --git a/backend/src/middleware/rateLimiter.ts b/backend/src/middleware/rateLimiter.ts new file mode 100644 index 0000000..da8be8e --- /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 = 600; // 每个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..4e850b3 --- /dev/null +++ b/backend/src/middleware/requestLogger.ts @@ -0,0 +1,23 @@ +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, + 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..71652ed 100644 --- a/backend/src/routes/api.ts +++ b/backend/src/routes/api.ts @@ -1,36 +1,55 @@ -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"; +import { SponsorsController } from "../controllers/sponsors"; -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); +const sponsorsController = container.get(TYPES.SponsorsController); // 用户相关路由 -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)); + +// 获取赞助者列表 +router.get("/sponsors", (req, res) => sponsorsController.get(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/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 4b4543e..d4245e4 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 { ShareInfoResponse, FolderListResponse, SaveFileParams } from "../types/cloud"; +import { injectable } from "inversify"; +import { Request } from "express"; +import UserSetting from "../models/UserSetting"; +import { ICloudStorageService } from "@/types/services"; +import { logger } from "../utils/logger"; interface Cloud115ListItem { cid: string; @@ -15,16 +19,12 @@ interface Cloud115FolderItem { ns: number; } -interface Cloud115PathItem { - cid: string; - name: string; -} - -export class Cloud115Service { +@injectable() +export class Cloud115Service implements ICloudStorageService { private api: AxiosInstance; private cookie: string = ""; - constructor(cookie?: string) { + constructor() { this.api = createAxiosInstance( "https://webapi.115.com", AxiosHeaders.from({ @@ -44,19 +44,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 { @@ -71,19 +75,21 @@ export class Cloud115Service { }); 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, @@ -114,32 +120,27 @@ export class Cloud115Service { })), }; } else { - Logger.error("获取目录列表失败:", response.data.error); + logger.error("获取目录列表失败:", response.data.error); throw new Error("获取115pan目录列表失败:" + response.data.error); } } - 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); + 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..ec447b9 --- /dev/null +++ b/backend/src/services/DatabaseService.ts @@ -0,0 +1,62 @@ +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 = sequelize; + } + + 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"); + 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 '\\'", + { 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..1f59ff5 --- /dev/null +++ b/backend/src/services/ImageService.ts @@ -0,0 +1,68 @@ +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() { + // 移除构造函数中的初始化,改为懒加载 + } + + private async ensureAxiosInstance(): Promise { + if (!this.axiosInstance) { + const settings = await GlobalSetting.findOne(); + const globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes); + + 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, + }); + + 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 { + 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 2173bdf..d5b3f8a 100644 --- a/backend/src/services/QuarkService.ts +++ b/backend/src/services/QuarkService.ts @@ -1,6 +1,16 @@ 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"; +import { + ShareInfoResponse, + FolderListResponse, + QuarkFolderItem, + SaveFileParams, +} from "../types/cloud"; +import { ICloudStorageService } from "@/types/services"; interface QuarkShareInfo { stoken?: string; @@ -14,17 +24,12 @@ interface QuarkShareInfo { }[]; } -interface QuarkFolderItem { - fid: string; - file_name: string; - file_type: number; -} - -export class QuarkService { +@injectable() +export class QuarkService implements ICloudStorageService { private api: AxiosInstance; private cookie: string = ""; - constructor(cookie?: string) { + constructor() { this.api = createAxiosInstance( "https://drive-h.quark.cn", AxiosHeaders.from({ @@ -41,22 +46,26 @@ 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 }> { + 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()}`, { @@ -76,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", @@ -117,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", @@ -148,24 +155,25 @@ export class QuarkService { }; } else { const message = "获取夸克目录列表失败:" + response.data.error; - Logger.error(message); + logger.error(message); throw new Error(message); } } - 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 3dcca04..91c9bdc 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,23 @@ interface sourceItem { cloudType?: string; } +@injectable() export class Searcher { - private axiosInstance: AxiosInstance | null = null; + private static instance: Searcher; + private api: AxiosInstance | null = null; constructor() { - this.initializeAxiosInstance(); + this.initAxiosInstance(); + Searcher.instance = this; } - private async initializeAxiosInstance(isUpdate = false): Promise { - let settings = null; + private async initAxiosInstance(isUpdate: boolean = false) { + let globalSetting = {} as GlobalSettingAttributes; if (isUpdate) { - settings = await GlobalSetting.findOne(); + const settings = await GlobalSetting.findOne(); + globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes); } - const globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes); - this.axiosInstance = createAxiosInstance( + this.api = createAxiosInstance( config.telegram.baseUrl, AxiosHeaders.from({ accept: @@ -56,8 +60,9 @@ export class Searcher { : undefined ); } - public async updateAxiosInstance() { - await this.initializeAxiosInstance(true); + + public static async updateAxiosInstance(): Promise { + await Searcher.instance.initAxiosInstance(true); } private extractCloudLinks(text: string): { links: string[]; cloudType: string } { @@ -67,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 { @@ -111,7 +116,7 @@ export class Searcher { } }); } catch (error) { - Logger.error(`搜索频道 ${channel.name} 失败:`, error); + logger.error(`搜索频道 ${channel.name} 失败:`, error); } }); @@ -125,10 +130,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 +210,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..98e75b4 --- /dev/null +++ b/backend/src/services/SettingService.ts @@ -0,0 +1,59 @@ +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无效"); + } + + 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: string | 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 this.updateSettings(); + return { message: "保存成功" }; + } + + async updateSettings(/* 参数 */): Promise { + // ... 其他代码 ... + + // 修改这一行,使用注入的实例方法而不是静态方法 + await this.imageService.updateAxiosInstance(); + await Searcher.updateAxiosInstance(); + + // ... 其他代码 ... + } +} diff --git a/backend/src/services/SponsorsService.ts b/backend/src/services/SponsorsService.ts new file mode 100644 index 0000000..fc18874 --- /dev/null +++ b/backend/src/services/SponsorsService.ts @@ -0,0 +1,25 @@ +import { injectable } from "inversify"; +import { createAxiosInstance } from "../utils/axiosInstance"; +import { AxiosInstance } from "axios"; +import sponsors from "../sponsors/sponsors.json"; + +@injectable() +export class SponsorsService { + private axiosInstance: AxiosInstance; + + constructor() { + this.axiosInstance = createAxiosInstance("http://oss.jiangmuxin.cn/cloudsaver/"); + } + async getSponsors() { + try { + const response = await this.axiosInstance.get("sponsors.json"); + return { + data: response.data.sponsors, + }; + } catch (error) { + return { + data: sponsors.sponsors, + }; + } + } +} diff --git a/backend/src/services/UserService.ts b/backend/src/services/UserService.ts new file mode 100644 index 0000000..06c0eb5 --- /dev/null +++ b/backend/src/services/UserService.ts @@ -0,0 +1,63 @@ +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/sponsors/sponsors.json b/backend/src/sponsors/sponsors.json new file mode 100644 index 0000000..ca00fa1 --- /dev/null +++ b/backend/src/sponsors/sponsors.json @@ -0,0 +1,54 @@ +{ + "sponsors": [ + { + "name": "立本狗头", + "avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks1.jpg", + "message": "怒搓楼上狗头! " + }, + { + "name": "帝国鼻屎", + "avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks2.jpg", + "message": "芜湖起飞! " + }, + { + "name": "雷霆222", + "avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks3.jpg", + "message": "把我弄帅点 " + }, + { + "name": "黑田奈奈子", + "avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks4.jpg", + "message": "流年笑掷 未来可期 ", + "link": "https://github.com/htnanako" + }, + { + "name": "原野🐇", + "avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks5.jpg" + }, + { + "name": "我摆烂!", + "avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks6.jpg", + "message": "人生苦短,及时行乐,卷什么卷,随缘摆烂 " + }, + { + "name": "田培", + "avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks7.jpg" + }, + { + "name": "River", + "avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks8.jpg" + }, + { + "name": "午夜学徒", + "avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks9.jpg" + }, + { + "name": "阿潘", + "avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks10.jpg" + }, + { + "name": "闹闹黑", + "avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks11.jpg" + } + ] +} diff --git a/backend/src/types/cloud.ts b/backend/src/types/cloud.ts new file mode 100644 index 0000000..4b428f1 --- /dev/null +++ b/backend/src/types/cloud.ts @@ -0,0 +1,83 @@ +export interface ShareInfoResponse { + data: { + 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; + name: string; + path: { cid: string; name: string }[]; + }[]; +} + +export interface SaveFileParams { + 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/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..f07fba4 --- /dev/null +++ b/backend/src/types/services.ts @@ -0,0 +1,9 @@ +import { Request } from "express"; +import { ShareInfoResponse, FolderListResponse, SaveFileParams } from "./cloud"; + +export interface ICloudStorageService { + setCookie(req: Request): Promise; + getShareInfo(shareCode: string, receiveCode?: string): Promise; + getFolderList(parentCid?: string): Promise; + saveSharedFile(params: SaveFileParams): Promise; +} diff --git a/backend/src/utils/axiosInstance.ts b/backend/src/utils/axiosInstance.ts index 3626a26..90a6df5 100644 --- a/backend/src/utils/axiosInstance.ts +++ b/backend/src/utils/axiosInstance.ts @@ -8,12 +8,11 @@ interface ProxyConfig { export function createAxiosInstance( baseURL: string, - headers: AxiosRequestHeaders, + headers?: AxiosRequestHeaders, useProxy: boolean = false, proxyConfig?: ProxyConfig ): AxiosInstance { let agent; - console.log(proxyConfig); if (useProxy && proxyConfig) { agent = tunnel.httpsOverHttp({ proxy: proxyConfig, diff --git a/backend/src/utils/handleError.ts b/backend/src/utils/handleError.ts index 2923ca5..3a5268d 100644 --- a/backend/src/utils/handleError.ts +++ b/backend/src/utils/handleError.ts @@ -1,5 +1,5 @@ import { Response, NextFunction } from "express"; -import { Logger } from "../utils/logger"; +import { logger } from "../utils/logger"; interface CustomError { name?: string; @@ -13,6 +13,6 @@ export default function handleError( message: string, next: NextFunction ) { - Logger.error(message, error); + logger.error(message, error); next(error || { success: false, message }); } diff --git a/backend/src/utils/logger.ts b/backend/src/utils/logger.ts index cca5e9a..1770bdd 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/frontend/package-lock.json b/frontend/package-lock.json index 28b3e1c..3f6511b 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.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cloud-disk-web", - "version": "0.2.3", + "version": "0.2.5", "dependencies": { "axios": "^1.6.7", "element-plus": "^2.6.1", diff --git a/frontend/package.json b/frontend/package.json index 9dd137a..316175e 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.5", "type": "module", "scripts": { "dev": "vite --host", @@ -12,8 +12,10 @@ "@element-plus/icons-vue": "^2.3.1", "axios": "^1.6.7", "element-plus": "^2.6.1", + "gsap": "^3.12.7", "pinia": "^2.1.7", "socket.io-client": "^4.8.1", + "typeit": "^8.8.7", "vant": "^4.9.17", "vue": "^3.4.21", "vue-router": "^4.3.0" 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 68bebb5..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(pwdId: string, passcode = "") { + async getShareInfo(params: GetShareInfoParams) { const { data } = await request.get("/api/quark/share-info", { - params: { pwdId, passcode }, + 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/api/user.ts b/frontend/src/api/user.ts index c9ee255..c6631d2 100644 --- a/frontend/src/api/user.ts +++ b/frontend/src/api/user.ts @@ -7,4 +7,7 @@ export const userApi = { register: (data: { username: string; password: string; registerCode: string }) => { return request.post<{ token: string }>("/api/user/register", data); }, + getSponsors: () => { + return request.get("/api/sponsors?timestamp=" + Date.now()); + }, }; diff --git a/frontend/src/assets/images/default.png b/frontend/src/assets/images/default.png new file mode 100644 index 0000000..583c09f Binary files /dev/null and b/frontend/src/assets/images/default.png differ diff --git a/frontend/src/components/AsideMenu.vue b/frontend/src/components/AsideMenu.vue index 2e2efff..983a6da 100644 --- a/frontend/src/components/AsideMenu.vue +++ b/frontend/src/components/AsideMenu.vue @@ -45,12 +45,7 @@