待看名单支持数据库同步
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
const { defaultLogger } = require('../utils/logger');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const logger = defaultLogger.child('WatchlistDatabase');
|
||||
|
||||
/**
|
||||
* 数据库版本的待看名单
|
||||
* 提供与JSON版本相同的接口,但使用MySQL数据库存储
|
||||
*/
|
||||
class WatchlistDatabase {
|
||||
constructor(databaseManager) {
|
||||
this.db = databaseManager;
|
||||
this.loaded = false;
|
||||
this.tableName = 'watchlist_items';
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据库表
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
// 如果表不存在则创建
|
||||
if (!(await this.db.tableExists(this.tableName))) {
|
||||
const createSQL = `
|
||||
CREATE TABLE ${this.tableName} (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
url_hash CHAR(64) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uniq_url_hash (url_hash)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='待看名单表';
|
||||
`;
|
||||
await this.db.createTable(this.tableName, createSQL);
|
||||
}
|
||||
this.loaded = true;
|
||||
logger.info('待看名单数据库表初始化完成');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
logger.error('初始化待看名单数据库失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有待看项目
|
||||
*/
|
||||
async getAllItems() {
|
||||
try {
|
||||
const result = await this.db.query(`
|
||||
SELECT id, title, url, created_at, updated_at
|
||||
FROM ${this.tableName}
|
||||
ORDER BY created_at ASC
|
||||
`);
|
||||
return result.data.map(row => ({
|
||||
id: row.id.toString(),
|
||||
title: row.title,
|
||||
url: row.url,
|
||||
createdAt: (row.created_at instanceof Date) ? row.created_at.toISOString() : row.created_at,
|
||||
updatedAt: (row.updated_at instanceof Date) ? row.updated_at.toISOString() : row.updated_at
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('获取待看项目失败(数据库):', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加或更新待看项目(按URL唯一)
|
||||
*/
|
||||
async addItem(item) {
|
||||
try {
|
||||
const { url, title } = item;
|
||||
if (!url) throw new Error('URL是必填项');
|
||||
const urlHash = crypto.createHash('sha256').update(url).digest('hex');
|
||||
|
||||
// 先查是否存在
|
||||
const exists = await this.db.select(this.tableName, { url_hash: urlHash });
|
||||
if (exists.data && exists.data.length > 0) {
|
||||
// 更新标题
|
||||
await this.db.update(this.tableName, { title }, { url_hash: urlHash });
|
||||
} else {
|
||||
const id = (item.id?.toString()) || Date.now().toString();
|
||||
await this.db.insert(this.tableName, {
|
||||
id,
|
||||
title: title || '待看页面',
|
||||
url,
|
||||
url_hash: urlHash
|
||||
});
|
||||
}
|
||||
return await this.getAllItems();
|
||||
} catch (error) {
|
||||
logger.error('添加待看项目失败(数据库):', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除待看项目
|
||||
*/
|
||||
async removeItem(id) {
|
||||
try {
|
||||
const result = await this.db.delete(this.tableName, { id });
|
||||
if (result.affectedRows === 0) {
|
||||
throw new Error('待看项目不存在');
|
||||
}
|
||||
return await this.getAllItems();
|
||||
} catch (error) {
|
||||
logger.error('删除待看项目失败(数据库):', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新待看项目
|
||||
*/
|
||||
async updateItem(id, updates) {
|
||||
try {
|
||||
const allowed = {};
|
||||
if (typeof updates.title === 'string') allowed.title = updates.title;
|
||||
if (typeof updates.url === 'string') {
|
||||
allowed.url = updates.url;
|
||||
allowed.url_hash = crypto.createHash('sha256').update(updates.url).digest('hex');
|
||||
}
|
||||
if (Object.keys(allowed).length === 0) {
|
||||
// 没有可更新字段则返回原项
|
||||
const rows = await this.db.select(this.tableName, { id });
|
||||
if (!rows.data || rows.data.length === 0) throw new Error('待看项目不存在');
|
||||
const row = rows.data[0];
|
||||
return {
|
||||
id: row.id.toString(),
|
||||
title: row.title,
|
||||
url: row.url,
|
||||
createdAt: (row.created_at instanceof Date) ? row.created_at.toISOString() : row.created_at,
|
||||
updatedAt: (row.updated_at instanceof Date) ? row.updated_at.toISOString() : row.updated_at
|
||||
};
|
||||
}
|
||||
await this.db.update(this.tableName, allowed, { id });
|
||||
const rows = await this.db.select(this.tableName, { id });
|
||||
const row = rows.data[0];
|
||||
return {
|
||||
id: row.id.toString(),
|
||||
title: row.title,
|
||||
url: row.url,
|
||||
createdAt: (row.created_at instanceof Date) ? row.created_at.toISOString() : row.created_at,
|
||||
updatedAt: (row.updated_at instanceof Date) ? row.updated_at.toISOString() : row.updated_at
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('更新待看项目失败(数据库):', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空表
|
||||
*/
|
||||
async clearAll() {
|
||||
await this.db.query(`DELETE FROM ${this.tableName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入
|
||||
*/
|
||||
async batchInsert(items) {
|
||||
const rows = items.map(i => {
|
||||
const url = i.url;
|
||||
const url_hash = url ? crypto.createHash('sha256').update(url).digest('hex') : null;
|
||||
return {
|
||||
id: i.id?.toString() || Date.now().toString(),
|
||||
title: i.title || '待看页面',
|
||||
url,
|
||||
url_hash
|
||||
};
|
||||
});
|
||||
return await this.db.batchInsert(this.tableName, rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已有URL集合
|
||||
*/
|
||||
async getExistingUrlSet() {
|
||||
const result = await this.db.query(`SELECT url FROM ${this.tableName}`);
|
||||
return new Set(result.data.map(r => r.url));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WatchlistDatabase;
|
||||
Reference in New Issue
Block a user