增加收藏更新邮件发送
This commit is contained in:
@@ -33,6 +33,7 @@ import {
|
||||
ExternalLink,
|
||||
FileText,
|
||||
FolderOpen,
|
||||
Mail,
|
||||
Palette,
|
||||
Settings,
|
||||
Tv,
|
||||
@@ -8852,6 +8853,386 @@ const XiaoyaConfigComponent = ({
|
||||
);
|
||||
};
|
||||
|
||||
// 邮件配置组件
|
||||
const EmailConfigComponent = ({
|
||||
config,
|
||||
refreshConfig,
|
||||
}: {
|
||||
config: AdminConfig | null;
|
||||
refreshConfig: () => Promise<void>;
|
||||
}) => {
|
||||
const { alertModal, showAlert, hideAlert } = useAlertModal();
|
||||
const { isLoading, withLoading } = useLoadingState();
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [provider, setProvider] = useState<'smtp' | 'resend'>('smtp');
|
||||
|
||||
// SMTP配置
|
||||
const [smtpHost, setSmtpHost] = useState('');
|
||||
const [smtpPort, setSmtpPort] = useState(587);
|
||||
const [smtpSecure, setSmtpSecure] = useState(false);
|
||||
const [smtpUser, setSmtpUser] = useState('');
|
||||
const [smtpPassword, setSmtpPassword] = useState('');
|
||||
const [smtpFrom, setSmtpFrom] = useState('');
|
||||
|
||||
// Resend配置
|
||||
const [resendApiKey, setResendApiKey] = useState('');
|
||||
const [resendFrom, setResendFrom] = useState('');
|
||||
|
||||
// 测试邮件
|
||||
const [testEmail, setTestEmail] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (config?.EmailConfig) {
|
||||
setEnabled(config.EmailConfig.enabled || false);
|
||||
setProvider(config.EmailConfig.provider || 'smtp');
|
||||
|
||||
if (config.EmailConfig.smtp) {
|
||||
setSmtpHost(config.EmailConfig.smtp.host || '');
|
||||
setSmtpPort(config.EmailConfig.smtp.port || 587);
|
||||
setSmtpSecure(config.EmailConfig.smtp.secure || false);
|
||||
setSmtpUser(config.EmailConfig.smtp.user || '');
|
||||
setSmtpPassword(config.EmailConfig.smtp.password || '');
|
||||
setSmtpFrom(config.EmailConfig.smtp.from || '');
|
||||
}
|
||||
|
||||
if (config.EmailConfig.resend) {
|
||||
setResendApiKey(config.EmailConfig.resend.apiKey || '');
|
||||
setResendFrom(config.EmailConfig.resend.from || '');
|
||||
}
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
const handleSave = async () => {
|
||||
await withLoading('saveEmail', async () => {
|
||||
try {
|
||||
const emailConfig: AdminConfig['EmailConfig'] = {
|
||||
enabled,
|
||||
provider,
|
||||
smtp: provider === 'smtp' ? {
|
||||
host: smtpHost,
|
||||
port: smtpPort,
|
||||
secure: smtpSecure,
|
||||
user: smtpUser,
|
||||
password: smtpPassword,
|
||||
from: smtpFrom,
|
||||
} : undefined,
|
||||
resend: provider === 'resend' ? {
|
||||
apiKey: resendApiKey,
|
||||
from: resendFrom,
|
||||
} : undefined,
|
||||
};
|
||||
|
||||
const response = await fetch('/api/admin/email', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'save',
|
||||
config: emailConfig,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || '保存失败');
|
||||
}
|
||||
|
||||
showSuccess('保存成功', showAlert);
|
||||
await refreshConfig();
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '保存失败', showAlert);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleTest = async () => {
|
||||
if (!testEmail) {
|
||||
showError('请输入测试邮箱地址', showAlert);
|
||||
return;
|
||||
}
|
||||
|
||||
await withLoading('testEmail', async () => {
|
||||
try {
|
||||
const emailConfig: AdminConfig['EmailConfig'] = {
|
||||
enabled: true,
|
||||
provider,
|
||||
smtp: provider === 'smtp' ? {
|
||||
host: smtpHost,
|
||||
port: smtpPort,
|
||||
secure: smtpSecure,
|
||||
user: smtpUser,
|
||||
password: smtpPassword,
|
||||
from: smtpFrom,
|
||||
} : undefined,
|
||||
resend: provider === 'resend' ? {
|
||||
apiKey: resendApiKey,
|
||||
from: resendFrom,
|
||||
} : undefined,
|
||||
};
|
||||
|
||||
const response = await fetch('/api/admin/email', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'test',
|
||||
config: emailConfig,
|
||||
testEmail,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showSuccess('测试邮件发送成功,请检查收件箱', showAlert);
|
||||
} else {
|
||||
showError(data.error || '发送失败', showAlert);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '发送失败', showAlert);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
<div className='bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4'>
|
||||
<h3 className='text-sm font-medium text-blue-900 dark:text-blue-100 mb-2'>
|
||||
关于邮件通知
|
||||
</h3>
|
||||
<div className='text-sm text-blue-800 dark:text-blue-200 space-y-1'>
|
||||
<p>• 当用户收藏的影片有更新时,自动发送邮件通知</p>
|
||||
<p>• 支持 SMTP 和 Resend 两种发送方式</p>
|
||||
<p>• 用户可在个人设置中配置邮箱和通知偏好</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='space-y-4'>
|
||||
{/* 启用开关 */}
|
||||
<div className='flex items-center justify-between py-3 border-b border-gray-200 dark:border-gray-700'>
|
||||
<div>
|
||||
<h3 className='text-sm font-medium text-gray-900 dark:text-white'>
|
||||
启用邮件通知
|
||||
</h3>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
开启后用户可以接收收藏更新的邮件通知
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setEnabled(!enabled)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
enabled ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
enabled ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 发送方式选择 */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
发送方式
|
||||
</label>
|
||||
<div className='flex gap-4'>
|
||||
<label className='flex items-center'>
|
||||
<input
|
||||
type='radio'
|
||||
value='smtp'
|
||||
checked={provider === 'smtp'}
|
||||
onChange={(e) => setProvider(e.target.value as 'smtp')}
|
||||
className='mr-2'
|
||||
/>
|
||||
<span className='text-sm text-gray-700 dark:text-gray-300'>SMTP</span>
|
||||
</label>
|
||||
<label className='flex items-center'>
|
||||
<input
|
||||
type='radio'
|
||||
value='resend'
|
||||
checked={provider === 'resend'}
|
||||
onChange={(e) => setProvider(e.target.value as 'resend')}
|
||||
className='mr-2'
|
||||
/>
|
||||
<span className='text-sm text-gray-700 dark:text-gray-300'>Resend</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SMTP配置 */}
|
||||
{provider === 'smtp' && (
|
||||
<div className='space-y-4 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700'>
|
||||
<h4 className='text-sm font-medium text-gray-900 dark:text-white'>SMTP 配置</h4>
|
||||
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||
SMTP 主机 *
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
value={smtpHost}
|
||||
onChange={(e) => setSmtpHost(e.target.value)}
|
||||
placeholder='smtp.gmail.com'
|
||||
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-white'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||
SMTP 端口 *
|
||||
</label>
|
||||
<input
|
||||
type='number'
|
||||
value={smtpPort}
|
||||
onChange={(e) => setSmtpPort(parseInt(e.target.value))}
|
||||
placeholder='587'
|
||||
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-white'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={smtpSecure}
|
||||
onChange={(e) => setSmtpSecure(e.target.checked)}
|
||||
className='mr-2'
|
||||
/>
|
||||
<label className='text-sm text-gray-700 dark:text-gray-300'>
|
||||
使用 SSL/TLS(端口 465 时启用)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||
SMTP 用户名 *
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
value={smtpUser}
|
||||
onChange={(e) => setSmtpUser(e.target.value)}
|
||||
placeholder='[email protected]'
|
||||
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-white'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||
SMTP 密码 *
|
||||
</label>
|
||||
<input
|
||||
type='password'
|
||||
value={smtpPassword}
|
||||
onChange={(e) => setSmtpPassword(e.target.value)}
|
||||
placeholder='应用专用密码'
|
||||
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-white'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||
发件人邮箱 *
|
||||
</label>
|
||||
<input
|
||||
type='email'
|
||||
value={smtpFrom}
|
||||
onChange={(e) => setSmtpFrom(e.target.value)}
|
||||
placeholder='[email protected]'
|
||||
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-white'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Resend配置 */}
|
||||
{provider === 'resend' && (
|
||||
<div className='space-y-4 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700'>
|
||||
<h4 className='text-sm font-medium text-gray-900 dark:text-white'>Resend 配置</h4>
|
||||
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||
Resend API Key *
|
||||
</label>
|
||||
<input
|
||||
type='password'
|
||||
value={resendApiKey}
|
||||
onChange={(e) => setResendApiKey(e.target.value)}
|
||||
placeholder='re_xxxxx'
|
||||
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-white'
|
||||
/>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
在 <a href='https://resend.com/api-keys' target='_blank' rel='noopener noreferrer' className='text-blue-600 hover:underline'>Resend 控制台</a> 获取
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||
发件人邮箱 *
|
||||
</label>
|
||||
<input
|
||||
type='email'
|
||||
value={resendFrom}
|
||||
onChange={(e) => setResendFrom(e.target.value)}
|
||||
placeholder='[email protected]'
|
||||
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-white'
|
||||
/>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
需要先在 Resend 中验证域名
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 测试邮件 */}
|
||||
<div className='p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg'>
|
||||
<h4 className='text-sm font-medium text-blue-900 dark:text-blue-100 mb-2'>
|
||||
发送测试邮件
|
||||
</h4>
|
||||
<div className='flex flex-col sm:flex-row gap-2'>
|
||||
<input
|
||||
type='email'
|
||||
value={testEmail}
|
||||
onChange={(e) => setTestEmail(e.target.value)}
|
||||
placeholder='输入测试邮箱地址'
|
||||
className='flex-1 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-white text-sm'
|
||||
/>
|
||||
<button
|
||||
onClick={handleTest}
|
||||
disabled={isLoading('testEmail') || !testEmail}
|
||||
className={`${buttonStyles.primary} whitespace-nowrap`}
|
||||
>
|
||||
{isLoading('testEmail') ? '发送中...' : '发送测试'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<div className='flex gap-3'>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isLoading('saveEmail')}
|
||||
className={buttonStyles.success}
|
||||
>
|
||||
{isLoading('saveEmail') ? '保存中...' : '保存配置'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AlertModal
|
||||
isOpen={alertModal.isOpen}
|
||||
onClose={hideAlert}
|
||||
type={alertModal.type}
|
||||
title={alertModal.title}
|
||||
message={alertModal.message}
|
||||
timer={alertModal.timer}
|
||||
showConfirm={alertModal.showConfirm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 求片列表组件
|
||||
const MovieRequestsComponent = ({ config, refreshConfig }: { config: AdminConfig | null; refreshConfig: () => Promise<void> }) => {
|
||||
const { alertModal, showAlert, hideAlert } = useAlertModal();
|
||||
@@ -10322,6 +10703,7 @@ function AdminPageClient() {
|
||||
dataMigration: false,
|
||||
customAdFilter: false,
|
||||
themeConfig: false,
|
||||
emailConfig: false,
|
||||
});
|
||||
|
||||
// 获取管理员配置
|
||||
@@ -10712,6 +11094,18 @@ function AdminPageClient() {
|
||||
<AIConfigComponent config={config} refreshConfig={fetchConfig} />
|
||||
</CollapsibleTab>
|
||||
|
||||
{/* 邮件配置标签 */}
|
||||
<CollapsibleTab
|
||||
title='邮件配置'
|
||||
icon={
|
||||
<Mail size={20} className='text-gray-600 dark:text-gray-400' />
|
||||
}
|
||||
isExpanded={expandedTabs.emailConfig}
|
||||
onToggle={() => toggleTab('emailConfig')}
|
||||
>
|
||||
<EmailConfigComponent config={config} refreshConfig={fetchConfig} />
|
||||
</CollapsibleTab>
|
||||
|
||||
{/* 分类配置标签 */}
|
||||
<CollapsibleTab
|
||||
title='分类配置'
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getStorage } from '@/lib/db';
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { EmailService } from '@/lib/email.service';
|
||||
import type { AdminConfig } from '@/lib/admin.types';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
/**
|
||||
* GET - 获取邮件配置
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const storage = getStorage();
|
||||
const userInfo = await storage.getUserInfoV2?.(authInfo.username);
|
||||
|
||||
// 只有管理员和站长可以访问
|
||||
if (!userInfo || (userInfo.role !== 'admin' && userInfo.role !== 'owner')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const adminConfig = await getConfig();
|
||||
const emailConfig = adminConfig?.EmailConfig || {
|
||||
enabled: false,
|
||||
provider: 'smtp' as const,
|
||||
};
|
||||
|
||||
// 不返回敏感信息(密码、API Key)
|
||||
const safeConfig = {
|
||||
enabled: emailConfig.enabled,
|
||||
provider: emailConfig.provider,
|
||||
smtp: emailConfig.smtp
|
||||
? {
|
||||
host: emailConfig.smtp.host,
|
||||
port: emailConfig.smtp.port,
|
||||
secure: emailConfig.smtp.secure,
|
||||
user: emailConfig.smtp.user,
|
||||
from: emailConfig.smtp.from,
|
||||
password: emailConfig.smtp.password ? '******' : '',
|
||||
}
|
||||
: undefined,
|
||||
resend: emailConfig.resend
|
||||
? {
|
||||
from: emailConfig.resend.from,
|
||||
apiKey: emailConfig.resend.apiKey ? '******' : '',
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return NextResponse.json(safeConfig);
|
||||
} catch (error) {
|
||||
console.error('获取邮件配置失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST - 保存邮件配置或发送测试邮件
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const storage = getStorage();
|
||||
const userInfo = await storage.getUserInfoV2?.(authInfo.username);
|
||||
|
||||
// 只有管理员和站长可以访问
|
||||
if (!userInfo || (userInfo.role !== 'admin' && userInfo.role !== 'owner')) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { action, config, testEmail } = body;
|
||||
|
||||
// 发送测试邮件
|
||||
if (action === 'test') {
|
||||
if (!testEmail) {
|
||||
return NextResponse.json(
|
||||
{ error: '请提供测试邮箱地址' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const emailConfig = config as AdminConfig['EmailConfig'];
|
||||
if (!emailConfig || !emailConfig.enabled) {
|
||||
return NextResponse.json(
|
||||
{ error: '邮件配置未启用' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const adminConfig = await getConfig();
|
||||
const siteName = adminConfig?.SiteConfig?.SiteName || 'MoonTVPlus';
|
||||
await EmailService.sendTestEmail(emailConfig, testEmail, siteName);
|
||||
return NextResponse.json({ success: true, message: '测试邮件发送成功' });
|
||||
} catch (error) {
|
||||
console.error('发送测试邮件失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: `发送失败: ${(error as Error).message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存邮件配置
|
||||
if (action === 'save') {
|
||||
const emailConfig = config as AdminConfig['EmailConfig'];
|
||||
if (!emailConfig) {
|
||||
return NextResponse.json(
|
||||
{ error: '邮件配置不能为空' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 验证配置
|
||||
if (emailConfig.enabled) {
|
||||
if (emailConfig.provider === 'smtp') {
|
||||
if (!emailConfig.smtp?.host || !emailConfig.smtp?.port || !emailConfig.smtp?.user || !emailConfig.smtp?.from) {
|
||||
return NextResponse.json(
|
||||
{ error: 'SMTP配置不完整' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
} else if (emailConfig.provider === 'resend') {
|
||||
if (!emailConfig.resend?.apiKey || !emailConfig.resend?.from) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Resend配置不完整' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取现有配置
|
||||
const adminConfig = await getConfig();
|
||||
if (!adminConfig) {
|
||||
return NextResponse.json(
|
||||
{ error: '管理员配置不存在' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// 如果密码或API Key是占位符,保留原有值
|
||||
if (emailConfig.smtp?.password === '******') {
|
||||
const oldConfig = adminConfig.EmailConfig;
|
||||
if (oldConfig?.smtp?.password) {
|
||||
emailConfig.smtp.password = oldConfig.smtp.password;
|
||||
}
|
||||
}
|
||||
|
||||
if (emailConfig.resend?.apiKey === '******') {
|
||||
const oldConfig = adminConfig.EmailConfig;
|
||||
if (oldConfig?.resend?.apiKey) {
|
||||
emailConfig.resend.apiKey = oldConfig.resend.apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
adminConfig.EmailConfig = emailConfig;
|
||||
await storage.setAdminConfig(adminConfig);
|
||||
|
||||
return NextResponse.json({ success: true, message: '邮件配置保存成功' });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: '无效的操作' },
|
||||
{ status: 400 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('处理邮件配置失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import { fetchVideoDetail } from '@/lib/fetchVideoDetail';
|
||||
import { refreshLiveChannels } from '@/lib/live';
|
||||
import { startOpenListRefresh } from '@/lib/openlist-refresh';
|
||||
import { SearchResult } from '@/lib/types';
|
||||
import { EmailService } from '@/lib/email.service';
|
||||
import { getBatchFavoriteUpdateEmailTemplate, FavoriteUpdate } from '@/lib/email.templates';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -230,6 +232,7 @@ async function refreshRecordAndFavorites() {
|
||||
const totalFavorites = Object.keys(favorites).length;
|
||||
let processedFavorites = 0;
|
||||
const now = Date.now();
|
||||
const userUpdates: FavoriteUpdate[] = []; // 收集该用户的所有更新
|
||||
|
||||
for (const [key, fav] of Object.entries(favorites)) {
|
||||
try {
|
||||
@@ -279,6 +282,17 @@ async function refreshRecordAndFavorites() {
|
||||
|
||||
await storage.addNotification(user, notification);
|
||||
console.log(`已为用户 ${user} 创建收藏更新通知: ${fav.title}`);
|
||||
|
||||
// 收集更新信息用于邮件
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
||||
const playUrl = `${siteUrl}/play?source=${source}&id=${id}`;
|
||||
userUpdates.push({
|
||||
title: fav.title,
|
||||
oldEpisodes: fav.total_episodes,
|
||||
newEpisodes: favEpisodeCount,
|
||||
url: playUrl,
|
||||
cover: favDetail.poster || fav.cover,
|
||||
});
|
||||
}
|
||||
|
||||
processedFavorites++;
|
||||
@@ -289,6 +303,42 @@ async function refreshRecordAndFavorites() {
|
||||
}
|
||||
|
||||
console.log(`收藏处理完成: ${processedFavorites}/${totalFavorites}`);
|
||||
|
||||
// 如果有更新,发送汇总邮件
|
||||
if (userUpdates.length > 0) {
|
||||
try {
|
||||
const userEmail = storage.getUserEmail ? await storage.getUserEmail(user) : null;
|
||||
const emailNotifications = storage.getEmailNotificationPreference
|
||||
? await storage.getEmailNotificationPreference(user)
|
||||
: false;
|
||||
|
||||
if (userEmail && emailNotifications) {
|
||||
const config = await getConfig();
|
||||
const emailConfig = config?.EmailConfig;
|
||||
|
||||
if (emailConfig?.enabled) {
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
||||
const siteName = config?.SiteConfig?.SiteName || 'MoonTVPlus';
|
||||
|
||||
await EmailService.send(emailConfig, {
|
||||
to: userEmail,
|
||||
subject: `📺 收藏更新汇总 - ${userUpdates.length} 部影片有更新`,
|
||||
html: getBatchFavoriteUpdateEmailTemplate(
|
||||
user,
|
||||
userUpdates,
|
||||
siteUrl,
|
||||
siteName
|
||||
),
|
||||
});
|
||||
|
||||
console.log(`邮件汇总已发送至: ${userEmail} (${userUpdates.length} 个更新)`);
|
||||
}
|
||||
}
|
||||
} catch (emailError) {
|
||||
console.error(`发送邮件汇总失败 (${user}):`, emailError);
|
||||
// 邮件发送失败不影响主流程
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`获取用户收藏失败 (${user}):`, err);
|
||||
}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getStorage } from '@/lib/db';
|
||||
import { getAvailableApiSites } from '@/lib/config';
|
||||
import { getDetailFromApi } from '@/lib/downstream';
|
||||
import { Notification } from '@/lib/types';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const storage = getStorage();
|
||||
const username = authInfo.username;
|
||||
const now = Date.now();
|
||||
|
||||
console.log(`用户 ${username} 请求检查收藏更新`);
|
||||
console.log(`当前时间: ${new Date(now).toLocaleString('zh-CN')}`);
|
||||
console.log(`开始检查收藏更新...`);
|
||||
|
||||
// 获取所有收藏
|
||||
const favorites = await storage.getAllFavorites(username);
|
||||
const favoriteKeys = Object.keys(favorites);
|
||||
|
||||
if (favoriteKeys.length === 0) {
|
||||
return NextResponse.json({
|
||||
message: '没有收藏',
|
||||
updates: [],
|
||||
});
|
||||
}
|
||||
|
||||
// 获取可用的 API 站点
|
||||
const apiSites = await getAvailableApiSites(username);
|
||||
|
||||
// 检查每个收藏的更新
|
||||
const updates: Array<{
|
||||
source: string;
|
||||
id: string;
|
||||
title: string;
|
||||
old_episodes: number;
|
||||
new_episodes: number;
|
||||
}> = [];
|
||||
|
||||
// 限制并发请求数量,避免过载
|
||||
const BATCH_SIZE = 5;
|
||||
for (let i = 0; i < favoriteKeys.length; i += BATCH_SIZE) {
|
||||
const batch = favoriteKeys.slice(i, i + BATCH_SIZE);
|
||||
|
||||
await Promise.all(
|
||||
batch.map(async (key) => {
|
||||
try {
|
||||
const favorite = favorites[key];
|
||||
|
||||
// 跳过 live 类型的收藏
|
||||
if (favorite.origin === 'live') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 跳过已完结的收藏
|
||||
if (favorite.is_completed) {
|
||||
console.log(`跳过已完结的收藏: ${favorite.title}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析 source 和 id
|
||||
const [source, id] = key.split('+');
|
||||
if (!source || !id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找对应的 API 站点
|
||||
const apiSite = apiSites.find((site) => site.key === source);
|
||||
if (!apiSite) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取最新详情
|
||||
const detail = await getDetailFromApi(apiSite, id);
|
||||
|
||||
// 比较集数
|
||||
const oldEpisodes = favorite.total_episodes;
|
||||
const newEpisodes = detail.episodes.length;
|
||||
|
||||
console.log(`检查收藏: ${favorite.title} (${source}+${id})`);
|
||||
console.log(` 旧集数: ${oldEpisodes}, 新集数: ${newEpisodes}`);
|
||||
console.log(` 是否完结: ${favorite.is_completed}, 备注: ${favorite.vod_remarks}`);
|
||||
|
||||
if (newEpisodes > oldEpisodes) {
|
||||
updates.push({
|
||||
source,
|
||||
id,
|
||||
title: favorite.title,
|
||||
old_episodes: oldEpisodes,
|
||||
new_episodes: newEpisodes,
|
||||
});
|
||||
|
||||
// 更新收藏的集数和完结状态
|
||||
await storage.setFavorite(username, key, {
|
||||
...favorite,
|
||||
total_episodes: newEpisodes,
|
||||
is_completed: detail.vod_remarks
|
||||
? ['全', '完结', '大结局', 'end', '完'].some((keyword) =>
|
||||
detail.vod_remarks!.toLowerCase().includes(keyword)
|
||||
)
|
||||
: false,
|
||||
vod_remarks: detail.vod_remarks,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`检查收藏更新失败 (${key}):`, error);
|
||||
// 继续处理其他收藏
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`检查完成,发现 ${updates.length} 个更新`);
|
||||
|
||||
// 如果有更新,创建通知
|
||||
if (updates.length > 0) {
|
||||
for (const update of updates) {
|
||||
const notification: Notification = {
|
||||
id: `fav_update_${update.source}_${update.id}_${now}`,
|
||||
type: 'favorite_update',
|
||||
title: '收藏更新',
|
||||
message: `《${update.title}》有新集数更新!从 ${update.old_episodes} 集更新到 ${update.new_episodes} 集`,
|
||||
timestamp: now,
|
||||
read: false,
|
||||
metadata: {
|
||||
source: update.source,
|
||||
id: update.id,
|
||||
title: update.title,
|
||||
old_episodes: update.old_episodes,
|
||||
new_episodes: update.new_episodes,
|
||||
},
|
||||
};
|
||||
|
||||
await storage.addNotification(username, notification);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: updates.length > 0 ? `发现 ${updates.length} 个更新` : '没有更新',
|
||||
updates,
|
||||
checked: favoriteKeys.length,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('检查收藏更新失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getStorage } from '@/lib/db';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
/**
|
||||
* GET - 获取用户邮箱设置
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const storage = getStorage();
|
||||
const username = authInfo.username;
|
||||
|
||||
const email = storage.getUserEmail
|
||||
? await storage.getUserEmail(username)
|
||||
: null;
|
||||
|
||||
const emailNotifications = storage.getEmailNotificationPreference
|
||||
? await storage.getEmailNotificationPreference(username)
|
||||
: false;
|
||||
|
||||
return NextResponse.json({
|
||||
email: email || '',
|
||||
emailNotifications,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取用户邮箱设置失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST - 保存用户邮箱设置
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const storage = getStorage();
|
||||
const username = authInfo.username;
|
||||
const body = await request.json();
|
||||
const { email, emailNotifications } = body;
|
||||
|
||||
// 验证邮箱格式
|
||||
if (email && typeof email === 'string') {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return NextResponse.json(
|
||||
{ error: '邮箱格式不正确' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (storage.setUserEmail) {
|
||||
await storage.setUserEmail(username, email);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存邮件通知偏好
|
||||
if (typeof emailNotifications === 'boolean') {
|
||||
if (storage.setEmailNotificationPreference) {
|
||||
await storage.setEmailNotificationPreference(username, emailNotifications);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '邮箱设置保存成功',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('保存用户邮箱设置失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user