Files
CloudSearch/source_clean/src/database/database.ts.bak_idx
admin 83cbfaf03f v0.2.7: 修复Redis连接 + 启动管理后台
- 修复Redis认证 (配置密码)
- 启动Python管理后台 (端口9531, 15个功能开关)
- 统一版本号 0.2.7
- 更新docker-compose.yml (镜像版本/Redis URL/Admin服务)
2026-05-17 02:22:18 +08:00

307 lines
15 KiB
Plaintext
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;