477 lines
12 KiB
TypeScript
477 lines
12 KiB
TypeScript
/* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
|
|
|
import { AdminConfig } from './admin.types';
|
|
import { Favorite, IStorage, PlayRecord } from './types';
|
|
|
|
// 搜索历史最大条数
|
|
const SEARCH_HISTORY_LIMIT = 20;
|
|
|
|
// D1 数据库接口
|
|
interface D1Database {
|
|
prepare(sql: string): D1PreparedStatement;
|
|
exec(sql: string): Promise<D1ExecResult>;
|
|
batch(statements: D1PreparedStatement[]): Promise<D1Result[]>;
|
|
}
|
|
|
|
interface D1PreparedStatement {
|
|
bind(...values: any[]): D1PreparedStatement;
|
|
first<T = any>(colName?: string): Promise<T | null>;
|
|
run(): Promise<D1Result>;
|
|
all<T = any>(): Promise<D1Result<T>>;
|
|
}
|
|
|
|
interface D1Result<T = any> {
|
|
results: T[];
|
|
success: boolean;
|
|
error?: string;
|
|
meta: {
|
|
changed_db: boolean;
|
|
changes: number;
|
|
last_row_id: number;
|
|
duration: number;
|
|
};
|
|
}
|
|
|
|
interface D1ExecResult {
|
|
count: number;
|
|
duration: number;
|
|
}
|
|
|
|
// 获取全局D1数据库实例
|
|
function getD1Database(): D1Database {
|
|
return (process.env as any).DB as D1Database;
|
|
}
|
|
|
|
export class D1Storage implements IStorage {
|
|
private db: D1Database | null = null;
|
|
|
|
private async getDatabase(): Promise<D1Database> {
|
|
if (!this.db) {
|
|
this.db = getD1Database();
|
|
}
|
|
return this.db;
|
|
}
|
|
|
|
// 播放记录相关
|
|
async getPlayRecord(
|
|
userName: string,
|
|
key: string
|
|
): Promise<PlayRecord | null> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
const result = await db
|
|
.prepare('SELECT * FROM play_records WHERE username = ? AND key = ?')
|
|
.bind(userName, key)
|
|
.first<any>();
|
|
|
|
if (!result) return null;
|
|
|
|
return {
|
|
title: result.title,
|
|
source_name: result.source_name,
|
|
cover: result.cover,
|
|
year: result.year,
|
|
index: result.index_episode,
|
|
total_episodes: result.total_episodes,
|
|
play_time: result.play_time,
|
|
total_time: result.total_time,
|
|
save_time: result.save_time,
|
|
search_title: result.search_title || undefined,
|
|
};
|
|
} catch (err) {
|
|
console.error('Failed to get play record:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async setPlayRecord(
|
|
userName: string,
|
|
key: string,
|
|
record: PlayRecord
|
|
): Promise<void> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
await db
|
|
.prepare(
|
|
`
|
|
INSERT OR REPLACE INTO play_records
|
|
(username, key, title, source_name, cover, year, index_episode, total_episodes, play_time, total_time, save_time, search_title)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`
|
|
)
|
|
.bind(
|
|
userName,
|
|
key,
|
|
record.title,
|
|
record.source_name,
|
|
record.cover,
|
|
record.year,
|
|
record.index,
|
|
record.total_episodes,
|
|
record.play_time,
|
|
record.total_time,
|
|
record.save_time,
|
|
record.search_title || null
|
|
)
|
|
.run();
|
|
} catch (err) {
|
|
console.error('Failed to set play record:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async getAllPlayRecords(
|
|
userName: string
|
|
): Promise<Record<string, PlayRecord>> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
const result = await db
|
|
.prepare(
|
|
'SELECT * FROM play_records WHERE username = ? ORDER BY save_time DESC'
|
|
)
|
|
.bind(userName)
|
|
.all<any>();
|
|
|
|
const records: Record<string, PlayRecord> = {};
|
|
|
|
result.results.forEach((row: any) => {
|
|
records[row.key] = {
|
|
title: row.title,
|
|
source_name: row.source_name,
|
|
cover: row.cover,
|
|
year: row.year,
|
|
index: row.index_episode,
|
|
total_episodes: row.total_episodes,
|
|
play_time: row.play_time,
|
|
total_time: row.total_time,
|
|
save_time: row.save_time,
|
|
search_title: row.search_title || undefined,
|
|
};
|
|
});
|
|
|
|
return records;
|
|
} catch (err) {
|
|
console.error('Failed to get all play records:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async deletePlayRecord(userName: string, key: string): Promise<void> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
await db
|
|
.prepare('DELETE FROM play_records WHERE username = ? AND key = ?')
|
|
.bind(userName, key)
|
|
.run();
|
|
} catch (err) {
|
|
console.error('Failed to delete play record:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// 收藏相关
|
|
async getFavorite(userName: string, key: string): Promise<Favorite | null> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
const result = await db
|
|
.prepare('SELECT * FROM favorites WHERE username = ? AND key = ?')
|
|
.bind(userName, key)
|
|
.first<any>();
|
|
|
|
if (!result) return null;
|
|
|
|
return {
|
|
title: result.title,
|
|
source_name: result.source_name,
|
|
cover: result.cover,
|
|
year: result.year,
|
|
total_episodes: result.total_episodes,
|
|
save_time: result.save_time,
|
|
search_title: result.search_title,
|
|
};
|
|
} catch (err) {
|
|
console.error('Failed to get favorite:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async setFavorite(
|
|
userName: string,
|
|
key: string,
|
|
favorite: Favorite
|
|
): Promise<void> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
await db
|
|
.prepare(
|
|
`
|
|
INSERT OR REPLACE INTO favorites
|
|
(username, key, title, source_name, cover, year, total_episodes, save_time)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`
|
|
)
|
|
.bind(
|
|
userName,
|
|
key,
|
|
favorite.title,
|
|
favorite.source_name,
|
|
favorite.cover,
|
|
favorite.year,
|
|
favorite.total_episodes,
|
|
favorite.save_time
|
|
)
|
|
.run();
|
|
} catch (err) {
|
|
console.error('Failed to set favorite:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async getAllFavorites(userName: string): Promise<Record<string, Favorite>> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
const result = await db
|
|
.prepare(
|
|
'SELECT * FROM favorites WHERE username = ? ORDER BY save_time DESC'
|
|
)
|
|
.bind(userName)
|
|
.all<any>();
|
|
|
|
const favorites: Record<string, Favorite> = {};
|
|
|
|
result.results.forEach((row: any) => {
|
|
favorites[row.key] = {
|
|
title: row.title,
|
|
source_name: row.source_name,
|
|
cover: row.cover,
|
|
year: row.year,
|
|
total_episodes: row.total_episodes,
|
|
save_time: row.save_time,
|
|
search_title: row.search_title,
|
|
};
|
|
});
|
|
|
|
return favorites;
|
|
} catch (err) {
|
|
console.error('Failed to get all favorites:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async deleteFavorite(userName: string, key: string): Promise<void> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
await db
|
|
.prepare('DELETE FROM favorites WHERE username = ? AND key = ?')
|
|
.bind(userName, key)
|
|
.run();
|
|
} catch (err) {
|
|
console.error('Failed to delete favorite:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// 用户相关
|
|
async registerUser(userName: string, password: string): Promise<void> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
await db
|
|
.prepare('INSERT INTO users (username, password) VALUES (?, ?)')
|
|
.bind(userName, password)
|
|
.run();
|
|
} catch (err) {
|
|
console.error('Failed to register user:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async verifyUser(userName: string, password: string): Promise<boolean> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
const result = await db
|
|
.prepare('SELECT password FROM users WHERE username = ?')
|
|
.bind(userName)
|
|
.first<{ password: string }>();
|
|
|
|
return result?.password === password;
|
|
} catch (err) {
|
|
console.error('Failed to verify user:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async checkUserExist(userName: string): Promise<boolean> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
const result = await db
|
|
.prepare('SELECT 1 FROM users WHERE username = ?')
|
|
.bind(userName)
|
|
.first();
|
|
|
|
return result !== null;
|
|
} catch (err) {
|
|
console.error('Failed to check user existence:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async changePassword(userName: string, newPassword: string): Promise<void> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
await db
|
|
.prepare('UPDATE users SET password = ? WHERE username = ?')
|
|
.bind(newPassword, userName)
|
|
.run();
|
|
} catch (err) {
|
|
console.error('Failed to change password:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async deleteUser(userName: string): Promise<void> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
const statements = [
|
|
db.prepare('DELETE FROM users WHERE username = ?').bind(userName),
|
|
db
|
|
.prepare('DELETE FROM play_records WHERE username = ?')
|
|
.bind(userName),
|
|
db.prepare('DELETE FROM favorites WHERE username = ?').bind(userName),
|
|
db
|
|
.prepare('DELETE FROM search_history WHERE username = ?')
|
|
.bind(userName),
|
|
];
|
|
|
|
await db.batch(statements);
|
|
} catch (err) {
|
|
console.error('Failed to delete user:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// 搜索历史相关
|
|
async getSearchHistory(userName: string): Promise<string[]> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
const result = await db
|
|
.prepare(
|
|
'SELECT keyword FROM search_history WHERE username = ? ORDER BY created_at DESC LIMIT ?'
|
|
)
|
|
.bind(userName, SEARCH_HISTORY_LIMIT)
|
|
.all<{ keyword: string }>();
|
|
|
|
return result.results.map((row) => row.keyword);
|
|
} catch (err) {
|
|
console.error('Failed to get search history:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async addSearchHistory(userName: string, keyword: string): Promise<void> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
// 先删除可能存在的重复记录
|
|
await db
|
|
.prepare(
|
|
'DELETE FROM search_history WHERE username = ? AND keyword = ?'
|
|
)
|
|
.bind(userName, keyword)
|
|
.run();
|
|
|
|
// 添加新记录
|
|
await db
|
|
.prepare('INSERT INTO search_history (username, keyword) VALUES (?, ?)')
|
|
.bind(userName, keyword)
|
|
.run();
|
|
|
|
// 保持历史记录条数限制
|
|
await db
|
|
.prepare(
|
|
`
|
|
DELETE FROM search_history
|
|
WHERE username = ? AND id NOT IN (
|
|
SELECT id FROM search_history
|
|
WHERE username = ?
|
|
ORDER BY created_at DESC
|
|
LIMIT ?
|
|
)
|
|
`
|
|
)
|
|
.bind(userName, userName, SEARCH_HISTORY_LIMIT)
|
|
.run();
|
|
} catch (err) {
|
|
console.error('Failed to add search history:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async deleteSearchHistory(userName: string, keyword?: string): Promise<void> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
if (keyword) {
|
|
await db
|
|
.prepare(
|
|
'DELETE FROM search_history WHERE username = ? AND keyword = ?'
|
|
)
|
|
.bind(userName, keyword)
|
|
.run();
|
|
} else {
|
|
await db
|
|
.prepare('DELETE FROM search_history WHERE username = ?')
|
|
.bind(userName)
|
|
.run();
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to delete search history:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// 用户列表
|
|
async getAllUsers(): Promise<string[]> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
const result = await db
|
|
.prepare('SELECT username FROM users ORDER BY created_at ASC')
|
|
.all<{ username: string }>();
|
|
|
|
return result.results.map((row) => row.username);
|
|
} catch (err) {
|
|
console.error('Failed to get all users:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// 管理员配置相关
|
|
async getAdminConfig(): Promise<AdminConfig | null> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
const result = await db
|
|
.prepare('SELECT config FROM admin_config WHERE id = 1')
|
|
.first<{ config: string }>();
|
|
|
|
if (!result) return null;
|
|
|
|
return JSON.parse(result.config) as AdminConfig;
|
|
} catch (err) {
|
|
console.error('Failed to get admin config:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async setAdminConfig(config: AdminConfig): Promise<void> {
|
|
try {
|
|
const db = await this.getDatabase();
|
|
await db
|
|
.prepare(
|
|
'INSERT OR REPLACE INTO admin_config (id, config) VALUES (1, ?)'
|
|
)
|
|
.bind(JSON.stringify(config))
|
|
.run();
|
|
} catch (err) {
|
|
console.error('Failed to set admin config:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
}
|