From 329256bd33b5f8a5cf13863a4dc185b135a33139 Mon Sep 17 00:00:00 2001 From: admin <362324317@qq.com> Date: Fri, 15 May 2026 06:45:48 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=BD=AC=E5=AD=98=E6=97=B6=E5=85=88?= =?UTF-8?q?=E6=9F=A5=E8=B5=84=E6=BA=90=E5=8E=86=E5=8F=B2,=20=E5=A4=8D?= =?UTF-8?q?=E7=94=A8=E5=8E=9F=E8=B4=A6=E5=8F=B7;=20save=5Frecords=E5=8A=A0?= =?UTF-8?q?config=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 资源维度优先级 > IP维度: 先查share_url是否被转存过 - save_records 表新增 config_id 字段 + 写入时记录 - cloud.service.ts 所有 INSERT 写入 config.id - credential.service.ts: getAndValidateCredential 加 shareUrl 参数 - 数据库 migration: config_id 到 save_records --- packages/backend/src/cloud/cloud.service.ts | 18 ++++++------ .../backend/src/cloud/credential.service.ts | 29 ++++++++++++++++++- packages/backend/src/database/database.ts | 29 +++++-------------- 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/packages/backend/src/cloud/cloud.service.ts b/packages/backend/src/cloud/cloud.service.ts index f2ce91a..70e488d 100644 --- a/packages/backend/src/cloud/cloud.service.ts +++ b/packages/backend/src/cloud/cloud.service.ts @@ -70,14 +70,14 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle? if (alreadySaved && recentRecord.share_url) { console.log(`[Share] 🛡️ Dedup: ${shareUrl} was saved ${DEDUP_WINDOW_SEC}s ago (status=${recentRecord.status}), returning existing share link`); db.prepare( - `INSERT INTO save_records (source_type, source_title, source_url, target_cloud, share_url, share_pwd, file_size, file_count, folder_count, duration_ms, status, error_message, folder_name, original_folder_name, ip_address, ip_location, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + `INSERT INTO save_records (source_type, source_title, source_url, target_cloud, share_url, share_pwd, file_size, file_count, folder_count, duration_ms, status, error_message, folder_name, original_folder_name, ip_address, ip_location, created_at, config_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ).run( cloudType, sourceTitle || null, shareUrl, cloudType, recentRecord.share_url, recentRecord.share_pwd || null, null, 0, 0, 0, 'reused', null, recentRecord.folder_name || null, recentRecord.original_folder_name || null, - ipAddress || null, ipLocation, localTimestamp(), + ipAddress || null, ipLocation, localTimestamp(), null, ); return { success: true, @@ -141,7 +141,7 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle? } // ── Unified credential validation ── - const credential = await getAndValidateCredential(cloudType, ipAddress); + const credential = await getAndValidateCredential(cloudType, ipAddress, shareUrl); if (!credential.valid || !credential.config) { return { success: false, message: credential.message }; } @@ -189,8 +189,8 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle? } db.prepare( - `INSERT INTO save_records (source_type, source_title, source_url, target_cloud, share_url, share_pwd, file_size, file_count, folder_count, duration_ms, status, error_message, folder_name, original_folder_name, ip_address, ip_location, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + `INSERT INTO save_records (source_type, source_title, source_url, target_cloud, share_url, share_pwd, file_size, file_count, folder_count, duration_ms, status, error_message, folder_name, original_folder_name, ip_address, ip_location, created_at, config_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ).run( cloudType, sourceTitle || driverResult.folderName || null, shareUrl, cloudType, driverResult.shareUrl || null, driverResult.sharePwd || null, @@ -198,7 +198,7 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle? durationMs, driverResult.success ? 'success' : 'failed', driverResult.success ? null : driverResult.message, driverResult.folderName || null, driverResult.originalFolderName || null, - ipAddress || null, ipLocation, localTimestamp(), + ipAddress || null, ipLocation, localTimestamp(), config.id ); return { @@ -221,9 +221,9 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle? ).run(config.id); db.prepare( - `INSERT INTO save_records (source_type, source_url, target_cloud, duration_ms, status, error_message, ip_address, ip_location, created_at) + `INSERT INTO save_records (source_type, source_url, target_cloud, duration_ms, status, error_message, ip_address, ip_location, created_at, config_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` - ).run(cloudType, shareUrl, cloudType, durationMs, 'failed', errorMessage, ipAddress || null, ipLocation, localTimestamp()); + ).run(cloudType, shareUrl, cloudType, durationMs, 'failed', errorMessage, ipAddress || null, ipLocation, localTimestamp(), null); return { success: false, message: errorMessage }; } diff --git a/packages/backend/src/cloud/credential.service.ts b/packages/backend/src/cloud/credential.service.ts index 17b8faf..bdfa923 100644 --- a/packages/backend/src/cloud/credential.service.ts +++ b/packages/backend/src/cloud/credential.service.ts @@ -423,11 +423,38 @@ export interface CredentialValidationResult { * * Reference: search-ucmao get_and_validate_credential() pattern. */ -export async function getAndValidateCredential(cloudType: string, ipAddress?: string): Promise { +export async function getAndValidateCredential(cloudType: string, ipAddress?: string, shareUrl?: string): Promise { const db = getDb(); let config: CloudConfig | undefined; + // ── Resource history lookup: if this share URL was saved before, reuse that account ── + if (shareUrl) { + const historyRecord = db.prepare( + `SELECT config_id, target_cloud, folder_name FROM save_records + WHERE share_url = ? AND target_cloud = ? AND status IN ('success', 'reused') + ORDER BY id DESC LIMIT 1` + ).get(shareUrl, cloudType) as { config_id: number; target_cloud: string; folder_name: string } | undefined; + + if (historyRecord) { + // Resource was previously saved — reuse the exact same config if still healthy + if (historyRecord.config_id) { + config = db.prepare( + `SELECT * FROM cloud_configs WHERE id = ? AND is_active = 1 AND consecutive_failures < 5` + ).get(historyRecord.config_id) as CloudConfig | undefined; + } + if (!config) { + // Fallback: pick any healthy account from this cloud type + config = db.prepare( + `SELECT * FROM cloud_configs + WHERE cloud_type = ? AND is_active = 1 AND consecutive_failures < 5 + ORDER BY is_primary DESC, last_used_at ASC NULLS FIRST + LIMIT 1` + ).get(cloudType) as CloudConfig | undefined; + } + } + } + if (!ipAddress) { // No IP info — fallback to simple LUR config = db.prepare( diff --git a/packages/backend/src/database/database.ts b/packages/backend/src/database/database.ts index f07b500..29cace1 100755 --- a/packages/backend/src/database/database.ts +++ b/packages/backend/src/database/database.ts @@ -138,27 +138,6 @@ function runMigrations(db: Database.Database): void { } /** 迁移: 给已有 save_records 表补充新列 */ -function migrateSaveRecords(db: Database.Database): void { - const newCols: { col: string; def: string }[] = [ - { col: 'share_pwd', def: 'TEXT' }, - { col: 'file_count', def: 'INTEGER DEFAULT 0' }, - { col: 'folder_count', def: 'INTEGER DEFAULT 0' }, - { col: 'duration_ms', def: 'INTEGER DEFAULT 0' }, - { col: 'status', def: "TEXT NOT NULL DEFAULT ''" }, - { col: 'error_message', def: 'TEXT' }, - { col: 'folder_name', def: 'TEXT' }, - { col: 'request_url', def: 'TEXT' }, - { col: 'ip_location', def: 'TEXT' }, - { col: 'original_folder_name', def: 'TEXT' }, - ]; - for (const { col, def } of newCols) { - try { - db.exec(`ALTER TABLE save_records ADD COLUMN ${col} ${def}`); - } catch { - // Column already exists — ignore - } - } -} /** 迁移: 给 content_cache 表加 douban_url 列 */ function migrateContentCache(db: Database.Database): void { @@ -263,6 +242,14 @@ function migrateCloudConfigs(db: Database.Database): void { } } +/** 迁移: 给 save_records 表加 config_id 字段 */ +function migrateSaveRecords(db: any): void { + if (!db.prepare("SELECT sql FROM sqlite_master WHERE name='save_records' AND sql LIKE '%config_id%'").get()) { + db.exec("ALTER TABLE save_records ADD COLUMN config_id INTEGER DEFAULT NULL"); + console.log('[DB] save_records migration: config_id column added'); + } +} + function seedAdmin(db: Database.Database): void { const existing = db.prepare('SELECT id FROM admins WHERE username = ?').get(config.adminUsername); if (existing) return;