diff --git a/packages/backend/package.json b/packages/backend/package.json index e69bae8..73a712c 100755 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "cloudsearch-backend", - "version": "0.0.2", + "version": "0.0.3", "private": true, "scripts": { "dev": "tsx watch src/main.ts", diff --git a/packages/backend/src/cloud/credential.service.ts b/packages/backend/src/cloud/credential.service.ts index 242c7ce..17b8faf 100644 --- a/packages/backend/src/cloud/credential.service.ts +++ b/packages/backend/src/cloud/credential.service.ts @@ -33,6 +33,7 @@ export interface CloudConfig { is_active: number; promotion_account?: string; is_transfer_enabled: number; + is_primary: number; storage_used?: string; storage_total?: string; checkin_status: string; // 'none'|'success'|'failed'|'pending'|'skipped' @@ -70,7 +71,7 @@ function extractQuarkUid(cookie: string): string | null { export function getCloudConfigs(): CloudConfig[] { const db = getDb(); return db.prepare( - `SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, storage_used, storage_total, + `SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, is_primary, storage_used, storage_total, cloud_type_uid, checkin_status, last_checkin_at, checkin_message, consecutive_failures, last_used_at, total_saves, created_at, updated_at, verification_status @@ -81,7 +82,7 @@ export function getCloudConfigs(): CloudConfig[] { export function getAvailableClouds(): CloudConfig[] { const db = getDb(); return db.prepare( - `SELECT id, cloud_type, nickname, is_active, promotion_account, is_transfer_enabled, storage_used, storage_total, + `SELECT id, cloud_type, nickname, is_active, promotion_account, is_transfer_enabled, is_primary, storage_used, storage_total, cloud_type_uid, checkin_status, last_checkin_at, checkin_message, consecutive_failures, last_used_at, total_saves, created_at, updated_at @@ -93,7 +94,7 @@ export function getAvailableClouds(): CloudConfig[] { export function getCloudConfigByType(cloudType: string): CloudConfig | undefined { const db = getDb(); return db.prepare( - `SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, storage_used, storage_total, + `SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, is_primary, storage_used, storage_total, cloud_type_uid, checkin_status, last_checkin_at, checkin_message, consecutive_failures, last_used_at, total_saves, created_at, updated_at, verification_status @@ -105,7 +106,7 @@ export function getCloudConfigByType(cloudType: string): CloudConfig | undefined export function getCloudConfigById(id: number): CloudConfig | undefined { const db = getDb(); return db.prepare( - `SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, storage_used, storage_total, + `SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, is_primary, storage_used, storage_total, cloud_type_uid, checkin_status, last_checkin_at, checkin_message, consecutive_failures, last_used_at, total_saves, created_at, updated_at, verification_status @@ -117,7 +118,7 @@ export function getCloudConfigById(id: number): CloudConfig | undefined { export function getActiveCloudConfigs(): CloudConfig[] { const db = getDb(); return db.prepare( - `SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, storage_used, storage_total, + `SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, is_primary, storage_used, storage_total, cloud_type_uid, checkin_status, last_checkin_at, checkin_message, consecutive_failures, last_used_at, total_saves, created_at, updated_at @@ -126,6 +127,31 @@ export function getActiveCloudConfigs(): CloudConfig[] { ).all() as CloudConfig[]; } +/** + * Toggle the is_primary flag for a cloud config. + * Enforces max 2 primary accounts per cloud type. + */ +export function togglePrimary(id: number, setPrimary: boolean): CloudConfig { + const db = getDb(); + const config = getCloudConfigById(id); + if (!config) throw new Error(`Cloud config ${id} not found`); + + if (setPrimary) { + // Check how many primary accounts already exist for this cloud type + const primaryCount = db.prepare( + `SELECT COUNT(*) as c FROM cloud_configs WHERE cloud_type = ? AND is_primary = 1 AND id != ?` + ).get(config.cloud_type, id) as { c: number }; + if (primaryCount.c >= 2) { + throw new Error(`同类型网盘最多只能设置 2 个默认账号(已存在 ${primaryCount.c} 个)`); + } + } + + db.prepare(`UPDATE cloud_configs SET is_primary = ?, updated_at = datetime('now', 'localtime') WHERE id = ?`) + .run(setPrimary ? 1 : 0, id); + + return getCloudConfigById(id)!; +} + export function saveCloudConfig(data: { id?: number; cloud_type: string; @@ -199,7 +225,7 @@ export function saveCloudConfig(data: { // Re-read savedId for return const savedId = existing.id; return db.prepare( - `SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, storage_used, storage_total, + `SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, is_primary, storage_used, storage_total, cloud_type_uid, checkin_status, last_checkin_at, checkin_message, consecutive_failures, last_used_at, total_saves, created_at, updated_at @@ -215,7 +241,7 @@ export function saveCloudConfig(data: { const savedId = data.id || (db.prepare('SELECT last_insert_rowid() as id').get() as any).id; return db.prepare( - `SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, storage_used, storage_total, + `SELECT id, cloud_type, cookie, nickname, is_active, promotion_account, is_transfer_enabled, is_primary, storage_used, storage_total, cloud_type_uid, checkin_status, last_checkin_at, checkin_message, consecutive_failures, last_used_at, total_saves, created_at, updated_at @@ -408,7 +434,7 @@ export async function getAndValidateCredential(cloudType: string, ipAddress?: st `SELECT * FROM cloud_configs WHERE cloud_type = ? AND is_active = 1 AND consecutive_failures < 5 - ORDER BY last_used_at ASC NULLS FIRST + ORDER BY is_primary DESC, last_used_at ASC NULLS FIRST LIMIT 1` ).get(cloudType) as CloudConfig | undefined; } else { @@ -429,12 +455,12 @@ export async function getAndValidateCredential(cloudType: string, ipAddress?: st const ipTodayCount = ipCountRow?.total || 0; if (ipTodayCount < 3) { - // First 2 saves — use the primary account (least recently used, healthy) + // First 2 saves — use a primary account (is_primary=1), fallback to any 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 + ORDER BY is_primary DESC, last_used_at ASC NULLS FIRST LIMIT 1` ).get(cloudType) as CloudConfig | undefined; } else { @@ -464,7 +490,7 @@ export async function getAndValidateCredential(cloudType: string, ipAddress?: st `SELECT * FROM cloud_configs WHERE cloud_type = ? AND is_active = 1 AND consecutive_failures < 5 - ORDER BY last_used_at ASC NULLS FIRST + ORDER BY is_primary DESC, last_used_at ASC NULLS FIRST LIMIT 1` ).get(cloudType) as CloudConfig | undefined; } diff --git a/packages/backend/src/database/database.ts b/packages/backend/src/database/database.ts index f95e11d..f07b500 100755 --- a/packages/backend/src/database/database.ts +++ b/packages/backend/src/database/database.ts @@ -254,6 +254,13 @@ function migrateCloudConfigs(db: Database.Database): void { db.exec("ALTER TABLE cloud_configs ADD COLUMN is_transfer_enabled INTEGER DEFAULT 1"); console.log('[DB] cloud_configs migration: is_transfer_enabled column added'); } + + // Migration 6: Add is_primary column (default account, max 2 per cloud type) + const row6 = db.prepare("SELECT sql FROM sqlite_master WHERE name='cloud_configs' AND sql LIKE '%is_primary%'").get(); + if (!row6) { + db.exec("ALTER TABLE cloud_configs ADD COLUMN is_primary INTEGER DEFAULT 0"); + console.log('[DB] cloud_configs migration: is_primary column added'); + } } function seedAdmin(db: Database.Database): void { @@ -267,6 +274,12 @@ function seedAdmin(db: Database.Database): void { 'INSERT INTO admins (username, password_hash) VALUES (?, ?)' ).run(config.adminUsername, hash); + // Migration 6: Add is_primary column (default account, max 2 per cloud type) + const row6 = db.prepare("SELECT sql FROM sqlite_master WHERE name='cloud_configs' AND sql LIKE %'is_primary%'").get(); + if (!row6) { + db.exec("ALTER TABLE cloud_configs ADD COLUMN is_primary INTEGER DEFAULT 0"); + console.log("[DB] cloud_configs migration: is_primary column added"); + } console.log(`[DB] Admin user "${config.adminUsername}" created`); } diff --git a/packages/backend/src/routes/admin.routes.ts b/packages/backend/src/routes/admin.routes.ts index b048162..5ff548c 100644 --- a/packages/backend/src/routes/admin.routes.ts +++ b/packages/backend/src/routes/admin.routes.ts @@ -4,7 +4,7 @@ import fs from "fs"; import { execSync } from 'child_process'; import { adminLimiter, loginLimiter } from '../middleware/rate-limit'; import { getSaveRecords } from '../cloud/cloud.service'; -import { getCloudConfigs, getCloudConfigById, saveCloudConfig, deleteCloudConfig, getCloudConfigByType, testCloudConnection, testCloudConnectionWithCookie } from '../cloud/credential.service'; +import { getCloudConfigs, getCloudConfigById, saveCloudConfig, deleteCloudConfig, getCloudConfigByType, testCloudConnection, testCloudConnectionWithCookie, togglePrimary } from '../cloud/credential.service'; // Note: check-in routes were removed (sign-in feature removed) import { getAllCloudTypes } from '../cloud/cloud-types.service'; import { login, authMiddleware, verifyToken, changePassword } from '../admin/auth.service'; @@ -199,6 +199,20 @@ router.delete('/admin/cloud-configs/:id', (req: Request, res: Response) => { } }); +/** + * PUT /api/admin/cloud-configs/:id/primary — toggle primary status (max 2 per type) + */ +router.put('/admin/cloud-configs/:id/primary', (req: Request, res: Response) => { + try { + const id = parseInt(req.params.id as string); + const { primary } = req.body; + const config = togglePrimary(id, !!primary); + res.json(config); + } catch (err: any) { + res.status(400).json({ error: err.message || 'Failed to toggle primary status' }); + } +}); + /** POST /api/admin/cloud-configs/:type/test — test cloud connection (by type or id) */ router.post('/admin/cloud-configs/:type/test', async (req: Request, res: Response) => { try { diff --git a/packages/frontend/src/api/index.ts b/packages/frontend/src/api/index.ts index 5747b5b..0d464e9 100755 --- a/packages/frontend/src/api/index.ts +++ b/packages/frontend/src/api/index.ts @@ -267,6 +267,14 @@ export async function deleteCloudConfig( await api.delete(`/admin/cloud-configs/${id}`) } +export async function setPrimary( + id: number, + primary: boolean +): Promise { + const { data } = await api.put(`/admin/cloud-configs/${id}/primary`, { primary }) + return data +} + export async function getStats(days?: number): Promise { const params: Record = {} if (days) params.days = days diff --git a/packages/frontend/src/pages/admin/CloudConfig.vue b/packages/frontend/src/pages/admin/CloudConfig.vue index 2e97f25..fa1238e 100755 --- a/packages/frontend/src/pages/admin/CloudConfig.vue +++ b/packages/frontend/src/pages/admin/CloudConfig.vue @@ -99,6 +99,16 @@ /> + + +