Merge pull request #147 from senshinya/d1

support D1 storage
This commit is contained in:
senshinya
2025-07-14 13:15:39 +08:00
committed by GitHub
6 changed files with 302 additions and 222 deletions

75
D1初始化.md Normal file
View File

@@ -0,0 +1,75 @@
```sql
CREATE TABLE IF NOT EXISTS users (
username TEXT PRIMARY KEY,
password TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
CREATE TABLE IF NOT EXISTS play_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
key TEXT NOT NULL,
title TEXT NOT NULL,
source_name TEXT NOT NULL,
cover TEXT NOT NULL,
year TEXT NOT NULL,
index_episode INTEGER NOT NULL,
total_episodes INTEGER NOT NULL,
play_time INTEGER NOT NULL,
total_time INTEGER NOT NULL,
save_time INTEGER NOT NULL,
search_title TEXT,
UNIQUE(username, key)
);
CREATE TABLE IF NOT EXISTS favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
key TEXT NOT NULL,
title TEXT NOT NULL,
source_name TEXT NOT NULL,
cover TEXT NOT NULL,
year TEXT NOT NULL,
total_episodes INTEGER NOT NULL,
save_time INTEGER NOT NULL,
UNIQUE(username, key)
);
CREATE TABLE IF NOT EXISTS search_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
keyword TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(username, keyword)
);
CREATE TABLE IF NOT EXISTS admin_config (
id INTEGER PRIMARY KEY DEFAULT 1,
config TEXT NOT NULL,
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
-- 基本索引
CREATE INDEX IF NOT EXISTS idx_play_records_username ON play_records(username);
CREATE INDEX IF NOT EXISTS idx_favorites_username ON favorites(username);
CREATE INDEX IF NOT EXISTS idx_search_history_username ON search_history(username);
-- 复合索引优化查询性能
-- 播放记录:用户名+键值的复合索引,用于快速查找特定记录
CREATE INDEX IF NOT EXISTS idx_play_records_username_key ON play_records(username, key);
-- 播放记录:用户名+保存时间的复合索引,用于按时间排序的查询
CREATE INDEX IF NOT EXISTS idx_play_records_username_save_time ON play_records(username, save_time DESC);
-- 收藏:用户名+键值的复合索引,用于快速查找特定收藏
CREATE INDEX IF NOT EXISTS idx_favorites_username_key ON favorites(username, key);
-- 收藏:用户名+保存时间的复合索引,用于按时间排序的查询
CREATE INDEX IF NOT EXISTS idx_favorites_username_save_time ON favorites(username, save_time DESC);
-- 搜索历史:用户名+关键词的复合索引,用于快速查找/删除特定搜索记录
CREATE INDEX IF NOT EXISTS idx_search_history_username_keyword ON search_history(username, keyword);
-- 搜索历史:用户名+创建时间的复合索引,用于按时间排序的查询
CREATE INDEX IF NOT EXISTS idx_search_history_username_created_at ON search_history(username, created_at DESC);
-- 搜索历史清理查询的优化索引
CREATE INDEX IF NOT EXISTS idx_search_history_username_id_created_at ON search_history(username, id, created_at DESC);
```

View File

@@ -126,6 +126,11 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
// 当前登录用户名
const currentUsername = getAuthInfoFromBrowserCookie()?.username || null;
// 检测存储类型是否为 d1
const isD1Storage =
typeof window !== 'undefined' &&
(window as any).RUNTIME_CONFIG?.STORAGE_TYPE === 'd1';
useEffect(() => {
if (config?.UserConfig) {
setUserSettings({
@@ -285,18 +290,29 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
</h4>
<div className='flex items-center justify-between'>
<label className='text-gray-700 dark:text-gray-300'>
<label
className={`text-gray-700 dark:text-gray-300 ${
isD1Storage ? 'opacity-50' : ''
}`}
>
{isD1Storage && (
<span className='ml-2 text-xs text-gray-500 dark:text-gray-400'>
(D1 )
</span>
)}
</label>
<button
onClick={() =>
!isD1Storage &&
toggleAllowRegister(!userSettings.enableRegistration)
}
disabled={isD1Storage}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
userSettings.enableRegistration
? 'bg-green-600'
: 'bg-gray-200 dark:bg-gray-700'
}`}
} ${isD1Storage ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
@@ -937,6 +953,11 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
// 保存状态
const [saving, setSaving] = useState(false);
// 检测存储类型是否为 d1
const isD1Storage =
typeof window !== 'undefined' &&
(window as any).RUNTIME_CONFIG?.STORAGE_TYPE === 'd1';
useEffect(() => {
if (config?.SiteConfig) {
setSiteSettings(config.SiteConfig);
@@ -978,34 +999,60 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
<div className='space-y-6'>
{/* 站点名称 */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
isD1Storage ? 'opacity-50' : ''
}`}
>
{isD1Storage && (
<span className='ml-2 text-xs text-gray-500 dark:text-gray-400'>
(D1 )
</span>
)}
</label>
<input
type='text'
value={siteSettings.SiteName}
onChange={(e) =>
!isD1Storage &&
setSiteSettings((prev) => ({ ...prev, SiteName: e.target.value }))
}
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
disabled={isD1Storage}
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent ${
isD1Storage ? 'opacity-50 cursor-not-allowed' : ''
}`}
/>
</div>
{/* 站点公告 */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
isD1Storage ? 'opacity-50' : ''
}`}
>
{isD1Storage && (
<span className='ml-2 text-xs text-gray-500 dark:text-gray-400'>
(D1 )
</span>
)}
</label>
<textarea
value={siteSettings.Announcement}
onChange={(e) =>
!isD1Storage &&
setSiteSettings((prev) => ({
...prev,
Announcement: e.target.value,
}))
}
disabled={isD1Storage}
rows={3}
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent ${
isD1Storage ? 'opacity-50 cursor-not-allowed' : ''
}`}
/>
</div>
@@ -1049,21 +1096,32 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
{/* 默认按标题和年份聚合 */}
<div className='flex items-center justify-between'>
<label className='text-gray-700 dark:text-gray-300'>
<label
className={`text-gray-700 dark:text-gray-300 ${
isD1Storage ? 'opacity-50' : ''
}`}
>
{isD1Storage && (
<span className='ml-2 text-xs text-gray-500 dark:text-gray-400'>
(D1 )
</span>
)}
</label>
<button
onClick={() =>
!isD1Storage &&
setSiteSettings((prev) => ({
...prev,
SearchResultDefaultAggregate: !prev.SearchResultDefaultAggregate,
}))
}
disabled={isD1Storage}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
siteSettings.SearchResultDefaultAggregate
? 'bg-green-600'
: 'bg-gray-200 dark:bg-gray-700'
}`}
} ${isD1Storage ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
@@ -1079,9 +1137,11 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
<div className='flex justify-end'>
<button
onClick={handleSave}
disabled={saving}
disabled={saving || isD1Storage}
className={`px-4 py-2 ${
saving ? 'bg-gray-400' : 'bg-green-600 hover:bg-green-700'
saving || isD1Storage
? 'bg-gray-400 cursor-not-allowed'
: 'bg-green-600 hover:bg-green-700'
} text-white rounded-lg transition-colors`}
>
{saving ? '保存中…' : '保存'}

View File

@@ -13,10 +13,14 @@ const inter = Inter({ subsets: ['latin'] });
// 动态生成 metadata支持配置更新后的标题变化
export async function generateMetadata(): Promise<Metadata> {
const config = await getConfig();
let siteName = process.env.NEXT_PUBLIC_SITE_NAME;
if (process.env.NEXT_PUBLIC_STORAGE_TYPE !== 'd1') {
const config = await getConfig();
siteName = config.SiteConfig.SiteName;
}
return {
title: config.SiteConfig.SiteName,
title: siteName,
description: '影视聚合',
manifest: '/manifest.json',
};
@@ -31,15 +35,26 @@ export default async function RootLayout({
}: {
children: React.ReactNode;
}) {
const config = await getConfig();
const siteName = config.SiteConfig.SiteName;
const announcement = config.SiteConfig.Announcement;
let siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTV';
let announcement =
process.env.ANNOUNCEMENT ||
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。';
let enableRegister = process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true';
let aggregateSearchResult =
process.env.NEXT_PUBLIC_AGGREGATE_SEARCH_RESULT !== 'false';
if (process.env.NEXT_PUBLIC_STORAGE_TYPE !== 'd1') {
const config = await getConfig();
siteName = config.SiteConfig.SiteName;
announcement = config.SiteConfig.Announcement;
enableRegister = config.UserConfig.AllowRegister;
aggregateSearchResult = config.SiteConfig.SearchResultDefaultAggregate;
}
// 将运行时配置注入到全局 window 对象,供客户端在运行时读取
const runtimeConfig = {
STORAGE_TYPE: process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage',
ENABLE_REGISTER: config.UserConfig.AllowRegister,
AGGREGATE_SEARCH_RESULT: config.SiteConfig.SearchResultDefaultAggregate,
ENABLE_REGISTER: enableRegister,
AGGREGATE_SEARCH_RESULT: aggregateSearchResult,
};
return (

View File

@@ -68,128 +68,126 @@ async function initConfig() {
if (storageType !== 'localstorage') {
// 数据库存储,读取并补全管理员配置
const storage = getStorage();
(async () => {
try {
// 尝试从数据库获取管理员配置
let adminConfig: AdminConfig | null = null;
if (storage && typeof (storage as any).getAdminConfig === 'function') {
adminConfig = await (storage as any).getAdminConfig();
try {
// 尝试从数据库获取管理员配置
let adminConfig: AdminConfig | null = null;
if (storage && typeof (storage as any).getAdminConfig === 'function') {
adminConfig = await (storage as any).getAdminConfig();
}
// 获取所有用户名,用于补全 Users
let userNames: string[] = [];
if (storage && typeof (storage as any).getAllUsers === 'function') {
try {
userNames = await (storage as any).getAllUsers();
} catch (e) {
console.error('获取用户列表失败:', e);
}
}
// 获取所有用户名,用于补全 Users
let userNames: string[] = [];
if (storage && typeof (storage as any).getAllUsers === 'function') {
try {
userNames = await (storage as any).getAllUsers();
} catch (e) {
console.error('获取用户列表失败:', e);
}
}
// 从文件中获取源信息,用于补全源
const apiSiteEntries = Object.entries(fileConfig.api_site);
// 从文件中获取源信息,用于补全源
const apiSiteEntries = Object.entries(fileConfig.api_site);
if (adminConfig) {
// 补全 SourceConfig
const existed = new Set(
(adminConfig.SourceConfig || []).map((s) => s.key)
);
apiSiteEntries.forEach(([key, site]) => {
if (!existed.has(key)) {
adminConfig!.SourceConfig.push({
key,
name: site.name,
api: site.api,
detail: site.detail,
from: 'config',
disabled: false,
});
}
});
// 检查现有源是否在 fileConfig.api_site 中,如果不在则标记为 custom
const apiSiteKeys = new Set(apiSiteEntries.map(([key]) => key));
adminConfig.SourceConfig.forEach((source) => {
if (!apiSiteKeys.has(source.key)) {
source.from = 'custom';
}
});
const existedUsers = new Set(
(adminConfig.UserConfig.Users || []).map((u) => u.username)
);
userNames.forEach((uname) => {
if (!existedUsers.has(uname)) {
adminConfig!.UserConfig.Users.push({
username: uname,
role: 'user',
});
}
});
// 站长
const ownerUser = process.env.USERNAME;
if (ownerUser) {
adminConfig!.UserConfig.Users =
adminConfig!.UserConfig.Users.filter(
(u) => u.username !== ownerUser
);
adminConfig!.UserConfig.Users.unshift({
username: ownerUser,
role: 'owner',
});
}
} else {
// 数据库中没有配置,创建新的管理员配置
let allUsers = userNames.map((uname) => ({
username: uname,
role: 'user',
}));
const ownerUser = process.env.USERNAME;
if (ownerUser) {
allUsers = allUsers.filter((u) => u.username !== ownerUser);
allUsers.unshift({
username: ownerUser,
role: 'owner',
});
}
adminConfig = {
SiteConfig: {
SiteName: process.env.SITE_NAME || 'MoonTV',
Announcement:
process.env.ANNOUNCEMENT ||
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。',
SearchDownstreamMaxPage:
Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5,
SiteInterfaceCacheTime: fileConfig.cache_time || 7200,
SearchResultDefaultAggregate:
process.env.NEXT_PUBLIC_AGGREGATE_SEARCH_RESULT !== 'false',
},
UserConfig: {
AllowRegister: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true',
Users: allUsers as any,
},
SourceConfig: apiSiteEntries.map(([key, site]) => ({
if (adminConfig) {
// 补全 SourceConfig
const existed = new Set(
(adminConfig.SourceConfig || []).map((s) => s.key)
);
apiSiteEntries.forEach(([key, site]) => {
if (!existed.has(key)) {
adminConfig!.SourceConfig.push({
key,
name: site.name,
api: site.api,
detail: site.detail,
from: 'config',
disabled: false,
})),
};
}
});
}
});
// 写回数据库(更新/创建)
if (storage && typeof (storage as any).setAdminConfig === 'function') {
await (storage as any).setAdminConfig(adminConfig);
}
// 检查现有源是否在 fileConfig.api_site 中,如果不在则标记为 custom
const apiSiteKeys = new Set(apiSiteEntries.map(([key]) => key));
adminConfig.SourceConfig.forEach((source) => {
if (!apiSiteKeys.has(source.key)) {
source.from = 'custom';
}
});
// 更新缓存
cachedConfig = adminConfig;
} catch (err) {
console.error('加载管理员配置失败:', err);
const existedUsers = new Set(
(adminConfig.UserConfig.Users || []).map((u) => u.username)
);
userNames.forEach((uname) => {
if (!existedUsers.has(uname)) {
adminConfig!.UserConfig.Users.push({
username: uname,
role: 'user',
});
}
});
// 站长
const ownerUser = process.env.USERNAME;
if (ownerUser) {
adminConfig!.UserConfig.Users = adminConfig!.UserConfig.Users.filter(
(u) => u.username !== ownerUser
);
adminConfig!.UserConfig.Users.unshift({
username: ownerUser,
role: 'owner',
});
}
} else {
// 数据库中没有配置,创建新的管理员配置
let allUsers = userNames.map((uname) => ({
username: uname,
role: 'user',
}));
const ownerUser = process.env.USERNAME;
if (ownerUser) {
allUsers = allUsers.filter((u) => u.username !== ownerUser);
allUsers.unshift({
username: ownerUser,
role: 'owner',
});
}
adminConfig = {
SiteConfig: {
SiteName: process.env.SITE_NAME || 'MoonTV',
Announcement:
process.env.ANNOUNCEMENT ||
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。',
SearchDownstreamMaxPage:
Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5,
SiteInterfaceCacheTime: fileConfig.cache_time || 7200,
SearchResultDefaultAggregate:
process.env.NEXT_PUBLIC_AGGREGATE_SEARCH_RESULT !== 'false',
},
UserConfig: {
AllowRegister: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true',
Users: allUsers as any,
},
SourceConfig: apiSiteEntries.map(([key, site]) => ({
key,
name: site.name,
api: site.api,
detail: site.detail,
from: 'config',
disabled: false,
})),
};
}
})();
// 写回数据库(更新/创建)
if (storage && typeof (storage as any).setAdminConfig === 'function') {
await (storage as any).setAdminConfig(adminConfig);
}
// 更新缓存
cachedConfig = adminConfig;
} catch (err) {
console.error('加载管理员配置失败:', err);
}
} else {
// 本地存储直接使用文件配置
cachedConfig = {
@@ -221,7 +219,32 @@ async function initConfig() {
}
export async function getConfig(): Promise<AdminConfig> {
await initConfig();
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
if (process.env.DOCKER_ENV === 'true' || storageType === 'localstorage') {
await initConfig();
return cachedConfig;
}
// 非 docker 环境且 DB 存储,直接读 db 配置
const storage = getStorage();
let adminConfig: AdminConfig | null = null;
if (storage && typeof (storage as any).getAdminConfig === 'function') {
adminConfig = await (storage as any).getAdminConfig();
}
if (adminConfig) {
// 合并一些环境变量配置
adminConfig.SiteConfig.SiteName = process.env.SITE_NAME || 'MoonTV';
adminConfig.SiteConfig.Announcement =
process.env.ANNOUNCEMENT ||
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。';
adminConfig.UserConfig.AllowRegister =
process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true';
adminConfig.SiteConfig.SearchResultDefaultAggregate =
process.env.NEXT_PUBLIC_AGGREGATE_SEARCH_RESULT !== 'false';
cachedConfig = adminConfig;
} else {
// DB 无配置,执行一次初始化
await initConfig();
}
return cachedConfig;
}

View File

@@ -37,83 +37,6 @@ interface D1ExecResult {
duration: number;
}
// 数据库初始化 SQL
const INIT_SQL = `
CREATE TABLE IF NOT EXISTS users (
username TEXT PRIMARY KEY,
password TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
CREATE TABLE IF NOT EXISTS play_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
key TEXT NOT NULL,
title TEXT NOT NULL,
source_name TEXT NOT NULL,
cover TEXT NOT NULL,
year TEXT NOT NULL,
index_episode INTEGER NOT NULL,
total_episodes INTEGER NOT NULL,
play_time INTEGER NOT NULL,
total_time INTEGER NOT NULL,
save_time INTEGER NOT NULL,
search_title TEXT,
UNIQUE(username, key)
);
CREATE TABLE IF NOT EXISTS favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
key TEXT NOT NULL,
title TEXT NOT NULL,
source_name TEXT NOT NULL,
cover TEXT NOT NULL,
year TEXT NOT NULL,
total_episodes INTEGER NOT NULL,
save_time INTEGER NOT NULL,
UNIQUE(username, key)
);
CREATE TABLE IF NOT EXISTS search_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
keyword TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(username, keyword)
);
CREATE TABLE IF NOT EXISTS admin_config (
id INTEGER PRIMARY KEY DEFAULT 1,
config TEXT NOT NULL,
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
-- 基本索引
CREATE INDEX IF NOT EXISTS idx_play_records_username ON play_records(username);
CREATE INDEX IF NOT EXISTS idx_favorites_username ON favorites(username);
CREATE INDEX IF NOT EXISTS idx_search_history_username ON search_history(username);
-- 复合索引优化查询性能
-- 播放记录:用户名+键值的复合索引,用于快速查找特定记录
CREATE INDEX IF NOT EXISTS idx_play_records_username_key ON play_records(username, key);
-- 播放记录:用户名+保存时间的复合索引,用于按时间排序的查询
CREATE INDEX IF NOT EXISTS idx_play_records_username_save_time ON play_records(username, save_time DESC);
-- 收藏:用户名+键值的复合索引,用于快速查找特定收藏
CREATE INDEX IF NOT EXISTS idx_favorites_username_key ON favorites(username, key);
-- 收藏:用户名+保存时间的复合索引,用于按时间排序的查询
CREATE INDEX IF NOT EXISTS idx_favorites_username_save_time ON favorites(username, save_time DESC);
-- 搜索历史:用户名+关键词的复合索引,用于快速查找/删除特定搜索记录
CREATE INDEX IF NOT EXISTS idx_search_history_username_keyword ON search_history(username, keyword);
-- 搜索历史:用户名+创建时间的复合索引,用于按时间排序的查询
CREATE INDEX IF NOT EXISTS idx_search_history_username_created_at ON search_history(username, created_at DESC);
-- 搜索历史清理查询的优化索引
CREATE INDEX IF NOT EXISTS idx_search_history_username_id_created_at ON search_history(username, id, created_at DESC);
`;
// 获取全局D1数据库实例
function getD1Database(): D1Database {
return (process.env as any).DB as D1Database;
@@ -121,30 +44,14 @@ function getD1Database(): D1Database {
export class D1Storage implements IStorage {
private db: D1Database | null = null;
private initialized = false;
private async getDatabase(): Promise<D1Database> {
if (!this.db) {
this.db = getD1Database();
if (!this.initialized) {
await this.initDatabase();
this.initialized = true;
}
}
return this.db;
}
private async initDatabase() {
try {
if (this.db) {
await this.db.exec(INIT_SQL);
}
} catch (err) {
console.error('Failed to initialize D1 database:', err);
throw err;
}
}
// 播放记录相关
async getPlayRecord(
userName: string,

View File

@@ -128,6 +128,6 @@ function shouldSkipAuth(pathname: string): boolean {
// 配置middleware匹配规则
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|api/detail|api/search|api/image-proxy|api/douban).*)',
'/((?!_next/static|_next/image|favicon.ico|api/detail|api/search|api/image-proxy|api/douban|api/cron|api/server-config).*)',
],
};