v0.2.7: 修复Redis连接 + 启动管理后台
- 修复Redis认证 (配置密码) - 启动Python管理后台 (端口9531, 15个功能开关) - 统一版本号 0.2.7 - 更新docker-compose.yml (镜像版本/Redis URL/Admin服务)
This commit is contained in:
327
source_clean/src/database/database.ts
Executable file
327
source_clean/src/database/database.ts
Executable file
@@ -0,0 +1,327 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import path from 'path';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import config from '../config';
|
||||
import { formatLocalDateTime } from '../utils/time';
|
||||
|
||||
let db: Database.Database | null = null;
|
||||
|
||||
export function getDb(): Database.Database {
|
||||
if (db) return db;
|
||||
|
||||
const dbDir = path.dirname(config.dbPath);
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync(dbDir)) {
|
||||
fs.mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
db = new Database(config.dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
// Performance indexes (IF NOT EXISTS ensures idempotent)
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_cc_type_active ON cloud_configs(cloud_type, is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_cc_uid ON cloud_configs(cookie_uid);
|
||||
CREATE INDEX IF NOT EXISTS idx_cc_verification ON cloud_configs(verification_status);
|
||||
`);
|
||||
|
||||
runMigrations(db);
|
||||
seedAdmin(db);
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
function runMigrations(db: Database.Database): void {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS admins (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
|
||||
last_login TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cloud_configs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cloud_type TEXT NOT NULL,
|
||||
cookie TEXT,
|
||||
nickname TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
storage_used TEXT,
|
||||
storage_total TEXT,
|
||||
checkin_status TEXT NOT NULL DEFAULT 'none',
|
||||
last_checkin_at TEXT,
|
||||
checkin_message TEXT,
|
||||
consecutive_failures INTEGER DEFAULT 0,
|
||||
last_used_at TEXT,
|
||||
total_saves INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS promotions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
image_url TEXT,
|
||||
link_url TEXT,
|
||||
position TEXT,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
click_count INTEGER NOT NULL DEFAULT 0,
|
||||
start_time TEXT,
|
||||
end_time TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS save_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source_type TEXT,
|
||||
source_title TEXT,
|
||||
source_url TEXT,
|
||||
target_cloud TEXT,
|
||||
share_url TEXT,
|
||||
share_pwd TEXT,
|
||||
file_size TEXT,
|
||||
file_count INTEGER DEFAULT 0,
|
||||
duration_ms INTEGER DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT '',
|
||||
error_message TEXT,
|
||||
ip_address TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS search_stats (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
keyword TEXT,
|
||||
intent TEXT,
|
||||
result_count INTEGER DEFAULT 0,
|
||||
ip_address TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hot_keywords (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
keyword TEXT UNIQUE NOT NULL,
|
||||
search_count INTEGER NOT NULL DEFAULT 1,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS system_configs (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL DEFAULT '',
|
||||
description TEXT,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS content_cache (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
keyword TEXT UNIQUE NOT NULL,
|
||||
title TEXT,
|
||||
description TEXT,
|
||||
tags TEXT,
|
||||
cover TEXT,
|
||||
source TEXT,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
`);
|
||||
seedSystemConfigs(db);
|
||||
migrateSaveRecords(db);
|
||||
migrateContentCache(db);
|
||||
migrateCloudConfigs(db);
|
||||
cleanupOldSaveRecords(db);
|
||||
}
|
||||
|
||||
/** 迁移: 给已有 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 {
|
||||
const columns: { col: string; def: string }[] = [
|
||||
{ col: 'douban_url', def: 'TEXT' },
|
||||
{ col: 'rating', def: 'TEXT' },
|
||||
{ col: 'rating_count', def: 'TEXT' },
|
||||
{ col: 'year', def: 'TEXT' },
|
||||
{ col: 'genres', def: 'TEXT' },
|
||||
{ col: 'directors', def: 'TEXT' },
|
||||
{ col: 'actors', def: 'TEXT' },
|
||||
{ col: 'region', def: 'TEXT' },
|
||||
{ col: 'duration', def: 'TEXT' },
|
||||
];
|
||||
for (const { col, def } of columns) {
|
||||
try {
|
||||
db.exec(`ALTER TABLE content_cache ADD COLUMN ${col} ${def}`);
|
||||
} catch {
|
||||
// Column already exists — ignore
|
||||
}
|
||||
}
|
||||
// 修复旧记录:source 为 NULL 但实际有 TMDB 数据的,标记为 tmdb
|
||||
db.exec(`UPDATE content_cache SET source = 'tmdb' WHERE source IS NULL AND title IS NOT NULL AND title != ''`);
|
||||
}
|
||||
|
||||
/** 迁移: 给 cloud_configs 表去UNIQUE约束 + 加签到/轮训字段 */
|
||||
function migrateCloudConfigs(db: Database.Database): void {
|
||||
// 加新列
|
||||
const newCols: { col: string; def: string }[] = [
|
||||
{ col: 'checkin_status', def: "TEXT NOT NULL DEFAULT 'none'" },
|
||||
{ col: 'last_checkin_at', def: 'TEXT' },
|
||||
{ col: 'checkin_message', def: 'TEXT' },
|
||||
{ col: 'consecutive_failures', def: 'INTEGER DEFAULT 0' },
|
||||
{ col: 'last_used_at', def: 'TEXT' },
|
||||
{ col: 'total_saves', def: 'INTEGER DEFAULT 0' },
|
||||
];
|
||||
for (const { col, def } of newCols) {
|
||||
try { db.exec(`ALTER TABLE cloud_configs ADD COLUMN ${col} ${def}`); } catch {}
|
||||
}
|
||||
// 检查旧表是否有 UNIQUE 约束,有则重建表
|
||||
const row = db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='cloud_configs'`).get() as any;
|
||||
if (row && row.sql && row.sql.includes('cloud_type TEXT UNIQUE')) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS cloud_configs_v2 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cloud_type TEXT NOT NULL,
|
||||
cookie TEXT,
|
||||
nickname TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
storage_used TEXT,
|
||||
storage_total TEXT,
|
||||
checkin_status TEXT NOT NULL DEFAULT 'none',
|
||||
last_checkin_at TEXT,
|
||||
checkin_message TEXT,
|
||||
consecutive_failures INTEGER DEFAULT 0,
|
||||
last_used_at TEXT,
|
||||
total_saves INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
INSERT INTO cloud_configs_v2 (id, cloud_type, cookie, nickname, is_active, storage_used, storage_total, checkin_status, last_checkin_at, checkin_message, consecutive_failures, last_used_at, total_saves, created_at, updated_at)
|
||||
SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total, COALESCE(checkin_status,'none'), last_checkin_at, checkin_message, COALESCE(consecutive_failures,0), last_used_at, COALESCE(total_saves,0), created_at, updated_at FROM cloud_configs;
|
||||
DROP TABLE cloud_configs;
|
||||
ALTER TABLE cloud_configs_v2 RENAME TO cloud_configs;
|
||||
`);
|
||||
console.log('[DB] cloud_configs migration: UNIQUE constraint removed, new fields added');
|
||||
}
|
||||
|
||||
// Migration 2: Add verification_status column
|
||||
const row2 = db.prepare("SELECT sql FROM sqlite_master WHERE name='cloud_configs' AND sql LIKE '%verification_status%'").get();
|
||||
if (!row2) {
|
||||
db.exec("ALTER TABLE cloud_configs ADD COLUMN verification_status TEXT DEFAULT NULL");
|
||||
console.log('[DB] cloud_configs migration: verification_status column added');
|
||||
}
|
||||
|
||||
// Migration 3: Add cookie_uid column
|
||||
const hasCookieUid = db.prepare("SELECT sql FROM sqlite_master WHERE name='cloud_configs' AND sql LIKE '%cookie_uid%'").get();
|
||||
if (!hasCookieUid) {
|
||||
db.exec("ALTER TABLE cloud_configs ADD COLUMN cookie_uid TEXT DEFAULT NULL");
|
||||
console.log('[DB] cloud_configs migration: cookie_uid column added');
|
||||
}
|
||||
|
||||
// Migration 4: Add promotion_account column
|
||||
const hasPromotionAccount = db.prepare("SELECT sql FROM sqlite_master WHERE name='cloud_configs' AND sql LIKE '%promotion_account%'").get();
|
||||
if (!hasPromotionAccount) {
|
||||
db.exec("ALTER TABLE cloud_configs ADD COLUMN promotion_account TEXT DEFAULT NULL");
|
||||
console.log('[DB] cloud_configs migration: promotion_account column added');
|
||||
}
|
||||
}
|
||||
|
||||
function seedAdmin(db: Database.Database): void {
|
||||
const existing = db.prepare('SELECT id FROM admins WHERE username = ?').get(config.adminUsername);
|
||||
if (existing) return;
|
||||
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
const hash = bcrypt.hashSync(config.adminPassword, salt);
|
||||
|
||||
db.prepare(
|
||||
'INSERT INTO admins (username, password_hash) VALUES (?, ?)'
|
||||
).run(config.adminUsername, hash);
|
||||
|
||||
console.log(`[DB] Admin user "${config.adminUsername}" created`);
|
||||
}
|
||||
|
||||
function seedSystemConfigs(db: Database.Database): void {
|
||||
const defaults: { key: string; value: string; description: string }[] = [
|
||||
{ key: 'pansou_url', value: config.pansouUrl, description: 'PanSou 搜索引擎服务地址' },
|
||||
{ key: 'video_parser_url', value: config.videoParserUrl, description: '视频解析服务地址' },
|
||||
{ key: 'validation_concurrency', value: String(config.validation.concurrency), description: '链接验证并发数' },
|
||||
{ key: 'validation_timeout', value: String(config.validation.timeout), description: '链接验证超时(ms)' },
|
||||
{ key: 'validation_cache_ttl_valid', value: String(config.validation.cacheTtlValid), description: '有效链接缓存时间(s)' },
|
||||
{ key: 'validation_cache_ttl_invalid', value: String(config.validation.cacheTtlInvalid), description: '无效链接缓存时间(s)' },
|
||||
{ key: 'search_proxy_enabled', value: 'false', description: '搜索代理开关(true/false)' },
|
||||
{ key: 'search_proxy_url', value: '', description: '搜索代理地址 (如 http://127.0.0.1:7890)' },
|
||||
{ key: 'search_strategy', value: 'wait_all', description: '搜索结果展示方式: wait_all=等待全部后展示, stream_channel=频道逐步展示' },
|
||||
{ key: 'link_validation_enabled', value: 'true', description: '资源链接有效性检测开关(true/false)' },
|
||||
{ key: 'cloud_enabled_quark', value: 'true', description: '夸克网盘' },
|
||||
{ key: 'cloud_enabled_baidu', value: 'true', description: '百度网盘' },
|
||||
{ key: 'cloud_enabled_aliyun', value: 'true', description: '阿里云盘' },
|
||||
{ key: 'cloud_enabled_115', value: 'true', description: '115 网盘' },
|
||||
{ key: 'cloud_enabled_tianyi', value: 'true', description: '天翼云盘' },
|
||||
{ key: 'cloud_enabled_123pan', value: 'true', description: '123 云盘' },
|
||||
{ key: 'cloud_enabled_uc', value: 'true', description: 'UC 网盘' },
|
||||
{ key: 'cloud_enabled_xunlei', value: 'true', description: '迅雷网盘' },
|
||||
{ key: 'cloud_enabled_pikpak', value: 'true', description: 'PikPak 网盘' },
|
||||
{ key: 'cloud_enabled_magnet', value: 'true', description: '磁力链接' },
|
||||
{ key: 'cloud_enabled_ed2k', value: 'true', description: '电驴链接' },
|
||||
{ key: 'cloud_enabled_others', value: 'false', description: '其他类型(默认关闭)' },
|
||||
{ key: 'search_result_limit', value: '10', description: '每类网盘最多展示的有效结果数' },
|
||||
{ key: 'search_fallback_image', value: '', description: '无图资源的兜底封面图 URL(留空使用渐变色)' },
|
||||
{ key: 'site_logo', value: '', description: '网站 LOGO 图片 URL(留空使用默认图标/文字)' },
|
||||
{ key: 'site_name', value: 'CloudSearch', description: '网站名称(显示在首页标题/页脚)' },
|
||||
{ key: 'site_disclaimer', value: '本站为非盈利性个人站点,所有资源仅供学习、研究使用,版权归原作者所有。请于下载后24小时内删除,切勿用于商业或非法用途。若侵犯了您的权益,请联系我们(邮箱:3337598077@qq.com),我们将及时处理。', description: '网站底部免责声明' },
|
||||
{ key: 'site_marquee', value: '📢 欢迎使用CloudSearch,所有资源仅供学习交流,请于下载后24小时内删除', description: '搜索栏下方滚动通知文字(从右往左滚动显示)' },
|
||||
{ key: 'tmdb_api_token', value: '', description: 'TMDB API 读取令牌(用于增强豆瓣内容信息)' },
|
||||
{ key: 'ip_geo_api_url', value: 'https://cn.apihz.cn/api/ip/chaapi.php?id=10014356&key=ca7ccb3b9ca044dd993c8604bc9afd93&ip={ip}&td=0', description: 'IP 归属地查询接口({ip} 会被替换为实际IP)' },
|
||||
{ key: 'ip_geo_api_key', value: '', description: 'IP 归属地备用 API Key(留空使用默认)' },
|
||||
{ key: 'title_filter_rules', value: '', description: '搜索结果标题过滤规则(一行一条:纯文本直接移除 / 正则用/包围/)' },
|
||||
{ key: 'timezone', value: 'Asia/Shanghai', description: '系统时区(如 Asia/Shanghai、America/New_York、UTC)' },
|
||||
{ key: 'redis_url', value: 'redis://redis:6379', description: 'Redis 连接地址(用于缓存优化)' },
|
||||
{ key: 'pansou_auth_token', value: '', description: 'PanSou API 认证令牌(用于私有搜索服务)' },
|
||||
{ key: 'pansou_web_enabled', value: 'false', description: '启用 PanSou Web 端访问(在 /pansou 路径提供 PanSou 搜索引擎管理界面)' },
|
||||
{ key: 'cleanup_enabled', value: 'true', description: '启用自动清理(每天检查一次,移入回收站+清空日志+清空回收站)' },
|
||||
{ key: 'cleanup_file_retention_days', value: '7', description: '云盘文件保留天数(超过此天数的日期文件夹将被移入回收站)' },
|
||||
{ key: 'cleanup_log_retention_days', value: '30', description: '转存日志保留天数' },
|
||||
{ key: 'cleanup_empty_trash', value: 'true', description: '清理时是否清空回收站(永久删除释放空间)' },
|
||||
{ key: 'cleanup_space_threshold_enabled', value: 'false', description: '启用空间阈值自动清理(已用空间超过XX%时按比例删除最旧的转存文件)' },
|
||||
{ key: 'cleanup_space_threshold_percent', value: '90', description: '空间使用阈值百分比(超过此值时触发强制清理)' },
|
||||
{ key: 'cleanup_space_threshold_delete_percent', value: '10', description: '触发阈值清理时释放总空间的百分比(如 10 表示累计删除最旧文件直到达到总空间的 10%,6TB 总空间 → 释放 ~600GB)' },
|
||||
{ key: 'save_reuse_enabled', value: 'true', description: '启用分享链接复用(相同原始链接不再重复转存,直接复用之前的分享链接)' },
|
||||
{ key: 'cleanup_last_run', value: '', description: '上次自动清理时间' },
|
||||
{ key: 'cleanup_last_stats', value: '', description: '上次清理结果统计(JSON)' },
|
||||
];
|
||||
const insert = db.prepare(
|
||||
'INSERT OR IGNORE INTO system_configs (key, value, description) VALUES (?, ?, ?)'
|
||||
);
|
||||
for (const entry of defaults) {
|
||||
insert.run(entry.key, entry.value, entry.description);
|
||||
}
|
||||
}
|
||||
|
||||
/** 清理 60 天前的转存记录 */
|
||||
function cleanupOldSaveRecords(db: Database.Database): void {
|
||||
const cutoff = formatLocalDateTime(new Date(Date.now() - 60 * 24 * 60 * 60 * 1000));
|
||||
const deleted = db.prepare('DELETE FROM save_records WHERE created_at < ?').run(cutoff);
|
||||
console.log(`[DB] Cleaned up ${deleted.changes} save records older than 60 days (before ${cutoff})`);
|
||||
}
|
||||
|
||||
export default getDb;
|
||||
306
source_clean/src/database/database.ts.bak_idx
Executable file
306
source_clean/src/database/database.ts.bak_idx
Executable file
@@ -0,0 +1,306 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import path from 'path';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import config from '../config';
|
||||
import { formatLocalDateTime } from '../utils/time';
|
||||
|
||||
let db: Database.Database | null = null;
|
||||
|
||||
export function getDb(): Database.Database {
|
||||
if (db) return db;
|
||||
|
||||
const dbDir = path.dirname(config.dbPath);
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync(dbDir)) {
|
||||
fs.mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
db = new Database(config.dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
runMigrations(db);
|
||||
seedAdmin(db);
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
function runMigrations(db: Database.Database): void {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS admins (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
|
||||
last_login TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cloud_configs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cloud_type TEXT NOT NULL,
|
||||
cookie TEXT,
|
||||
nickname TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
storage_used TEXT,
|
||||
storage_total TEXT,
|
||||
checkin_status TEXT NOT NULL DEFAULT 'none',
|
||||
last_checkin_at TEXT,
|
||||
checkin_message TEXT,
|
||||
consecutive_failures INTEGER DEFAULT 0,
|
||||
last_used_at TEXT,
|
||||
total_saves INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS promotions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
image_url TEXT,
|
||||
link_url TEXT,
|
||||
position TEXT,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
click_count INTEGER NOT NULL DEFAULT 0,
|
||||
start_time TEXT,
|
||||
end_time TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS save_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source_type TEXT,
|
||||
source_title TEXT,
|
||||
source_url TEXT,
|
||||
target_cloud TEXT,
|
||||
share_url TEXT,
|
||||
share_pwd TEXT,
|
||||
file_size TEXT,
|
||||
file_count INTEGER DEFAULT 0,
|
||||
duration_ms INTEGER DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT '',
|
||||
error_message TEXT,
|
||||
ip_address TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS search_stats (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
keyword TEXT,
|
||||
intent TEXT,
|
||||
result_count INTEGER DEFAULT 0,
|
||||
ip_address TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hot_keywords (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
keyword TEXT UNIQUE NOT NULL,
|
||||
search_count INTEGER NOT NULL DEFAULT 1,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS system_configs (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL DEFAULT '',
|
||||
description TEXT,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS content_cache (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
keyword TEXT UNIQUE NOT NULL,
|
||||
title TEXT,
|
||||
description TEXT,
|
||||
tags TEXT,
|
||||
cover TEXT,
|
||||
source TEXT,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
`);
|
||||
seedSystemConfigs(db);
|
||||
migrateSaveRecords(db);
|
||||
migrateContentCache(db);
|
||||
migrateCloudConfigs(db);
|
||||
cleanupOldSaveRecords(db);
|
||||
}
|
||||
|
||||
/** 迁移: 给已有 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 {
|
||||
const columns: { col: string; def: string }[] = [
|
||||
{ col: 'douban_url', def: 'TEXT' },
|
||||
{ col: 'rating', def: 'TEXT' },
|
||||
{ col: 'rating_count', def: 'TEXT' },
|
||||
{ col: 'year', def: 'TEXT' },
|
||||
{ col: 'genres', def: 'TEXT' },
|
||||
{ col: 'directors', def: 'TEXT' },
|
||||
{ col: 'actors', def: 'TEXT' },
|
||||
{ col: 'region', def: 'TEXT' },
|
||||
{ col: 'duration', def: 'TEXT' },
|
||||
];
|
||||
for (const { col, def } of columns) {
|
||||
try {
|
||||
db.exec(`ALTER TABLE content_cache ADD COLUMN ${col} ${def}`);
|
||||
} catch {
|
||||
// Column already exists — ignore
|
||||
}
|
||||
}
|
||||
// 修复旧记录:source 为 NULL 但实际有 TMDB 数据的,标记为 tmdb
|
||||
db.exec(`UPDATE content_cache SET source = 'tmdb' WHERE source IS NULL AND title IS NOT NULL AND title != ''`);
|
||||
}
|
||||
|
||||
/** 迁移: 给 cloud_configs 表去UNIQUE约束 + 加签到/轮训字段 */
|
||||
function migrateCloudConfigs(db: Database.Database): void {
|
||||
// 加新列
|
||||
const newCols: { col: string; def: string }[] = [
|
||||
{ col: 'checkin_status', def: "TEXT NOT NULL DEFAULT 'none'" },
|
||||
{ col: 'last_checkin_at', def: 'TEXT' },
|
||||
{ col: 'checkin_message', def: 'TEXT' },
|
||||
{ col: 'consecutive_failures', def: 'INTEGER DEFAULT 0' },
|
||||
{ col: 'last_used_at', def: 'TEXT' },
|
||||
{ col: 'total_saves', def: 'INTEGER DEFAULT 0' },
|
||||
];
|
||||
for (const { col, def } of newCols) {
|
||||
try { db.exec(`ALTER TABLE cloud_configs ADD COLUMN ${col} ${def}`); } catch {}
|
||||
}
|
||||
// 检查旧表是否有 UNIQUE 约束,有则重建表
|
||||
const row = db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='cloud_configs'`).get() as any;
|
||||
if (row && row.sql && row.sql.includes('cloud_type TEXT UNIQUE')) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS cloud_configs_v2 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cloud_type TEXT NOT NULL,
|
||||
cookie TEXT,
|
||||
nickname TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
storage_used TEXT,
|
||||
storage_total TEXT,
|
||||
checkin_status TEXT NOT NULL DEFAULT 'none',
|
||||
last_checkin_at TEXT,
|
||||
checkin_message TEXT,
|
||||
consecutive_failures INTEGER DEFAULT 0,
|
||||
last_used_at TEXT,
|
||||
total_saves INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
INSERT INTO cloud_configs_v2 (id, cloud_type, cookie, nickname, is_active, storage_used, storage_total, checkin_status, last_checkin_at, checkin_message, consecutive_failures, last_used_at, total_saves, created_at, updated_at)
|
||||
SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total, COALESCE(checkin_status,'none'), last_checkin_at, checkin_message, COALESCE(consecutive_failures,0), last_used_at, COALESCE(total_saves,0), created_at, updated_at FROM cloud_configs;
|
||||
DROP TABLE cloud_configs;
|
||||
ALTER TABLE cloud_configs_v2 RENAME TO cloud_configs;
|
||||
`);
|
||||
console.log('[DB] cloud_configs migration: UNIQUE constraint removed, new fields added');
|
||||
}
|
||||
|
||||
// Migration 2: Add verification_status column
|
||||
const row2 = db.prepare("SELECT sql FROM sqlite_master WHERE name='cloud_configs' AND sql LIKE '%verification_status%'").get();
|
||||
if (!row2) {
|
||||
db.exec("ALTER TABLE cloud_configs ADD COLUMN verification_status TEXT DEFAULT NULL");
|
||||
console.log('[DB] cloud_configs migration: verification_status column added');
|
||||
}
|
||||
}
|
||||
|
||||
function seedAdmin(db: Database.Database): void {
|
||||
const existing = db.prepare('SELECT id FROM admins WHERE username = ?').get(config.adminUsername);
|
||||
if (existing) return;
|
||||
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
const hash = bcrypt.hashSync(config.adminPassword, salt);
|
||||
|
||||
db.prepare(
|
||||
'INSERT INTO admins (username, password_hash) VALUES (?, ?)'
|
||||
).run(config.adminUsername, hash);
|
||||
|
||||
console.log(`[DB] Admin user "${config.adminUsername}" created`);
|
||||
}
|
||||
|
||||
function seedSystemConfigs(db: Database.Database): void {
|
||||
const defaults: { key: string; value: string; description: string }[] = [
|
||||
{ key: 'pansou_url', value: config.pansouUrl, description: 'PanSou 搜索引擎服务地址' },
|
||||
{ key: 'video_parser_url', value: config.videoParserUrl, description: '视频解析服务地址' },
|
||||
{ key: 'validation_concurrency', value: String(config.validation.concurrency), description: '链接验证并发数' },
|
||||
{ key: 'validation_timeout', value: String(config.validation.timeout), description: '链接验证超时(ms)' },
|
||||
{ key: 'validation_cache_ttl_valid', value: String(config.validation.cacheTtlValid), description: '有效链接缓存时间(s)' },
|
||||
{ key: 'validation_cache_ttl_invalid', value: String(config.validation.cacheTtlInvalid), description: '无效链接缓存时间(s)' },
|
||||
{ key: 'search_proxy_enabled', value: 'false', description: '搜索代理开关(true/false)' },
|
||||
{ key: 'search_proxy_url', value: '', description: '搜索代理地址 (如 http://127.0.0.1:7890)' },
|
||||
{ key: 'search_strategy', value: 'wait_all', description: '搜索结果展示方式: wait_all=等待全部后展示, stream_channel=频道逐步展示' },
|
||||
{ key: 'link_validation_enabled', value: 'true', description: '资源链接有效性检测开关(true/false)' },
|
||||
{ key: 'cloud_enabled_quark', value: 'true', description: '夸克网盘' },
|
||||
{ key: 'cloud_enabled_baidu', value: 'true', description: '百度网盘' },
|
||||
{ key: 'cloud_enabled_aliyun', value: 'true', description: '阿里云盘' },
|
||||
{ key: 'cloud_enabled_115', value: 'true', description: '115 网盘' },
|
||||
{ key: 'cloud_enabled_tianyi', value: 'true', description: '天翼云盘' },
|
||||
{ key: 'cloud_enabled_123pan', value: 'true', description: '123 云盘' },
|
||||
{ key: 'cloud_enabled_uc', value: 'true', description: 'UC 网盘' },
|
||||
{ key: 'cloud_enabled_xunlei', value: 'true', description: '迅雷网盘' },
|
||||
{ key: 'cloud_enabled_pikpak', value: 'true', description: 'PikPak 网盘' },
|
||||
{ key: 'cloud_enabled_magnet', value: 'true', description: '磁力链接' },
|
||||
{ key: 'cloud_enabled_ed2k', value: 'true', description: '电驴链接' },
|
||||
{ key: 'cloud_enabled_others', value: 'false', description: '其他类型(默认关闭)' },
|
||||
{ key: 'search_result_limit', value: '10', description: '每类网盘最多展示的有效结果数' },
|
||||
{ key: 'search_fallback_image', value: '', description: '无图资源的兜底封面图 URL(留空使用渐变色)' },
|
||||
{ key: 'site_logo', value: '', description: '网站 LOGO 图片 URL(留空使用默认图标/文字)' },
|
||||
{ key: 'site_name', value: 'CloudSearch', description: '网站名称(显示在首页标题/页脚)' },
|
||||
{ key: 'site_disclaimer', value: '本站为非盈利性个人站点,所有资源仅供学习、研究使用,版权归原作者所有。请于下载后24小时内删除,切勿用于商业或非法用途。若侵犯了您的权益,请联系我们(邮箱:3337598077@qq.com),我们将及时处理。', description: '网站底部免责声明' },
|
||||
{ key: 'site_marquee', value: '📢 欢迎使用CloudSearch,所有资源仅供学习交流,请于下载后24小时内删除', description: '搜索栏下方滚动通知文字(从右往左滚动显示)' },
|
||||
{ key: 'tmdb_api_token', value: '', description: 'TMDB API 读取令牌(用于增强豆瓣内容信息)' },
|
||||
{ key: 'ip_geo_api_url', value: 'https://cn.apihz.cn/api/ip/chaapi.php?id=10014356&key=ca7ccb3b9ca044dd993c8604bc9afd93&ip={ip}&td=0', description: 'IP 归属地查询接口({ip} 会被替换为实际IP)' },
|
||||
{ key: 'ip_geo_api_key', value: '', description: 'IP 归属地备用 API Key(留空使用默认)' },
|
||||
{ key: 'title_filter_rules', value: '', description: '搜索结果标题过滤规则(一行一条:纯文本直接移除 / 正则用/包围/)' },
|
||||
{ key: 'timezone', value: 'Asia/Shanghai', description: '系统时区(如 Asia/Shanghai、America/New_York、UTC)' },
|
||||
{ key: 'redis_url', value: 'redis://redis:6379', description: 'Redis 连接地址(用于缓存优化)' },
|
||||
{ key: 'pansou_auth_token', value: '', description: 'PanSou API 认证令牌(用于私有搜索服务)' },
|
||||
{ key: 'pansou_web_enabled', value: 'false', description: '启用 PanSou Web 端访问(在 /pansou 路径提供 PanSou 搜索引擎管理界面)' },
|
||||
{ key: 'cleanup_enabled', value: 'true', description: '启用自动清理(每天检查一次,移入回收站+清空日志+清空回收站)' },
|
||||
{ key: 'cleanup_file_retention_days', value: '7', description: '云盘文件保留天数(超过此天数的日期文件夹将被移入回收站)' },
|
||||
{ key: 'cleanup_log_retention_days', value: '30', description: '转存日志保留天数' },
|
||||
{ key: 'cleanup_empty_trash', value: 'true', description: '清理时是否清空回收站(永久删除释放空间)' },
|
||||
{ key: 'cleanup_space_threshold_enabled', value: 'false', description: '启用空间阈值自动清理(已用空间超过XX%时按比例删除最旧的转存文件)' },
|
||||
{ key: 'cleanup_space_threshold_percent', value: '90', description: '空间使用阈值百分比(超过此值时触发强制清理)' },
|
||||
{ key: 'cleanup_space_threshold_delete_percent', value: '10', description: '触发阈值清理时释放总空间的百分比(如 10 表示累计删除最旧文件直到达到总空间的 10%,6TB 总空间 → 释放 ~600GB)' },
|
||||
{ key: 'save_reuse_enabled', value: 'true', description: '启用分享链接复用(相同原始链接不再重复转存,直接复用之前的分享链接)' },
|
||||
{ key: 'cleanup_last_run', value: '', description: '上次自动清理时间' },
|
||||
{ key: 'cleanup_last_stats', value: '', description: '上次清理结果统计(JSON)' },
|
||||
];
|
||||
const insert = db.prepare(
|
||||
'INSERT OR IGNORE INTO system_configs (key, value, description) VALUES (?, ?, ?)'
|
||||
);
|
||||
for (const entry of defaults) {
|
||||
insert.run(entry.key, entry.value, entry.description);
|
||||
}
|
||||
}
|
||||
|
||||
/** 清理 60 天前的转存记录 */
|
||||
function cleanupOldSaveRecords(db: Database.Database): void {
|
||||
const cutoff = formatLocalDateTime(new Date(Date.now() - 60 * 24 * 60 * 60 * 1000));
|
||||
const deleted = db.prepare('DELETE FROM save_records WHERE created_at < ?').run(cutoff);
|
||||
console.log(`[DB] Cleaned up ${deleted.changes} save records older than 60 days (before ${cutoff})`);
|
||||
}
|
||||
|
||||
export default getDb;
|
||||
Reference in New Issue
Block a user