d1/sqlite支持

This commit is contained in:
mtvpls
2026-01-30 14:27:43 +08:00
parent c4198e0954
commit c86f9cdca7
11 changed files with 2110 additions and 11 deletions

170
src/lib/d1-adapter.ts Normal file
View File

@@ -0,0 +1,170 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* 统一的数据库适配器接口
* 兼容 Cloudflare D1 和 better-sqlite3
*
* 注意:此模块仅在服务端使用,通过 webpack 配置排除客户端打包
*/
// D1 PreparedStatement 接口
export interface D1PreparedStatement {
bind(...values: any[]): D1PreparedStatement;
first<T = any>(colName?: string): Promise<T | null>;
run<T = any>(): Promise<D1Result<T>>;
all<T = any>(): Promise<D1Result<T>>;
}
export interface D1Result<T = any> {
results?: T[];
success: boolean;
meta?: any;
error?: string;
}
// 统一的数据库接口
export interface DatabaseAdapter {
prepare(query: string): D1PreparedStatement;
batch?(statements: D1PreparedStatement[]): Promise<D1Result[]>;
exec?(query: string): void;
}
/**
* Cloudflare D1 适配器(生产环境)
*/
export class CloudflareD1Adapter implements DatabaseAdapter {
constructor(private db: D1Database) {}
prepare(query: string): D1PreparedStatement {
return this.db.prepare(query);
}
async batch(statements: D1PreparedStatement[]): Promise<D1Result[]> {
return this.db.batch(statements as any);
}
}
/**
* SQLite 适配器(开发环境)
* 包装 better-sqlite3 以兼容 D1 API
*/
export class SQLiteAdapter implements DatabaseAdapter {
private db: any; // better-sqlite3 Database
constructor(db: any) {
this.db = db;
}
prepare(query: string): D1PreparedStatement {
const stmt = this.db.prepare(query);
return new SQLitePreparedStatement(stmt);
}
batch(statements: D1PreparedStatement[]): Promise<D1Result[]> {
// SQLite 使用事务模拟 batch
return new Promise((resolve, reject) => {
try {
const results: D1Result[] = [];
const transaction = this.db.transaction(() => {
for (const stmt of statements) {
const result = (stmt as any).runSync();
results.push(result);
}
});
transaction();
resolve(results);
} catch (err) {
reject(err);
}
});
}
exec(query: string): void {
this.db.exec(query);
}
}
/**
* SQLite PreparedStatement 包装器
* 将 better-sqlite3 API 转换为 D1 兼容 API
*/
class SQLitePreparedStatement implements D1PreparedStatement {
private stmt: any;
private params: any[] = [];
constructor(stmt: any) {
this.stmt = stmt;
}
bind(...values: any[]): D1PreparedStatement {
this.params = values;
return this;
}
async first<T = any>(colName?: string): Promise<T | null> {
try {
const result = this.stmt.get(...this.params);
if (!result) return null;
if (colName) return result[colName] ?? null;
return result;
} catch (err) {
console.error('SQLite first() error:', err);
return null;
}
}
async run<T = any>(): Promise<D1Result<T>> {
try {
const info = this.stmt.run(...this.params);
return {
success: true,
meta: {
changes: info.changes,
last_row_id: info.lastInsertRowid,
},
};
} catch (err: any) {
console.error('SQLite run() error:', err);
return {
success: false,
error: err.message,
};
}
}
async all<T = any>(): Promise<D1Result<T>> {
try {
const results = this.stmt.all(...this.params);
return {
success: true,
results: results || [],
};
} catch (err: any) {
console.error('SQLite all() error:', err);
return {
success: false,
error: err.message,
results: [],
};
}
}
// 同步版本(用于 batch
runSync(): D1Result {
try {
const info = this.stmt.run(...this.params);
return {
success: true,
meta: {
changes: info.changes,
last_row_id: info.lastInsertRowid,
},
};
} catch (err: any) {
return {
success: false,
error: err.message,
};
}
}
}

1414
src/lib/d1.db.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -6,13 +6,14 @@ import { RedisStorage } from './redis.db';
import { DanmakuFilterConfig,Favorite, IStorage, PlayRecord, SkipConfig } from './types';
import { UpstashRedisStorage } from './upstash.db';
// storage type 常量: 'localstorage' | 'redis' | 'upstash',默认 'localstorage'
// storage type 常量: 'localstorage' | 'redis' | 'upstash' | 'kvrocks' | 'd1',默认 'localstorage'
const STORAGE_TYPE =
(process.env.NEXT_PUBLIC_STORAGE_TYPE as
| 'localstorage'
| 'redis'
| 'upstash'
| 'kvrocks'
| 'd1'
| undefined) || 'localstorage';
// 创建存储实例
@@ -24,12 +25,67 @@ function createStorage(): IStorage {
return new UpstashRedisStorage();
case 'kvrocks':
return new KvrocksStorage();
case 'd1':
// D1Storage 只能在服务端使用,客户端会报错
if (typeof window !== 'undefined') {
throw new Error('D1Storage can only be used on the server side');
}
const adapter = getD1Adapter();
// 动态导入 D1Storage 以避免客户端打包
const { D1Storage } = require('./d1.db');
return new D1Storage(adapter);
case 'localstorage':
default:
return null as unknown as IStorage;
}
}
/**
* 获取 D1 适配器
* 开发环境:使用 better-sqlite3
* 生产环境:使用 Cloudflare D1
*/
function getD1Adapter(): any {
// 动态导入适配器以避免客户端打包
const { CloudflareD1Adapter, SQLiteAdapter } = require('./d1-adapter');
// 生产环境Cloudflare Workers/Pages
if (typeof process !== 'undefined' && (process as any).env?.DB) {
console.log('Using Cloudflare D1 database');
return new CloudflareD1Adapter((process as any).env.DB);
}
// 开发环境better-sqlite3
if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
try {
const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');
const dbPath = path.join(process.cwd(), '.data', 'moontv.db');
// 检查数据库文件是否存在
if (!fs.existsSync(dbPath)) {
console.error('❌ SQLite database not found. Please run: npm run init:sqlite');
throw new Error('SQLite database not initialized');
}
const db = new Database(dbPath);
db.pragma('journal_mode = WAL'); // 启用 WAL 模式提升性能
console.log('Using SQLite database (development mode)');
console.log('Database location:', dbPath);
return new SQLiteAdapter(db);
} catch (err) {
console.error('Failed to initialize SQLite:', err);
throw err;
}
}
throw new Error('D1 database not available. Set NEXT_PUBLIC_STORAGE_TYPE to another option or configure D1.');
}
// 单例存储实例
let storageInstance: IStorage | null = null;

View File

@@ -1,5 +1,10 @@
/* eslint-disable no-console */
import { TOKEN_CONFIG } from './token-config';
// Re-export TOKEN_CONFIG for backward compatibility
export { TOKEN_CONFIG };
// Lazy import to avoid Edge Runtime issues in middleware
let getStorage: (() => any) | null = null;
@@ -11,13 +16,6 @@ async function loadStorage() {
return getStorage();
}
// Token 配置
export const TOKEN_CONFIG = {
ACCESS_TOKEN_AGE: 4 * 60 * 60 * 1000, // 4 小时
REFRESH_TOKEN_AGE: 60 * 24 * 60 * 60 * 1000, // 60 天
RENEWAL_THRESHOLD: 10 * 60 * 1000, // 剩余 10 分钟时自动续期
};
interface TokenData {
token: string;
deviceInfo: string;

8
src/lib/token-config.ts Normal file
View File

@@ -0,0 +1,8 @@
// Token 配置常量
// 这个文件不依赖任何服务端模块,可以在客户端安全使用
export const TOKEN_CONFIG = {
ACCESS_TOKEN_AGE: 4 * 60 * 60 * 1000, // 4 小时
REFRESH_TOKEN_AGE: 60 * 24 * 60 * 60 * 1000, // 60 天
RENEWAL_THRESHOLD: 10 * 60 * 1000, // 剩余 10 分钟时自动续期
};