d1/sqlite支持
This commit is contained in:
170
src/lib/d1-adapter.ts
Normal file
170
src/lib/d1-adapter.ts
Normal 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
1414
src/lib/d1.db.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
|
||||
@@ -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
8
src/lib/token-config.ts
Normal 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 分钟时自动续期
|
||||
};
|
||||
Reference in New Issue
Block a user