feat: IP-based daily account priority for save
- Added ip_daily_save_counts table to track per-IP daily usage - getAndValidateCredential now accepts ipAddress parameter - First 2 saves by same IP use primary account; 3rd+ round-robins to other accounts - Counter increments on each successful credential acquisition - All timestamps in Asia/Shanghai timezone
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -397,16 +397,79 @@ export interface CredentialValidationResult {
|
||||
*
|
||||
* Reference: search-ucmao get_and_validate_credential() pattern.
|
||||
*/
|
||||
export async function getAndValidateCredential(cloudType: string): Promise<CredentialValidationResult> {
|
||||
export async function getAndValidateCredential(cloudType: string, ipAddress?: string): Promise<CredentialValidationResult> {
|
||||
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<Crede
|
||||
};
|
||||
}
|
||||
|
||||
// Track IP daily usage count (if ipAddress provided)
|
||||
if (ipAddress && config) {
|
||||
const today = (() => {
|
||||
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 },
|
||||
|
||||
Reference in New Issue
Block a user