feat: 网盘设置加「默认账号」列, 同类型最多2个主账号

- 数据库 migration: cloud_configs 加 is_primary 字段
- 后端: togglePrimary API (PUT /admin/cloud-configs/:id/primary)
- 后端: getAndValidateCredential 优先选 is_primary 账号
- 前端: CloudConfig.vue 转存启用后加「默认账号」开关列
- 前端: api/index.ts 加 setPrimary 方法
This commit is contained in:
2026-05-15 06:39:08 +08:00
parent abd0cb26f5
commit 58caaae37a
6 changed files with 95 additions and 14 deletions

View File

@@ -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;
}

View File

@@ -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`);
}

View File

@@ -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 {