From 2c805e25226037dbee4e77f62a6daedd9588fb3a Mon Sep 17 00:00:00 2001 From: mtvpls Date: Mon, 12 Jan 2026 21:06:45 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B1=82=E7=89=87=E5=A2=9E=E5=8A=A0=E5=BC=80?= =?UTF-8?q?=E5=85=B3=E5=92=8C=E5=86=B7=E5=8D=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/admin/page.tsx | 103 +++++++++++++++++++++++++++- src/app/api/movie-requests/route.ts | 26 +++++-- src/app/layout.tsx | 5 +- src/app/movie-request/page.tsx | 68 +++++++++++------- src/app/private-library/page.tsx | 16 +++-- src/lib/admin.types.ts | 3 + src/lib/redis-base.db.ts | 2 + src/lib/upstash.db.ts | 13 +++- 8 files changed, 194 insertions(+), 42 deletions(-) diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 1fee446..5caab46 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -8726,7 +8726,7 @@ const XiaoyaConfigComponent = ({ }; // 求片列表组件 -const MovieRequestsComponent = ({ config, refreshConfig }: { config: AdminConfig | null; refreshConfig: () => void }) => { +const MovieRequestsComponent = ({ config, refreshConfig }: { config: AdminConfig | null; refreshConfig: () => Promise }) => { const { alertModal, showAlert, hideAlert } = useAlertModal(); const { isLoading, withLoading } = useLoadingState(); const [requests, setRequests] = useState([]); @@ -8735,6 +8735,11 @@ const MovieRequestsComponent = ({ config, refreshConfig }: { config: AdminConfig const [fulfilledCount, setFulfilledCount] = useState(0); const [loading, setLoading] = useState(true); + // 求片功能设置 + const [enableMovieRequest, setEnableMovieRequest] = useState(config?.SiteConfig?.EnableMovieRequest ?? true); + const [movieRequestCooldown, setMovieRequestCooldown] = useState(config?.SiteConfig?.MovieRequestCooldown ?? 3600); + const [savingSettings, setSavingSettings] = useState(false); + useEffect(() => { loadRequests(); loadCounts(); @@ -8795,9 +8800,100 @@ const MovieRequestsComponent = ({ config, refreshConfig }: { config: AdminConfig }); }; + const handleSaveSettings = async () => { + setSavingSettings(true); + try { + if (!config) throw new Error('配置未加载'); + + const updatedConfig = { + ...config, + SiteConfig: { + ...config.SiteConfig, + EnableMovieRequest: enableMovieRequest, + MovieRequestCooldown: movieRequestCooldown, + }, + }; + + const response = await fetch('/api/admin/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updatedConfig), + }); + + if (!response.ok) throw new Error('保存失败'); + + showSuccess('求片设置已保存', showAlert); + await refreshConfig(); + } catch (err) { + showError(err instanceof Error ? err.message : '保存失败', showAlert); + } finally { + setSavingSettings(false); + } + }; + return (
-
+ {/* 求片功能设置 */} +
+

求片功能设置

+
+
+
+ +

+ 关闭后用户将无法访问求片页面 +

+
+ +
+ +
+ +

+ 用户两次求片之间的最小间隔时间,默认3600秒(1小时) +

+ setMovieRequestCooldown(parseInt(e.target.value) || 0)} + className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100' + /> +

+ {movieRequestCooldown >= 3600 + ? `约 ${Math.floor(movieRequestCooldown / 3600)} 小时 ${Math.floor((movieRequestCooldown % 3600) / 60)} 分钟` + : movieRequestCooldown >= 60 + ? `约 ${Math.floor(movieRequestCooldown / 60)} 分钟` + : `${movieRequestCooldown} 秒`} +

+
+ + +
+
+ + {/* 求片列表 */} +
+

求片列表

+
)} +
toggleTab('movieRequests')} > - +
diff --git a/src/app/api/movie-requests/route.ts b/src/app/api/movie-requests/route.ts index 4ea8879..aaad560 100644 --- a/src/app/api/movie-requests/route.ts +++ b/src/app/api/movie-requests/route.ts @@ -71,6 +71,14 @@ export async function POST(request: NextRequest) { } try { + // 检查求片功能是否启用并获取冷却时间 + const { getConfig } = await import('@/lib/config'); + const config = await getConfig(); + + if (config.SiteConfig.EnableMovieRequest === false) { + return NextResponse.json({ error: '求片功能已关闭' }, { status: 403 }); + } + const body = await request.json(); const { tmdbId, title, year, mediaType, season, poster, overview } = body; @@ -80,10 +88,11 @@ export async function POST(request: NextRequest) { const storage = getStorage(); - // 检查频率限制 - const userInfo = await storage.getUserInfoV2(authInfo.username); - const rateLimit = parseInt(process.env.MOVIE_REQUEST_RATE_LIMIT || '3600') * 1000; + // 检查频率限制 - 使用配置中的冷却时间 + const cooldownSeconds = config.SiteConfig.MovieRequestCooldown ?? 3600; + const rateLimit = cooldownSeconds * 1000; + const userInfo = await storage.getUserInfoV2(authInfo.username); if (userInfo?.last_movie_request_time) { const elapsed = Date.now() - userInfo.last_movie_request_time; if (elapsed < rateLimit) { @@ -163,12 +172,17 @@ export async function POST(request: NextRequest) { await storage.createMovieRequest(newRequest); await storage.addUserMovieRequest(authInfo.username, newRequest.id); - // 更新频率限制 - await storage.setGlobalValue( - `user:${authInfo.username}:info:last_movie_request_time`, + // 更新频率限制 - 保存到用户信息的 hash 中 + await storage.client.hSet( + `user:${authInfo.username}:info`, + 'last_movie_request_time', Date.now().toString() ); + // 清除用户信息缓存,确保下次读取到最新数据 + const { userInfoCache } = await import('@/lib/user-cache'); + userInfoCache?.delete(authInfo.username); + // 给站长发送通知 const ownerUsername = process.env.USERNAME; if (ownerUsername) { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index bb6c48c..62a91b1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -82,6 +82,7 @@ export default async function RootLayout({ let aiEnablePlayPageEntry = false; let aiDefaultMessageNoVideo = ''; let aiDefaultMessageWithVideo = ''; + let enableMovieRequest = true; let customCategories = [] as { name: string; type: 'movie' | 'tv'; @@ -124,6 +125,8 @@ export default async function RootLayout({ aiEnablePlayPageEntry = config.AIConfig?.EnablePlayPageEntry || false; aiDefaultMessageNoVideo = config.AIConfig?.DefaultMessageNoVideo || ''; aiDefaultMessageWithVideo = config.AIConfig?.DefaultMessageWithVideo || ''; + // 求片功能配置 + enableMovieRequest = config.SiteConfig.EnableMovieRequest ?? true; // 检查是否启用了 OpenList 功能 openListEnabled = !!( config.OpenListConfig?.Enabled && @@ -178,7 +181,7 @@ export default async function RootLayout({ AI_ENABLE_PLAYPAGE_ENTRY: aiEnablePlayPageEntry, AI_DEFAULT_MESSAGE_NO_VIDEO: aiDefaultMessageNoVideo, AI_DEFAULT_MESSAGE_WITH_VIDEO: aiDefaultMessageWithVideo, - ENABLE_SOURCE_SEARCH: process.env.NEXT_PUBLIC_ENABLE_SOURCE_SEARCH !== 'false', + ENABLE_MOVIE_REQUEST: enableMovieRequest, }; return ( diff --git a/src/app/movie-request/page.tsx b/src/app/movie-request/page.tsx index f52ab9d..a213ea2 100644 --- a/src/app/movie-request/page.tsx +++ b/src/app/movie-request/page.tsx @@ -49,6 +49,15 @@ export default function MovieRequestPage() { const [myRequests, setMyRequests] = useState([]); const [loadingMyRequests, setLoadingMyRequests] = useState(true); const [submitting, setSubmitting] = useState(false); + const [isFeatureEnabled, setIsFeatureEnabled] = useState(true); + + // 检查求片功能是否启用 + useEffect(() => { + const runtimeConfig = (window as any).RUNTIME_CONFIG; + if (runtimeConfig && runtimeConfig.ENABLE_MOVIE_REQUEST === false) { + setIsFeatureEnabled(false); + } + }, []); // TMDB搜索 const handleSearch = async () => { @@ -199,32 +208,43 @@ export default function MovieRequestPage() { 求片

- 搜索并提交您想看的影片 + {isFeatureEnabled ? '搜索并提交您想看的影片' : '求片功能已关闭,仅可查看已求片列表'}

- {/* 搜索框 */} -
-
- setSearchKeyword(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') handleSearch(); - }} - className='flex-1 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500' - /> - + {/* 功能关闭提示 */} + {!isFeatureEnabled && ( +
+

+ 求片功能已被管理员关闭,您可以查看已提交的求片记录 +

-
+ )} + + {/* 搜索框 - 仅在功能启用时显示 */} + {isFeatureEnabled && ( +
+
+ setSearchKeyword(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSearch(); + }} + className='flex-1 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500' + /> + +
+
+ )} {/* 我的求片列表 */} {searchResults.length === 0 && ( @@ -308,10 +328,10 @@ export default function MovieRequestPage() {

diff --git a/src/app/private-library/page.tsx b/src/app/private-library/page.tsx index 9c25414..2f36979 100644 --- a/src/app/private-library/page.tsx +++ b/src/app/private-library/page.tsx @@ -457,13 +457,15 @@ export default function PrivateLibraryPage() { 观看自我收藏的高清视频吧

- + {mounted && ( + + )} {/* 第一级:源类型选择(OpenList / Emby / 小雅) */} diff --git a/src/lib/admin.types.ts b/src/lib/admin.types.ts index c36a6ca..18a04cd 100644 --- a/src/lib/admin.types.ts +++ b/src/lib/admin.types.ts @@ -40,6 +40,9 @@ export interface AdminConfig { TurnstileSiteKey?: string; // Cloudflare Turnstile Site Key TurnstileSecretKey?: string; // Cloudflare Turnstile Secret Key DefaultUserTags?: string[]; // 新注册用户的默认用户组 + // 求片功能配置 + EnableMovieRequest?: boolean; // 启用求片功能 + MovieRequestCooldown?: number; // 求片冷却时间(秒),默认3600 // OIDC配置 EnableOIDCLogin?: boolean; // 启用OIDC登录 EnableOIDCRegistration?: boolean; // 启用OIDC注册 diff --git a/src/lib/redis-base.db.ts b/src/lib/redis-base.db.ts index 1a43eb6..ec1ecbb 100644 --- a/src/lib/redis-base.db.ts +++ b/src/lib/redis-base.db.ts @@ -659,6 +659,7 @@ export abstract class BaseRedisStorage implements IStorage { playrecord_migrated?: boolean; favorite_migrated?: boolean; skip_migrated?: boolean; + last_movie_request_time?: number; } | null> { const userInfo = await this.withRetry(() => this.client.hGetAll(this.userInfoKey(userName)) @@ -678,6 +679,7 @@ export abstract class BaseRedisStorage implements IStorage { playrecord_migrated: userInfo.playrecord_migrated === 'true', favorite_migrated: userInfo.favorite_migrated === 'true', skip_migrated: userInfo.skip_migrated === 'true', + last_movie_request_time: userInfo.last_movie_request_time ? parseInt(userInfo.last_movie_request_time, 10) : undefined, }; } diff --git a/src/lib/upstash.db.ts b/src/lib/upstash.db.ts index aa83dda..4c0345f 100644 --- a/src/lib/upstash.db.ts +++ b/src/lib/upstash.db.ts @@ -65,7 +65,12 @@ export class UpstashRedisStorage implements IStorage { this._client = getUpstashRedisClient(); // 创建兼容Redis API的client包装器(支持camelCase和lowercase) this.client = { - hSet: (key: string, data: Record) => this._client.hset(key, data), + hSet: (key: string, field: string | Record, value?: string) => { + if (typeof field === 'string' && value !== undefined) { + return this._client.hset(key, { [field]: value }); + } + return this._client.hset(key, field as Record); + }, hset: (key: string, data: Record) => this._client.hset(key, data), zAdd: (key: string, member: { score: number; value: string }) => this._client.zadd(key, { score: member.score, member: member.value }), zadd: (key: string, member: { score: number; value: string }) => this._client.zadd(key, { score: member.score, member: member.value }), @@ -584,6 +589,7 @@ export class UpstashRedisStorage implements IStorage { playrecord_migrated?: boolean; favorite_migrated?: boolean; skip_migrated?: boolean; + last_movie_request_time?: number; } | null> { // 先从缓存获取 const cached = userInfoCache?.get(userName); @@ -677,6 +683,11 @@ export class UpstashRedisStorage implements IStorage { playrecord_migrated, favorite_migrated, skip_migrated, + last_movie_request_time: userInfo.last_movie_request_time + ? (typeof userInfo.last_movie_request_time === 'number' + ? userInfo.last_movie_request_time + : parseInt(userInfo.last_movie_request_time as string, 10)) + : undefined, }; // 存入缓存