mirror of https://github.com/djteang/OrangeTV.git
Merge branch 'main' of https://github.com/djteang/OrangeTV
This commit is contained in:
commit
178f3a5b16
|
|
@ -77,6 +77,7 @@ services:
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
ports:
|
ports:
|
||||||
- '3000:3000'
|
- '3000:3000'
|
||||||
|
- '3001:3001'
|
||||||
environment:
|
environment:
|
||||||
- USERNAME=admin
|
- USERNAME=admin
|
||||||
- PASSWORD=orange
|
- PASSWORD=orange
|
||||||
|
|
@ -111,6 +112,7 @@ services:
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
ports:
|
ports:
|
||||||
- '3000:3000'
|
- '3000:3000'
|
||||||
|
- '3001:3001'
|
||||||
environment:
|
environment:
|
||||||
- USERNAME=admin
|
- USERNAME=admin
|
||||||
- PASSWORD=orange
|
- PASSWORD=orange
|
||||||
|
|
@ -147,6 +149,7 @@ services:
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
ports:
|
ports:
|
||||||
- '3000:3000'
|
- '3000:3000'
|
||||||
|
- '3001:3001'
|
||||||
environment:
|
environment:
|
||||||
- USERNAME=admin
|
- USERNAME=admin
|
||||||
- PASSWORD=orange
|
- PASSWORD=orange
|
||||||
|
|
|
||||||
|
|
@ -20,41 +20,74 @@ export async function GET(request: NextRequest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
const username = authInfo?.username;
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
||||||
}
|
|
||||||
const username = authInfo.username;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
const result: AdminConfigResult = {
|
|
||||||
Role: 'owner',
|
// 检查用户权限
|
||||||
Config: config,
|
let userRole = 'guest'; // 未登录用户为 guest
|
||||||
};
|
let isAdmin = false;
|
||||||
|
|
||||||
if (username === process.env.USERNAME) {
|
if (username === process.env.USERNAME) {
|
||||||
result.Role = 'owner';
|
userRole = 'owner';
|
||||||
} else {
|
isAdmin = true;
|
||||||
|
} else if (username) {
|
||||||
const user = config.UserConfig.Users.find((u) => u.username === username);
|
const user = config.UserConfig.Users.find((u) => u.username === username);
|
||||||
if (user && user.role === 'admin' && !user.banned) {
|
if (user && user.role === 'admin' && !user.banned) {
|
||||||
result.Role = 'admin';
|
userRole = 'admin';
|
||||||
|
isAdmin = true;
|
||||||
|
} else if (user && !user.banned) {
|
||||||
|
userRole = 'user';
|
||||||
|
} else if (user && user.banned) {
|
||||||
|
userRole = 'banned';
|
||||||
} else {
|
} else {
|
||||||
return NextResponse.json(
|
// 认证了但用户不存在,可能是数据不同步
|
||||||
{ error: '你是管理员吗你就访问?' },
|
userRole = 'unknown';
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(result, {
|
// 根据用户权限返回不同的配置信息
|
||||||
headers: {
|
if (isAdmin) {
|
||||||
'Cache-Control': 'no-store', // 管理员配置不缓存
|
// 管理员返回完整配置
|
||||||
},
|
const result: AdminConfigResult = {
|
||||||
});
|
Role: userRole as 'admin' | 'owner',
|
||||||
|
Config: config,
|
||||||
|
};
|
||||||
|
|
||||||
|
return NextResponse.json(result, {
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-store', // 管理员配置不缓存
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 普通用户或未登录用户只返回公开配置
|
||||||
|
const publicConfig = {
|
||||||
|
ThemeConfig: config.ThemeConfig,
|
||||||
|
SiteConfig: {
|
||||||
|
SiteName: config.SiteConfig.SiteName,
|
||||||
|
Announcement: config.SiteConfig.Announcement,
|
||||||
|
// 其他公开的站点配置可以在这里添加
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
Role: userRole,
|
||||||
|
Config: publicConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('返回公开配置给', userRole, ',包含主题配置:', !!publicConfig.ThemeConfig);
|
||||||
|
return NextResponse.json(result, {
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'public, max-age=60', // 公开配置可以缓存1分钟
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取管理员配置失败:', error);
|
console.error('获取配置失败:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: '获取管理员配置失败',
|
error: '获取配置失败',
|
||||||
details: (error as Error).message,
|
details: (error as Error).message,
|
||||||
},
|
},
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { AdminConfig } from '@/lib/admin.types';
|
import { AdminConfig } from '@/lib/admin.types';
|
||||||
import { headers, cookies } from 'next/headers';
|
import { headers, cookies } from 'next/headers';
|
||||||
|
import { getConfig, setCachedConfig, clearCachedConfig } from '@/lib/config';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -22,12 +23,8 @@ export async function GET() {
|
||||||
return NextResponse.json({ error: '认证信息无效' }, { status: 401 });
|
return NextResponse.json({ error: '认证信息无效' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await db.getAdminConfig();
|
const config = await getConfig();
|
||||||
const themeConfig = config?.ThemeConfig || {
|
const themeConfig = config.ThemeConfig;
|
||||||
defaultTheme: 'default' as const,
|
|
||||||
customCSS: '',
|
|
||||||
allowUserCustomization: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -75,40 +72,7 @@ export async function POST(request: Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前配置
|
// 获取当前配置
|
||||||
const currentConfig = await db.getAdminConfig();
|
const baseConfig = await getConfig();
|
||||||
|
|
||||||
// 如果没有配置,创建一个基础配置
|
|
||||||
let baseConfig: AdminConfig;
|
|
||||||
if (!currentConfig) {
|
|
||||||
baseConfig = {
|
|
||||||
ConfigSubscribtion: {
|
|
||||||
URL: "",
|
|
||||||
AutoUpdate: false,
|
|
||||||
LastCheck: "",
|
|
||||||
},
|
|
||||||
ConfigFile: "",
|
|
||||||
SiteConfig: {
|
|
||||||
SiteName: "OrangeTV",
|
|
||||||
Announcement: "",
|
|
||||||
SearchDownstreamMaxPage: 10,
|
|
||||||
SiteInterfaceCacheTime: 30,
|
|
||||||
DoubanProxyType: "direct",
|
|
||||||
DoubanProxy: "",
|
|
||||||
DoubanImageProxyType: "direct",
|
|
||||||
DoubanImageProxy: "",
|
|
||||||
DisableYellowFilter: false,
|
|
||||||
FluidSearch: true,
|
|
||||||
RequireDeviceCode: false,
|
|
||||||
},
|
|
||||||
UserConfig: {
|
|
||||||
Users: [],
|
|
||||||
},
|
|
||||||
SourceConfig: [],
|
|
||||||
CustomCategories: [],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
baseConfig = currentConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新主题配置
|
// 更新主题配置
|
||||||
const updatedConfig: AdminConfig = {
|
const updatedConfig: AdminConfig = {
|
||||||
|
|
@ -120,10 +84,23 @@ export async function POST(request: Request) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('保存主题配置:', updatedConfig.ThemeConfig);
|
console.log('=== 保存主题配置 ===');
|
||||||
|
console.log('请求参数:', { defaultTheme, customCSS, allowUserCustomization });
|
||||||
|
console.log('当前存储类型:', process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage');
|
||||||
|
console.log('待保存配置:', updatedConfig.ThemeConfig);
|
||||||
|
console.log('完整配置对象:', JSON.stringify(updatedConfig, null, 2));
|
||||||
|
|
||||||
await db.saveAdminConfig(updatedConfig);
|
await db.saveAdminConfig(updatedConfig);
|
||||||
console.log('主题配置保存成功');
|
console.log('主题配置保存成功');
|
||||||
|
|
||||||
|
// 直接更新缓存,确保缓存与数据库同步
|
||||||
|
await setCachedConfig(updatedConfig);
|
||||||
|
console.log('已更新配置缓存');
|
||||||
|
|
||||||
|
// 立即验证缓存中的配置
|
||||||
|
const cachedConfig = await getConfig();
|
||||||
|
console.log('保存后验证缓存中的配置:', cachedConfig.ThemeConfig);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '主题配置已更新',
|
message: '主题配置已更新',
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { db } from '@/lib/db';
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
console.log('API /theme: 开始获取主题配置...');
|
|
||||||
const config = await db.getAdminConfig();
|
|
||||||
|
|
||||||
console.log('API /theme: 获取到的管理员配置:', {
|
|
||||||
hasConfig: !!config,
|
|
||||||
hasThemeConfig: !!(config?.ThemeConfig),
|
|
||||||
themeConfig: config?.ThemeConfig
|
|
||||||
});
|
|
||||||
|
|
||||||
const themeConfig = config?.ThemeConfig || {
|
|
||||||
defaultTheme: 'default' as const,
|
|
||||||
customCSS: '',
|
|
||||||
allowUserCustomization: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('API /theme: 最终返回的主题配置:', themeConfig);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: themeConfig,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('API /theme: 获取主题配置失败,返回默认配置:', error);
|
|
||||||
|
|
||||||
const defaultThemeConfig = {
|
|
||||||
defaultTheme: 'default' as const,
|
|
||||||
customCSS: '',
|
|
||||||
allowUserCustomization: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: true, // 改为 success: true,因为我们提供了有效的默认配置
|
|
||||||
data: defaultThemeConfig,
|
|
||||||
fallback: true, // 标记这是备用配置
|
|
||||||
error: error instanceof Error ? error.message : '未知错误'
|
|
||||||
},
|
|
||||||
{ status: 200 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { Inter } from 'next/font/google';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { db } from '@/lib/db';
|
|
||||||
|
|
||||||
import { GlobalErrorIndicator } from '../components/GlobalErrorIndicator';
|
import { GlobalErrorIndicator } from '../components/GlobalErrorIndicator';
|
||||||
import { SiteProvider } from '../components/SiteProvider';
|
import { SiteProvider } from '../components/SiteProvider';
|
||||||
|
|
@ -44,33 +43,6 @@ export default async function RootLayout({
|
||||||
}) {
|
}) {
|
||||||
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
|
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
|
||||||
|
|
||||||
// 预先获取主题配置
|
|
||||||
let themeConfig = {
|
|
||||||
defaultTheme: 'default',
|
|
||||||
customCSS: '',
|
|
||||||
allowUserCustomization: true
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (storageType !== 'localstorage') {
|
|
||||||
const adminConfig = await db.getAdminConfig();
|
|
||||||
if (adminConfig?.ThemeConfig) {
|
|
||||||
themeConfig = {
|
|
||||||
defaultTheme: adminConfig.ThemeConfig.defaultTheme || 'default',
|
|
||||||
customCSS: adminConfig.ThemeConfig.customCSS || '',
|
|
||||||
allowUserCustomization: adminConfig.ThemeConfig.allowUserCustomization !== false,
|
|
||||||
};
|
|
||||||
console.log('服务端获取主题配置成功:', themeConfig);
|
|
||||||
} else {
|
|
||||||
console.log('服务端配置中没有主题配置,使用默认配置');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('LocalStorage模式,使用默认主题配置');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('服务端获取主题配置失败,使用默认配置:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
let siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'OrangeTV';
|
let siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'OrangeTV';
|
||||||
let announcement =
|
let announcement =
|
||||||
process.env.ANNOUNCEMENT ||
|
process.env.ANNOUNCEMENT ||
|
||||||
|
|
@ -122,7 +94,6 @@ export default async function RootLayout({
|
||||||
CUSTOM_CATEGORIES: customCategories,
|
CUSTOM_CATEGORIES: customCategories,
|
||||||
FLUID_SEARCH: fluidSearch,
|
FLUID_SEARCH: fluidSearch,
|
||||||
REQUIRE_DEVICE_CODE: requireDeviceCode,
|
REQUIRE_DEVICE_CODE: requireDeviceCode,
|
||||||
THEME_CONFIG: themeConfig,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -141,84 +112,59 @@ export default async function RootLayout({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 主题初始化脚本 - 内联执行,确保在生产环境中立即应用主题 */}
|
{/* 立即从缓存应用主题,避免闪烁 */}
|
||||||
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
|
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `
|
__html: `
|
||||||
(function() {
|
(function() {
|
||||||
try {
|
try {
|
||||||
console.log('开始初始化主题...');
|
// 从localStorage立即获取缓存的主题配置
|
||||||
|
const cachedTheme = localStorage.getItem('theme-cache');
|
||||||
|
|
||||||
// 应用主题函数
|
if (cachedTheme) {
|
||||||
function applyTheme(themeId, css) {
|
|
||||||
try {
|
try {
|
||||||
|
const themeConfig = JSON.parse(cachedTheme);
|
||||||
|
console.log('应用缓存主题配置:', themeConfig);
|
||||||
|
|
||||||
|
// 立即应用缓存的主题,避免闪烁
|
||||||
const html = document.documentElement;
|
const html = document.documentElement;
|
||||||
|
|
||||||
// 移除所有主题属性
|
// 清除现有主题
|
||||||
html.removeAttribute('data-theme');
|
html.removeAttribute('data-theme');
|
||||||
|
|
||||||
// 应用主题
|
// 应用缓存的主题
|
||||||
if (themeId && themeId !== 'default') {
|
if (themeConfig.defaultTheme && themeConfig.defaultTheme !== 'default') {
|
||||||
html.setAttribute('data-theme', themeId);
|
html.setAttribute('data-theme', themeConfig.defaultTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用自定义CSS
|
// 应用缓存的自定义CSS
|
||||||
if (css) {
|
if (themeConfig.customCSS) {
|
||||||
let customStyleEl = document.getElementById('custom-theme-css');
|
let customStyleEl = document.getElementById('custom-theme-css');
|
||||||
if (!customStyleEl) {
|
if (!customStyleEl) {
|
||||||
customStyleEl = document.createElement('style');
|
customStyleEl = document.createElement('style');
|
||||||
customStyleEl.id = 'custom-theme-css';
|
customStyleEl.id = 'custom-theme-css';
|
||||||
document.head.appendChild(customStyleEl);
|
document.head.appendChild(customStyleEl);
|
||||||
}
|
}
|
||||||
customStyleEl.textContent = css;
|
customStyleEl.textContent = themeConfig.customCSS;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
} catch (e) {
|
console.log('缓存主题已应用:', themeConfig.defaultTheme);
|
||||||
console.error('应用主题失败:', e);
|
} catch (parseError) {
|
||||||
return false;
|
console.warn('解析缓存主题配置失败:', parseError);
|
||||||
|
localStorage.removeItem('theme-cache'); // 清除无效缓存
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 获取预设的主题配置
|
|
||||||
let themeConfig = {
|
|
||||||
defaultTheme: 'default',
|
|
||||||
customCSS: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (window.RUNTIME_CONFIG && window.RUNTIME_CONFIG.THEME_CONFIG) {
|
|
||||||
themeConfig = window.RUNTIME_CONFIG.THEME_CONFIG;
|
|
||||||
console.log('使用服务端主题配置:', themeConfig);
|
|
||||||
} else {
|
|
||||||
console.log('未找到服务端主题配置,使用默认配置');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('获取服务端主题配置失败:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 立即应用主题配置
|
|
||||||
const success = applyTheme(themeConfig.defaultTheme, themeConfig.customCSS);
|
|
||||||
if (success) {
|
|
||||||
console.log('主题已初始化:', themeConfig.defaultTheme);
|
|
||||||
} else {
|
} else {
|
||||||
console.log('主题初始化失败,将等待客户端重新加载');
|
console.log('未找到缓存主题,等待API获取');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('主题初始化过程失败:', error);
|
console.error('应用缓存主题失败:', error);
|
||||||
// 最终备用方案:确保HTML至少没有错误的主题属性
|
|
||||||
try {
|
|
||||||
document.documentElement.removeAttribute('data-theme');
|
|
||||||
console.log('已清除主题属性作为备用方案');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('连备用方案也失败了:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
`,
|
`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
className={`${inter.className} min-h-screen bg-white text-gray-900 dark:bg-black dark:text-gray-200`}
|
className={`${inter.className} min-h-screen bg-white text-gray-900 dark:bg-black dark:text-gray-200`}
|
||||||
|
|
|
||||||
|
|
@ -2,90 +2,98 @@
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
// 全局主题加载器组件 - 确保主题配置始终正确,优先信任服务端配置
|
// 全局主题加载器组件 - 从API同步最新配置,确保缓存与服务端一致
|
||||||
const GlobalThemeLoader = () => {
|
const GlobalThemeLoader = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const validateAndSyncTheme = async () => {
|
const syncThemeWithAPI = async () => {
|
||||||
try {
|
try {
|
||||||
// 检查服务端预设的配置
|
console.log('从API同步主题配置...');
|
||||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
const response = await fetch('/api/admin/config');
|
||||||
const serverThemeConfig = runtimeConfig?.THEME_CONFIG;
|
|
||||||
|
|
||||||
console.log('检查服务端预设主题配置:', serverThemeConfig);
|
|
||||||
|
|
||||||
if (serverThemeConfig && serverThemeConfig.defaultTheme !== 'default') {
|
|
||||||
// 服务端有非默认配置,优先使用服务端配置
|
|
||||||
console.log('使用服务端主题配置,跳过API检查:', serverThemeConfig);
|
|
||||||
|
|
||||||
// 确保服务端配置已正确应用到DOM
|
|
||||||
const html = document.documentElement;
|
|
||||||
const currentTheme = html.getAttribute('data-theme');
|
|
||||||
|
|
||||||
if (currentTheme !== serverThemeConfig.defaultTheme) {
|
|
||||||
console.log('DOM主题与服务端配置不一致,重新应用:', {
|
|
||||||
current: currentTheme,
|
|
||||||
expected: serverThemeConfig.defaultTheme
|
|
||||||
});
|
|
||||||
applyTheme(serverThemeConfig.defaultTheme, serverThemeConfig.customCSS || '');
|
|
||||||
} else {
|
|
||||||
console.log('服务端主题配置已正确应用,无需更新');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只有当服务端配置为默认值时,才从API获取配置
|
|
||||||
console.log('服务端为默认配置,从API获取最新配置...');
|
|
||||||
const response = await fetch('/api/theme');
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (result?.Config?.ThemeConfig) {
|
||||||
const { defaultTheme, customCSS } = result.data;
|
const themeConfig = result.Config.ThemeConfig;
|
||||||
const isFallback = result.fallback;
|
const { defaultTheme, customCSS, allowUserCustomization } = themeConfig;
|
||||||
|
|
||||||
console.log('从API获取到主题配置:', {
|
console.log('API返回主题配置:', {
|
||||||
defaultTheme,
|
defaultTheme,
|
||||||
customCSS,
|
customCSS,
|
||||||
isFallback: !!isFallback,
|
allowUserCustomization
|
||||||
error: result.error
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isFallback) {
|
// 获取当前缓存的主题配置
|
||||||
console.log('API返回备用配置,保持服务端设置不变');
|
const cachedTheme = getCachedTheme();
|
||||||
return;
|
|
||||||
|
// 比较API配置与缓存配置
|
||||||
|
const configChanged = !cachedTheme ||
|
||||||
|
cachedTheme.defaultTheme !== defaultTheme ||
|
||||||
|
cachedTheme.customCSS !== customCSS;
|
||||||
|
|
||||||
|
if (configChanged) {
|
||||||
|
console.log('检测到主题配置变更,更新应用:', {
|
||||||
|
from: cachedTheme,
|
||||||
|
to: { defaultTheme, customCSS }
|
||||||
|
});
|
||||||
|
applyAndCacheTheme(defaultTheme, customCSS);
|
||||||
|
} else {
|
||||||
|
console.log('主题配置无变化,保持当前设置');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只有API返回非默认配置且非备用配置时才应用
|
// 将配置存储到运行时配置中,供ThemeManager使用
|
||||||
if (defaultTheme !== 'default' || customCSS) {
|
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||||
console.log('应用API获取的有效主题配置:', { defaultTheme, customCSS });
|
if (runtimeConfig) {
|
||||||
applyTheme(defaultTheme, customCSS);
|
runtimeConfig.THEME_CONFIG = themeConfig;
|
||||||
|
|
||||||
// 更新运行时配置
|
|
||||||
if (runtimeConfig) {
|
|
||||||
runtimeConfig.THEME_CONFIG = { defaultTheme, customCSS, allowUserCustomization: true };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('API返回默认配置,保持当前状态');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('API获取失败,保持当前主题配置');
|
console.log('无法获取主题配置,使用默认配置:', result);
|
||||||
|
// API失败时,如果有缓存就保持,没有缓存就用默认
|
||||||
|
const cachedTheme = getCachedTheme();
|
||||||
|
if (!cachedTheme) {
|
||||||
|
console.log('无缓存,应用默认主题');
|
||||||
|
applyAndCacheTheme('default', '');
|
||||||
|
} else {
|
||||||
|
console.log('保持缓存主题:', cachedTheme);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('主题配置验证失败:', error);
|
console.error('API同步失败:', error);
|
||||||
|
// 错误时如果有缓存就保持,没有缓存就用默认
|
||||||
// 出错时优先使用服务端配置
|
const cachedTheme = getCachedTheme();
|
||||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
if (!cachedTheme) {
|
||||||
const serverThemeConfig = runtimeConfig?.THEME_CONFIG;
|
console.log('无缓存且请求失败,应用默认主题');
|
||||||
|
applyAndCacheTheme('default', '');
|
||||||
if (serverThemeConfig) {
|
|
||||||
console.log('错误恢复:使用服务端配置:', serverThemeConfig);
|
|
||||||
applyTheme(serverThemeConfig.defaultTheme, serverThemeConfig.customCSS || '');
|
|
||||||
} else {
|
} else {
|
||||||
console.log('错误恢复:使用默认主题');
|
console.log('请求失败,保持缓存主题:', cachedTheme);
|
||||||
applyTheme('default', '');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取缓存的主题配置
|
||||||
|
const getCachedTheme = () => {
|
||||||
|
try {
|
||||||
|
const cached = localStorage.getItem('theme-cache');
|
||||||
|
return cached ? JSON.parse(cached) : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('读取主题缓存失败:', error);
|
||||||
|
localStorage.removeItem('theme-cache');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 应用主题并缓存
|
||||||
|
const applyAndCacheTheme = (themeId: string, css: string = '') => {
|
||||||
|
applyTheme(themeId, css);
|
||||||
|
|
||||||
|
// 缓存主题配置
|
||||||
|
const themeConfig = { defaultTheme: themeId, customCSS: css };
|
||||||
|
try {
|
||||||
|
localStorage.setItem('theme-cache', JSON.stringify(themeConfig));
|
||||||
|
console.log('主题配置已缓存:', themeConfig);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('缓存主题配置失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 应用主题函数
|
// 应用主题函数
|
||||||
const applyTheme = (themeId: string, css: string = '') => {
|
const applyTheme = (themeId: string, css: string = '') => {
|
||||||
const html = document.documentElement;
|
const html = document.documentElement;
|
||||||
|
|
@ -108,9 +116,9 @@ const GlobalThemeLoader = () => {
|
||||||
customStyleEl.textContent = css;
|
customStyleEl.textContent = css;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 稍作延迟,确保DOM完全加载后再验证主题
|
// 延迟一点时间,确保页面缓存主题已应用,然后同步API配置
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
validateAndSyncTheme();
|
syncThemeWithAPI();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
|
|
|
||||||
|
|
@ -277,32 +277,44 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
||||||
|
|
||||||
const isAdmin = role === 'admin' || role === 'owner';
|
const isAdmin = role === 'admin' || role === 'owner';
|
||||||
|
|
||||||
// 加载全局主题配置
|
// 更新主题缓存的辅助函数
|
||||||
|
const updateThemeCache = (themeId: string, css: string) => {
|
||||||
|
try {
|
||||||
|
const themeConfig = {
|
||||||
|
defaultTheme: themeId,
|
||||||
|
customCSS: css
|
||||||
|
};
|
||||||
|
localStorage.setItem('theme-cache', JSON.stringify(themeConfig));
|
||||||
|
console.log('主题配置已缓存:', themeConfig);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('缓存主题配置失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从API加载主题配置(唯一数据源)
|
||||||
const loadGlobalThemeConfig = async () => {
|
const loadGlobalThemeConfig = async () => {
|
||||||
try {
|
try {
|
||||||
// 优先使用运行时配置中的主题配置
|
console.log('从API获取主题配置...');
|
||||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
const response = await fetch('/api/admin/config');
|
||||||
const serverThemeConfig = runtimeConfig?.THEME_CONFIG;
|
|
||||||
|
|
||||||
if (serverThemeConfig) {
|
|
||||||
console.log('使用服务端预设的主题配置:', serverThemeConfig);
|
|
||||||
setGlobalThemeConfig(serverThemeConfig);
|
|
||||||
return serverThemeConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有服务端配置,则从API获取
|
|
||||||
console.log('服务端无主题配置,从API获取...');
|
|
||||||
const response = await fetch('/api/theme');
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
|
||||||
if (result.fallback) {
|
if (result?.Config?.ThemeConfig) {
|
||||||
console.log('API返回备用配置,可能存在数据库访问问题');
|
const themeConfig = result.Config.ThemeConfig;
|
||||||
|
console.log('API返回的主题配置:', themeConfig);
|
||||||
|
setGlobalThemeConfig(themeConfig);
|
||||||
|
|
||||||
|
// 更新运行时配置,保持同步
|
||||||
|
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||||
|
if (runtimeConfig) {
|
||||||
|
runtimeConfig.THEME_CONFIG = themeConfig;
|
||||||
}
|
}
|
||||||
setGlobalThemeConfig(result.data);
|
|
||||||
return result.data;
|
return themeConfig;
|
||||||
|
} else {
|
||||||
|
console.log('无法获取主题配置,可能未登录或权限不足:', result);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载全局主题配置失败:', error);
|
console.error('从API加载主题配置失败:', error);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
@ -332,6 +344,10 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
||||||
|
|
||||||
// 立即应用新的主题配置,确保当前页面也能看到更改
|
// 立即应用新的主题配置,确保当前页面也能看到更改
|
||||||
applyTheme(result.data.defaultTheme, result.data.customCSS);
|
applyTheme(result.data.defaultTheme, result.data.customCSS);
|
||||||
|
|
||||||
|
// 更新本地缓存
|
||||||
|
updateThemeCache(result.data.defaultTheme, result.data.customCSS);
|
||||||
|
|
||||||
console.log('已立即应用新主题配置:', result.data.defaultTheme);
|
console.log('已立即应用新主题配置:', result.data.defaultTheme);
|
||||||
|
|
||||||
showAlert({
|
showAlert({
|
||||||
|
|
@ -517,6 +533,9 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
||||||
allowUserCustomization: globalThemeConfig?.allowUserCustomization ?? true,
|
allowUserCustomization: globalThemeConfig?.allowUserCustomization ?? true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新本地缓存
|
||||||
|
updateThemeCache(currentTheme, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
showAlert({
|
showAlert({
|
||||||
|
|
|
||||||
|
|
@ -379,6 +379,15 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||||
adminConfig.SiteConfig.RequireDeviceCode = process.env.NEXT_PUBLIC_REQUIRE_DEVICE_CODE !== 'false';
|
adminConfig.SiteConfig.RequireDeviceCode = process.env.NEXT_PUBLIC_REQUIRE_DEVICE_CODE !== 'false';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保 ThemeConfig 存在
|
||||||
|
if (!adminConfig.ThemeConfig) {
|
||||||
|
adminConfig.ThemeConfig = {
|
||||||
|
defaultTheme: 'default',
|
||||||
|
customCSS: '',
|
||||||
|
allowUserCustomization: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 站长变更自检
|
// 站长变更自检
|
||||||
const ownerUser = process.env.USERNAME;
|
const ownerUser = process.env.USERNAME;
|
||||||
|
|
||||||
|
|
@ -516,4 +525,8 @@ export async function getAvailableApiSites(user?: string): Promise<ApiSite[]> {
|
||||||
|
|
||||||
export async function setCachedConfig(config: AdminConfig) {
|
export async function setCachedConfig(config: AdminConfig) {
|
||||||
cachedConfig = config;
|
cachedConfig = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCachedConfig() {
|
||||||
|
cachedConfig = undefined as any;
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +72,14 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
const val = await withRetry(() =>
|
const val = await withRetry(() =>
|
||||||
this.client.get(this.prKey(userName, key))
|
this.client.get(this.prKey(userName, key))
|
||||||
);
|
);
|
||||||
return val ? (val as PlayRecord) : null;
|
if (!val) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return typeof val === 'string' ? JSON.parse(val) : val as PlayRecord;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析播放记录失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setPlayRecord(
|
async setPlayRecord(
|
||||||
|
|
@ -80,7 +87,7 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
key: string,
|
key: string,
|
||||||
record: PlayRecord
|
record: PlayRecord
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await withRetry(() => this.client.set(this.prKey(userName, key), record));
|
await withRetry(() => this.client.set(this.prKey(userName, key), JSON.stringify(record)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllPlayRecords(
|
async getAllPlayRecords(
|
||||||
|
|
@ -94,9 +101,14 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
for (const fullKey of keys) {
|
for (const fullKey of keys) {
|
||||||
const value = await withRetry(() => this.client.get(fullKey));
|
const value = await withRetry(() => this.client.get(fullKey));
|
||||||
if (value) {
|
if (value) {
|
||||||
// 截取 source+id 部分
|
try {
|
||||||
const keyPart = ensureString(fullKey.replace(`u:${userName}:pr:`, ''));
|
// 截取 source+id 部分
|
||||||
result[keyPart] = value as PlayRecord;
|
const keyPart = ensureString(fullKey.replace(`u:${userName}:pr:`, ''));
|
||||||
|
const record = typeof value === 'string' ? JSON.parse(value) : value as PlayRecord;
|
||||||
|
result[keyPart] = record;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析播放记录失败:', error, 'key:', fullKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -115,7 +127,14 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
const val = await withRetry(() =>
|
const val = await withRetry(() =>
|
||||||
this.client.get(this.favKey(userName, key))
|
this.client.get(this.favKey(userName, key))
|
||||||
);
|
);
|
||||||
return val ? (val as Favorite) : null;
|
if (!val) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return typeof val === 'string' ? JSON.parse(val) : val as Favorite;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析收藏失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setFavorite(
|
async setFavorite(
|
||||||
|
|
@ -124,7 +143,7 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
favorite: Favorite
|
favorite: Favorite
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await withRetry(() =>
|
await withRetry(() =>
|
||||||
this.client.set(this.favKey(userName, key), favorite)
|
this.client.set(this.favKey(userName, key), JSON.stringify(favorite))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,8 +156,13 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
for (const fullKey of keys) {
|
for (const fullKey of keys) {
|
||||||
const value = await withRetry(() => this.client.get(fullKey));
|
const value = await withRetry(() => this.client.get(fullKey));
|
||||||
if (value) {
|
if (value) {
|
||||||
const keyPart = ensureString(fullKey.replace(`u:${userName}:fav:`, ''));
|
try {
|
||||||
result[keyPart] = value as Favorite;
|
const keyPart = ensureString(fullKey.replace(`u:${userName}:fav:`, ''));
|
||||||
|
const favorite = typeof value === 'string' ? JSON.parse(value) : value as Favorite;
|
||||||
|
result[keyPart] = favorite;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析收藏失败:', error, 'key:', fullKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -270,11 +294,24 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
|
|
||||||
async getAdminConfig(): Promise<AdminConfig | null> {
|
async getAdminConfig(): Promise<AdminConfig | null> {
|
||||||
const val = await withRetry(() => this.client.get(this.adminConfigKey()));
|
const val = await withRetry(() => this.client.get(this.adminConfigKey()));
|
||||||
return val ? (val as AdminConfig) : null;
|
if (!val) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 尝试解析JSON字符串(兼容BaseRedisStorage格式)
|
||||||
|
if (typeof val === 'string') {
|
||||||
|
return JSON.parse(val) as AdminConfig;
|
||||||
|
}
|
||||||
|
// 如果已经是对象,直接返回(Upstash自动反序列化的情况)
|
||||||
|
return val as AdminConfig;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析管理员配置失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAdminConfig(config: AdminConfig): Promise<void> {
|
async setAdminConfig(config: AdminConfig): Promise<void> {
|
||||||
await withRetry(() => this.client.set(this.adminConfigKey(), config));
|
// 统一使用JSON字符串格式存储,与BaseRedisStorage保持一致
|
||||||
|
await withRetry(() => this.client.set(this.adminConfigKey(), JSON.stringify(config)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 跳过片头片尾配置 ----------
|
// ---------- 跳过片头片尾配置 ----------
|
||||||
|
|
@ -290,7 +327,14 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
const val = await withRetry(() =>
|
const val = await withRetry(() =>
|
||||||
this.client.get(this.skipConfigKey(userName, source, id))
|
this.client.get(this.skipConfigKey(userName, source, id))
|
||||||
);
|
);
|
||||||
return val ? (val as SkipConfig) : null;
|
if (!val) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return typeof val === 'string' ? JSON.parse(val) : val as SkipConfig;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析跳过配置失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSkipConfig(
|
async setSkipConfig(
|
||||||
|
|
@ -300,7 +344,7 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
config: SkipConfig
|
config: SkipConfig
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await withRetry(() =>
|
await withRetry(() =>
|
||||||
this.client.set(this.skipConfigKey(userName, source, id), config)
|
this.client.set(this.skipConfigKey(userName, source, id), JSON.stringify(config))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -332,11 +376,16 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
keys.forEach((key, index) => {
|
keys.forEach((key, index) => {
|
||||||
const value = values[index];
|
const value = values[index];
|
||||||
if (value) {
|
if (value) {
|
||||||
// 从key中提取source+id
|
try {
|
||||||
const match = key.match(/^u:.+?:skip:(.+)$/);
|
// 从key中提取source+id
|
||||||
if (match) {
|
const match = key.match(/^u:.+?:skip:(.+)$/);
|
||||||
const sourceAndId = match[1];
|
if (match) {
|
||||||
configs[sourceAndId] = value as SkipConfig;
|
const sourceAndId = match[1];
|
||||||
|
const config = typeof value === 'string' ? JSON.parse(value) : value as SkipConfig;
|
||||||
|
configs[sourceAndId] = config;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析跳过配置失败:', error, 'key:', key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -373,7 +422,16 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
|
|
||||||
async getDanmu(videoId: string): Promise<any[]> {
|
async getDanmu(videoId: string): Promise<any[]> {
|
||||||
const val = await withRetry(() => this.client.lrange(this.danmuKey(videoId), 0, -1));
|
const val = await withRetry(() => this.client.lrange(this.danmuKey(videoId), 0, -1));
|
||||||
return val ? val.map(item => JSON.parse(ensureString(item))) : [];
|
if (!val || !Array.isArray(val)) return [];
|
||||||
|
|
||||||
|
return val.map(item => {
|
||||||
|
try {
|
||||||
|
return typeof item === 'string' ? JSON.parse(item) : item;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析弹幕数据失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}).filter(item => item !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveDanmu(videoId: string, userName: string, danmu: {
|
async saveDanmu(videoId: string, userName: string, danmu: {
|
||||||
|
|
@ -425,9 +483,11 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
if (!val) return null;
|
if (!val) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(ensureString(val));
|
// 处理不同的序列化格式
|
||||||
|
const data = typeof val === 'string' ? JSON.parse(val) : val;
|
||||||
return data.machineCode || null;
|
return data.machineCode || null;
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.error('解析用户机器码失败:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -439,7 +499,7 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
bindTime: Date.now()
|
bindTime: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存用户的机器码
|
// 保存用户的机器码 - 统一使用JSON序列化
|
||||||
await withRetry(() =>
|
await withRetry(() =>
|
||||||
this.client.set(this.machineCodeKey(userName), JSON.stringify(data))
|
this.client.set(this.machineCodeKey(userName), JSON.stringify(data))
|
||||||
);
|
);
|
||||||
|
|
@ -481,10 +541,11 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
|
|
||||||
if (val) {
|
if (val) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(ensureString(val));
|
// 处理不同的序列化格式
|
||||||
|
const data = typeof val === 'string' ? JSON.parse(val) : val;
|
||||||
result[userName] = data;
|
result[userName] = data;
|
||||||
} catch {
|
} catch (error) {
|
||||||
// 忽略解析错误
|
console.error('解析机器码用户数据失败:', error, 'key:', key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -536,9 +597,9 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
|
|
||||||
// 消息管理
|
// 消息管理
|
||||||
async saveMessage(message: ChatMessage): Promise<void> {
|
async saveMessage(message: ChatMessage): Promise<void> {
|
||||||
// 保存消息详情
|
// 保存消息详情 - 使用JSON序列化
|
||||||
await withRetry(() =>
|
await withRetry(() =>
|
||||||
this.client.set(this.messageKey(message.id), message)
|
this.client.set(this.messageKey(message.id), JSON.stringify(message))
|
||||||
);
|
);
|
||||||
|
|
||||||
// 将消息ID添加到对话的消息列表中(按时间排序)
|
// 将消息ID添加到对话的消息列表中(按时间排序)
|
||||||
|
|
@ -560,7 +621,12 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
for (const messageId of messageIds) {
|
for (const messageId of messageIds) {
|
||||||
const messageData = await withRetry(() => this.client.get(this.messageKey(messageId as string)));
|
const messageData = await withRetry(() => this.client.get(this.messageKey(messageId as string)));
|
||||||
if (messageData) {
|
if (messageData) {
|
||||||
messages.push(messageData as ChatMessage);
|
try {
|
||||||
|
const message = typeof messageData === 'string' ? JSON.parse(messageData) : messageData;
|
||||||
|
messages.push(message as ChatMessage);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析消息失败:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -570,11 +636,15 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
async markMessageAsRead(messageId: string): Promise<void> {
|
async markMessageAsRead(messageId: string): Promise<void> {
|
||||||
const messageData = await withRetry(() => this.client.get(this.messageKey(messageId)));
|
const messageData = await withRetry(() => this.client.get(this.messageKey(messageId)));
|
||||||
if (messageData) {
|
if (messageData) {
|
||||||
const message = messageData as ChatMessage;
|
try {
|
||||||
message.is_read = true;
|
const message = typeof messageData === 'string' ? JSON.parse(messageData) : messageData as ChatMessage;
|
||||||
await withRetry(() =>
|
message.is_read = true;
|
||||||
this.client.set(this.messageKey(messageId), message)
|
await withRetry(() =>
|
||||||
);
|
this.client.set(this.messageKey(messageId), JSON.stringify(message))
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('标记消息为已读失败:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -601,13 +671,20 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
this.client.get(this.conversationKey(conversationId))
|
this.client.get(this.conversationKey(conversationId))
|
||||||
);
|
);
|
||||||
|
|
||||||
return conversationData ? (conversationData as Conversation) : null;
|
if (!conversationData) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return typeof conversationData === 'string' ? JSON.parse(conversationData) : conversationData as Conversation;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析对话失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createConversation(conversation: Conversation): Promise<void> {
|
async createConversation(conversation: Conversation): Promise<void> {
|
||||||
// 保存对话详情
|
// 保存对话详情 - 使用JSON序列化
|
||||||
await withRetry(() =>
|
await withRetry(() =>
|
||||||
this.client.set(this.conversationKey(conversation.id), conversation)
|
this.client.set(this.conversationKey(conversation.id), JSON.stringify(conversation))
|
||||||
);
|
);
|
||||||
|
|
||||||
// 将对话ID添加到每个参与者的对话列表中
|
// 将对话ID添加到每个参与者的对话列表中
|
||||||
|
|
@ -623,7 +700,7 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
if (conversation) {
|
if (conversation) {
|
||||||
Object.assign(conversation, updates);
|
Object.assign(conversation, updates);
|
||||||
await withRetry(() =>
|
await withRetry(() =>
|
||||||
this.client.set(this.conversationKey(conversationId), conversation)
|
this.client.set(this.conversationKey(conversationId), JSON.stringify(conversation))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -656,7 +733,12 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
for (const friendId of friendIds) {
|
for (const friendId of friendIds) {
|
||||||
const friendData = await withRetry(() => this.client.get(this.friendKey(friendId)));
|
const friendData = await withRetry(() => this.client.get(this.friendKey(friendId)));
|
||||||
if (friendData) {
|
if (friendData) {
|
||||||
friends.push(friendData as Friend);
|
try {
|
||||||
|
const friend = typeof friendData === 'string' ? JSON.parse(friendData) : friendData;
|
||||||
|
friends.push(friend as Friend);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析好友数据失败:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -664,9 +746,9 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
async addFriend(userName: string, friend: Friend): Promise<void> {
|
async addFriend(userName: string, friend: Friend): Promise<void> {
|
||||||
// 保存好友详情
|
// 保存好友详情 - 使用JSON序列化
|
||||||
await withRetry(() =>
|
await withRetry(() =>
|
||||||
this.client.set(this.friendKey(friend.id), friend)
|
this.client.set(this.friendKey(friend.id), JSON.stringify(friend))
|
||||||
);
|
);
|
||||||
|
|
||||||
// 将好友ID添加到用户的好友列表中
|
// 将好友ID添加到用户的好友列表中
|
||||||
|
|
@ -688,11 +770,15 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
async updateFriendStatus(friendId: string, status: Friend['status']): Promise<void> {
|
async updateFriendStatus(friendId: string, status: Friend['status']): Promise<void> {
|
||||||
const friendData = await withRetry(() => this.client.get(this.friendKey(friendId)));
|
const friendData = await withRetry(() => this.client.get(this.friendKey(friendId)));
|
||||||
if (friendData) {
|
if (friendData) {
|
||||||
const friend = friendData as Friend;
|
try {
|
||||||
friend.status = status;
|
const friend = typeof friendData === 'string' ? JSON.parse(friendData) : friendData as Friend;
|
||||||
await withRetry(() =>
|
friend.status = status;
|
||||||
this.client.set(this.friendKey(friendId), friend)
|
await withRetry(() =>
|
||||||
);
|
this.client.set(this.friendKey(friendId), JSON.stringify(friend))
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新好友状态失败:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -706,10 +792,14 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
for (const requestId of requestIds) {
|
for (const requestId of requestIds) {
|
||||||
const requestData = await withRetry(() => this.client.get(this.friendRequestKey(requestId)));
|
const requestData = await withRetry(() => this.client.get(this.friendRequestKey(requestId)));
|
||||||
if (requestData) {
|
if (requestData) {
|
||||||
const request = requestData as FriendRequest;
|
try {
|
||||||
// 只返回相关的申请(发送给该用户的或该用户发送的)
|
const request = typeof requestData === 'string' ? JSON.parse(requestData) : requestData as FriendRequest;
|
||||||
if (request.to_user === userName || request.from_user === userName) {
|
// 只返回相关的申请(发送给该用户的或该用户发送的)
|
||||||
requests.push(request);
|
if (request.to_user === userName || request.from_user === userName) {
|
||||||
|
requests.push(request);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析好友申请失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -718,9 +808,9 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
async createFriendRequest(request: FriendRequest): Promise<void> {
|
async createFriendRequest(request: FriendRequest): Promise<void> {
|
||||||
// 保存申请详情
|
// 保存申请详情 - 使用JSON序列化
|
||||||
await withRetry(() =>
|
await withRetry(() =>
|
||||||
this.client.set(this.friendRequestKey(request.id), request)
|
this.client.set(this.friendRequestKey(request.id), JSON.stringify(request))
|
||||||
);
|
);
|
||||||
|
|
||||||
// 将申请ID添加到双方的申请列表中
|
// 将申请ID添加到双方的申请列表中
|
||||||
|
|
@ -735,27 +825,35 @@ export class UpstashRedisStorage implements IStorage {
|
||||||
async updateFriendRequest(requestId: string, status: FriendRequest['status']): Promise<void> {
|
async updateFriendRequest(requestId: string, status: FriendRequest['status']): Promise<void> {
|
||||||
const requestData = await withRetry(() => this.client.get(this.friendRequestKey(requestId)));
|
const requestData = await withRetry(() => this.client.get(this.friendRequestKey(requestId)));
|
||||||
if (requestData) {
|
if (requestData) {
|
||||||
const request = requestData as FriendRequest;
|
try {
|
||||||
request.status = status;
|
const request = typeof requestData === 'string' ? JSON.parse(requestData) : requestData as FriendRequest;
|
||||||
request.updated_at = Date.now();
|
request.status = status;
|
||||||
await withRetry(() =>
|
request.updated_at = Date.now();
|
||||||
this.client.set(this.friendRequestKey(requestId), request)
|
await withRetry(() =>
|
||||||
);
|
this.client.set(this.friendRequestKey(requestId), JSON.stringify(request))
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新好友申请失败:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteFriendRequest(requestId: string): Promise<void> {
|
async deleteFriendRequest(requestId: string): Promise<void> {
|
||||||
const requestData = await withRetry(() => this.client.get(this.friendRequestKey(requestId)));
|
const requestData = await withRetry(() => this.client.get(this.friendRequestKey(requestId)));
|
||||||
if (requestData) {
|
if (requestData) {
|
||||||
const request = requestData as FriendRequest;
|
try {
|
||||||
|
const request = typeof requestData === 'string' ? JSON.parse(requestData) : requestData as FriendRequest;
|
||||||
|
|
||||||
// 从双方的申请列表中移除
|
// 从双方的申请列表中移除
|
||||||
await withRetry(() =>
|
await withRetry(() =>
|
||||||
this.client.srem(this.userFriendRequestsKey(request.from_user), requestId)
|
this.client.srem(this.userFriendRequestsKey(request.from_user), requestId)
|
||||||
);
|
);
|
||||||
await withRetry(() =>
|
await withRetry(() =>
|
||||||
this.client.srem(this.userFriendRequestsKey(request.to_user), requestId)
|
this.client.srem(this.userFriendRequestsKey(request.to_user), requestId)
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除好友申请失败:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除申请详情
|
// 删除申请详情
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,6 @@ function shouldSkipAuth(pathname: string): boolean {
|
||||||
// 配置middleware匹配规则
|
// 配置middleware匹配规则
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: [
|
||||||
'/((?!_next/static|_next/image|favicon.ico|login|warning|api/login|api/register|api/logout|api/cron|api/server-config|api/theme).*)',
|
'/((?!_next/static|_next/image|favicon.ico|login|warning|api/login|api/register|api/logout|api/cron|api/server-config).*)',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue