diff --git a/packages/backend/src/cloud/cloud.service.ts b/packages/backend/src/cloud/cloud.service.ts index a7b49ea..f2ce91a 100644 --- a/packages/backend/src/cloud/cloud.service.ts +++ b/packages/backend/src/cloud/cloud.service.ts @@ -141,7 +141,7 @@ async function doSaveFromShare(shareUrl: string, cloudType: string, sourceTitle? } // ── Unified credential validation ── - const credential = await getAndValidateCredential(cloudType); + const credential = await getAndValidateCredential(cloudType, ipAddress); if (!credential.valid || !credential.config) { return { success: false, message: credential.message }; } diff --git a/packages/backend/src/cloud/credential.service.ts b/packages/backend/src/cloud/credential.service.ts index 75dd6a0..242c7ce 100644 --- a/packages/backend/src/cloud/credential.service.ts +++ b/packages/backend/src/cloud/credential.service.ts @@ -397,16 +397,79 @@ export interface CredentialValidationResult { * * Reference: search-ucmao get_and_validate_credential() pattern. */ -export async function getAndValidateCredential(cloudType: string): Promise { +export async function getAndValidateCredential(cloudType: string, ipAddress?: string): Promise { const db = getDb(); - const config = db.prepare( - `SELECT * FROM cloud_configs - WHERE cloud_type = ? AND is_active = 1 - AND consecutive_failures < 5 - ORDER BY last_used_at ASC NULLS FIRST - LIMIT 1` - ).get(cloudType) as CloudConfig | undefined; + let config: CloudConfig | undefined; + + if (!ipAddress) { + // No IP info — fallback to simple LUR + config = db.prepare( + `SELECT * FROM cloud_configs + WHERE cloud_type = ? AND is_active = 1 + AND consecutive_failures < 5 + ORDER BY last_used_at ASC NULLS FIRST + LIMIT 1` + ).get(cloudType) as CloudConfig | undefined; + } else { + // Get today's date in Shanghai time + const today = (() => { + const now = new Date(); + const shanghai = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Shanghai' })); + return shanghai.toISOString().slice(0, 10); + })(); + + // Count how many times this IP has saved today for this cloud type + const ipCountRow = db.prepare( + `SELECT COALESCE(SUM(save_count), 0) as total + FROM ip_daily_save_counts + WHERE ip_address = ? AND date = ? AND cloud_type = ?` + ).get(ipAddress, today, cloudType) as { total: number }; + + const ipTodayCount = ipCountRow?.total || 0; + + if (ipTodayCount < 3) { + // First 2 saves — use the primary account (least recently used, healthy) + config = db.prepare( + `SELECT * FROM cloud_configs + WHERE cloud_type = ? AND is_active = 1 + AND consecutive_failures < 5 + ORDER BY last_used_at ASC NULLS FIRST + LIMIT 1` + ).get(cloudType) as CloudConfig | undefined; + } else { + // 3rd+ save — exclude accounts this IP has already used today, + // fall back to other available accounts round-robin + const usedConfigIds = db.prepare( + `SELECT DISTINCT config_id FROM ip_daily_save_counts + WHERE ip_address = ? AND date = ? AND cloud_type = ? + ORDER BY config_id` + ).all(ipAddress, today, cloudType) as { config_id: number }[]; + + const usedIds = usedConfigIds.map(r => r.config_id); + const placeholders = usedIds.length > 0 ? usedIds.map(() => '?').join(',') : '-1'; + + config = db.prepare( + `SELECT * FROM cloud_configs + WHERE cloud_type = ? AND is_active = 1 + AND consecutive_failures < 5 + AND id NOT IN (${placeholders}) + ORDER BY last_used_at ASC NULLS FIRST + LIMIT 1` + ).get(cloudType, ...usedIds) as CloudConfig | undefined; + + // If all accounts have been used by this IP, fall back to primary + if (!config) { + config = db.prepare( + `SELECT * FROM cloud_configs + WHERE cloud_type = ? AND is_active = 1 + AND consecutive_failures < 5 + ORDER BY last_used_at ASC NULLS FIRST + LIMIT 1` + ).get(cloudType) as CloudConfig | undefined; + } + } + } if (!config) { return { @@ -457,6 +520,21 @@ export async function getAndValidateCredential(cloudType: string): Promise { + const now = new Date(); + const shanghai = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Shanghai' })); + return shanghai.toISOString().slice(0, 10); + })(); + db.prepare( + `INSERT INTO ip_daily_save_counts (ip_address, date, cloud_type, config_id, save_count) + VALUES (?, ?, ?, ?, 1) + ON CONFLICT(ip_address, date, cloud_type, config_id) + DO UPDATE SET save_count = save_count + 1` + ).run(ipAddress, today, cloudType, config.id); + } + return { valid: true, config: { ...config, cookie: decryptedCookie }, diff --git a/packages/backend/src/database/database.ts b/packages/backend/src/database/database.ts index 1310fb7..f95e11d 100755 --- a/packages/backend/src/database/database.ts +++ b/packages/backend/src/database/database.ts @@ -119,6 +119,16 @@ function runMigrations(db: Database.Database): void { source TEXT, updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) ); + + CREATE TABLE IF NOT EXISTS ip_daily_save_counts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ip_address TEXT NOT NULL, + date TEXT NOT NULL, + cloud_type TEXT NOT NULL, + config_id INTEGER NOT NULL, + save_count INTEGER NOT NULL DEFAULT 0, + UNIQUE(ip_address, date, cloud_type, config_id) + ); `); seedSystemConfigs(db); migrateSaveRecords(db);