From ef8d17719f2af0ddabdbd97dfe6ce21fa67969b2 Mon Sep 17 00:00:00 2001 From: ggyy <34892002@qq.com> Date: Wed, 22 Apr 2026 18:48:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E9=82=AE=E4=BB=B6):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=A4=9A=E9=82=AE=E5=B1=80=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将邮件配置从单例改为多实例,支持创建、编辑、删除多个配置 - 修改数据库模型,移除 provider 唯一约束,添加索引 - 新增配置激活、删除、清空日志功能 - 重构服务层,统一推送设置同步逻辑 - 更新前端接口,支持配置 ID 参数传递 --- modules/auth/service.ts | 2 +- modules/email/repository.ts | 98 ++- modules/email/service.ts | 357 +++++--- modules/email/types.ts | 6 + pages/admin/email/+Page.vue | 759 +++++++++++++----- pages/admin/email/+data.ts | 12 +- pages/admin/email/saveEmailConfig.telefunc.ts | 16 +- pages/admin/email/sendTestEmail.telefunc.ts | 1 + prisma/migrations/0001_init.sql | 2 +- prisma/migrations/migration_lock.toml | 4 +- prisma/schema.prisma | 4 +- 11 files changed, 922 insertions(+), 339 deletions(-) diff --git a/modules/auth/service.ts b/modules/auth/service.ts index f4dfac3..0739c9e 100644 --- a/modules/auth/service.ts +++ b/modules/auth/service.ts @@ -139,7 +139,7 @@ export async function logAdminOperation(input: { const prisma = options?.prisma ?? context!.prisma; const adminId = options?.adminId ?? Number(context?.session?.user?.id); - if (!Number.isFinite(adminId)) { + if (!Number.isFinite(adminId) || adminId <= 0) { return; } diff --git a/modules/email/repository.ts b/modules/email/repository.ts index 8b30819..49760ae 100644 --- a/modules/email/repository.ts +++ b/modules/email/repository.ts @@ -3,34 +3,34 @@ import type { EmailApiProvider, EmailChannel, EmailScene } from "./types"; export function listEmailConfigRecords(prisma: PrismaClient) { return prisma.emailConfig.findMany({ - orderBy: [{ provider: "asc" }], + orderBy: [{ id: "asc" }], }); } -export function getEmailConfigRecord(prisma: PrismaClient, provider: EmailChannel) { +export function getEmailConfigRecordById(prisma: PrismaClient, id: number) { return prisma.emailConfig.findUnique({ - where: { provider }, + where: { id }, }); } -export function upsertEmailConfigRecord( +export function getActiveEmailConfigRecord(prisma: PrismaClient) { + return prisma.emailConfig.findFirst({ + where: { isEnabled: true }, + }); +} + +export function createEmailConfigRecord( prisma: PrismaClient, - provider: EmailChannel, input: { + provider: EmailChannel; name: string; isEnabled: boolean; configJson: string; }, ) { - return prisma.emailConfig.upsert({ - where: { provider }, - create: { - provider, - name: input.name, - isEnabled: input.isEnabled, - configJson: input.configJson, - }, - update: { + return prisma.emailConfig.create({ + data: { + provider: input.provider, name: input.name, isEnabled: input.isEnabled, configJson: input.configJson, @@ -38,6 +38,74 @@ export function upsertEmailConfigRecord( }); } +export function updateEmailConfigRecord( + prisma: PrismaClient, + id: number, + input: { + provider?: EmailChannel; + name?: string; + isEnabled?: boolean; + configJson?: string; + }, +) { + return prisma.emailConfig.update({ + where: { id }, + data: input, + }); +} + +export function deleteEmailConfigRecord(prisma: PrismaClient, id: number) { + return prisma.emailConfig.delete({ + where: { id }, + }); +} + + + +export async function activateEmailConfigById(prisma: PrismaClient, id: number) { + await prisma.emailConfig.updateMany({ + where: { id: { not: id } }, + data: { isEnabled: false }, + }); + return prisma.emailConfig.update({ + where: { id }, + data: { isEnabled: true }, + }); +} + +export async function updatePushFlagsForAllConfigs( + prisma: PrismaClient, + flags: { + customerSendOrderPaidEmail: boolean; + customerSendDeliverySuccessEmail: boolean; + customerSendDeliveryFailedEmail: boolean; + adminSendOrderPaidEmail: boolean; + adminSendDeliverySuccessEmail: boolean; + adminSendDeliveryFailedEmail: boolean; + }, +) { + const records = await prisma.emailConfig.findMany(); + for (const record of records) { + let configJson: Record = {}; + try { + configJson = JSON.parse(record.configJson); + } catch { + configJson = {}; + } + configJson.customerSendOrderPaidEmail = flags.customerSendOrderPaidEmail; + configJson.customerSendDeliverySuccessEmail = flags.customerSendDeliverySuccessEmail; + configJson.customerSendDeliveryFailedEmail = flags.customerSendDeliveryFailedEmail; + configJson.adminSendOrderPaidEmail = flags.adminSendOrderPaidEmail; + configJson.adminSendDeliverySuccessEmail = flags.adminSendDeliverySuccessEmail; + configJson.adminSendDeliveryFailedEmail = flags.adminSendDeliveryFailedEmail; + + await prisma.emailConfig.update({ + where: { id: record.id }, + data: { configJson: JSON.stringify(configJson) }, + }); + } +} + export function listEmailTemplateRecords(prisma: PrismaClient) { return prisma.emailTemplate.findMany({ orderBy: [{ scene: "asc" }], @@ -108,4 +176,4 @@ export function createEmailLogRecord( triggeredBy: input.triggeredBy ?? null, }, }); -} +} \ No newline at end of file diff --git a/modules/email/service.ts b/modules/email/service.ts index f2fd470..ad862e3 100644 --- a/modules/email/service.ts +++ b/modules/email/service.ts @@ -6,15 +6,37 @@ import { validateEmailConfigInput, validateEmailTemplateInput, validateTestEmail import { getAdminContext, logAdminOperation } from "../auth/service"; import { getSiteSetting } from "../site/service"; import { createApiEmailAdapter } from "./provider"; -import { createEmailLogRecord, getEmailConfigRecord, listEmailConfigRecords, listEmailLogRecords, listEmailTemplateRecords, upsertEmailConfigRecord, upsertEmailTemplateRecord } from "./repository"; -import type { EmailApiConfigValue, EmailChannel, EmailCloudflareConfigValue, EmailConfigValue, EmailLogItem, EmailOverviewMetric, EmailScene, EmailSmtpConfigValue, EmailTemplateValue } from "./types"; +import { + activateEmailConfigById, + createEmailConfigRecord, + createEmailLogRecord, + deleteEmailConfigRecord, + getActiveEmailConfigRecord, + getEmailConfigRecordById, + listEmailConfigRecords, + listEmailLogRecords, + listEmailTemplateRecords, + updateEmailConfigRecord, + updatePushFlagsForAllConfigs, + upsertEmailTemplateRecord, +} from "./repository"; +import type { + EmailApiConfigValue, + EmailChannel, + EmailCloudflareConfigValue, + EmailConfigValue, + EmailLogItem, + EmailOverviewMetric, + EmailScene, + EmailSmtpConfigValue, + EmailTemplateValue, +} from "./types"; type EmailConfigRecord = Awaited>[number]; type EmailTemplateRecord = Awaited>[number]; type EmailLogRecord = Awaited>[number]; const emailScenes = ["TEST", "ORDER_PAID", "DELIVERY_SUCCESS", "DELIVERY_FAILED"] as const; -const emailChannels = ["API", "SMTP", "CLOUDFLARE"] as const; function getChannelDisplayName(channel: EmailChannel) { return channel === "API" ? "API" : channel === "SMTP" ? "SMTP" : "CloudFlare"; @@ -105,28 +127,30 @@ function getEmailContext() { } function getDefaultConfig(provider: EmailChannel): EmailConfigValue { - if (provider === "API") return defaultApiConfig; - if (provider === "SMTP") return defaultSmtpConfig; - return defaultCloudflareConfig; + if (provider === "API") return { ...defaultApiConfig }; + if (provider === "SMTP") return { ...defaultSmtpConfig }; + return { ...defaultCloudflareConfig }; } -function normalizeEmailConfig(record: Awaited>, provider: EmailChannel): EmailConfigValue { +function normalizeEmailConfigFromRecord(record: EmailConfigRecord): EmailConfigValue { + const provider = record.provider as EmailChannel; const defaults = getDefaultConfig(provider); - if (!record) { - return defaults; - } try { const parsed = JSON.parse(record.configJson) as Partial; return { ...defaults, ...parsed, + id: record.id, + name: record.name, provider, isEnabled: record.isEnabled, } as EmailConfigValue; } catch { return { ...defaults, + id: record.id, + name: record.name, isEnabled: record.isEnabled, } as EmailConfigValue; } @@ -198,14 +222,13 @@ async function createLog(prisma: PrismaClient, input: { }); } -async function getActiveEmailConfig(prisma: PrismaClient) { - const records = await listEmailConfigRecords(prisma); - const activeRecord = records.find((record: EmailConfigRecord) => record.isEnabled); - if (!activeRecord) { - throw badRequestError("请先启用一个邮件发送分类", "EMAIL_CHANNEL_NOT_ENABLED"); +async function getActiveEmailConfig(prisma: PrismaClient): Promise { + const record = await getActiveEmailConfigRecord(prisma); + if (!record) { + throw badRequestError("请先启用一个邮局配置", "EMAIL_CHANNEL_NOT_ENABLED"); } - return normalizeEmailConfig(activeRecord, activeRecord.provider as EmailChannel); + return normalizeEmailConfigFromRecord(record); } async function getEmailBaseValues(prisma: PrismaClient) { @@ -259,8 +282,9 @@ async function sendSceneEmail(prisma: PrismaClient, input: { values: Record; orderId?: number; triggeredBy?: string; + config?: EmailConfigValue; }) { - const config = await getActiveEmailConfig(prisma); + const config = input.config ?? await getActiveEmailConfig(prisma); const templates = await listEmailTemplateRecords(prisma); const template = normalizeEmailTemplate(templates.find((item: EmailTemplateRecord) => item.scene === input.scene), input.scene); @@ -321,6 +345,10 @@ async function sendSceneEmail(prisma: PrismaClient, input: { } } +// --------------------------------------------------------------------------- +// Public API: management data +// --------------------------------------------------------------------------- + export async function getEmailManagementData(prisma?: PrismaClient) { const client = prisma ?? getEmailContext().prisma; const [configRecords, templateRecords, logRecords] = await Promise.all([ @@ -329,11 +357,16 @@ export async function getEmailManagementData(prisma?: PrismaClient) { listEmailLogRecords(client, 100), ]); - const configs = Object.fromEntries( - emailChannels.map((provider) => [provider, normalizeEmailConfig(configRecords.find((item: EmailConfigRecord) => item.provider === provider) ?? null, provider)]), - ) as Record; + const configs: EmailConfigValue[] = configRecords.map((record: EmailConfigRecord) => + normalizeEmailConfigFromRecord(record), + ); - const templates = emailScenes.map((scene) => normalizeEmailTemplate(templateRecords.find((item: EmailTemplateRecord) => item.scene === scene), scene)); + const templates = emailScenes.map((scene) => + normalizeEmailTemplate( + templateRecords.find((item: EmailTemplateRecord) => item.scene === scene), + scene, + ), + ); const statsMap = { total: logRecords.length, @@ -363,14 +396,30 @@ export async function getEmailManagementData(prisma?: PrismaClient) { createdAt: item.createdAt.toISOString(), })); + // Derive current push settings from the first config (they're synced across all) + const firstConfig = configs[0] ?? defaultApiConfig; + const pushSettings = { + customerSendOrderPaidEmail: firstConfig.customerSendOrderPaidEmail, + customerSendDeliverySuccessEmail: firstConfig.customerSendDeliverySuccessEmail, + customerSendDeliveryFailedEmail: firstConfig.customerSendDeliveryFailedEmail, + adminSendOrderPaidEmail: firstConfig.adminSendOrderPaidEmail, + adminSendDeliverySuccessEmail: firstConfig.adminSendDeliverySuccessEmail, + adminSendDeliveryFailedEmail: firstConfig.adminSendDeliveryFailedEmail, + }; + return { configs, templates, logs, metrics, + pushSettings, }; } +// --------------------------------------------------------------------------- +// Public API: save push settings +// --------------------------------------------------------------------------- + export async function saveEmailPushSettings(input: { customerSendOrderPaidEmail: boolean; customerSendDeliverySuccessEmail: boolean; @@ -383,27 +432,14 @@ export async function saveEmailPushSettings(input: { const { prisma } = adminContext; const adminId = Number(adminContext.session?.user?.id); - const records = await prisma.emailConfig.findMany(); - - for (const record of records) { - let configJson: Record = {}; - try { - configJson = JSON.parse(record.configJson); - } catch { - configJson = {}; - } - configJson.customerSendOrderPaidEmail = Boolean(input.customerSendOrderPaidEmail); - configJson.customerSendDeliverySuccessEmail = Boolean(input.customerSendDeliverySuccessEmail); - configJson.customerSendDeliveryFailedEmail = Boolean(input.customerSendDeliveryFailedEmail); - configJson.adminSendOrderPaidEmail = Boolean(input.adminSendOrderPaidEmail); - configJson.adminSendDeliverySuccessEmail = Boolean(input.adminSendDeliverySuccessEmail); - configJson.adminSendDeliveryFailedEmail = Boolean(input.adminSendDeliveryFailedEmail); - - await prisma.emailConfig.update({ - where: { id: record.id }, - data: { configJson: JSON.stringify(configJson) } - }); - } + await updatePushFlagsForAllConfigs(prisma, { + customerSendOrderPaidEmail: Boolean(input.customerSendOrderPaidEmail), + customerSendDeliverySuccessEmail: Boolean(input.customerSendDeliverySuccessEmail), + customerSendDeliveryFailedEmail: Boolean(input.customerSendDeliveryFailedEmail), + adminSendOrderPaidEmail: Boolean(input.adminSendOrderPaidEmail), + adminSendDeliverySuccessEmail: Boolean(input.adminSendDeliverySuccessEmail), + adminSendDeliveryFailedEmail: Boolean(input.adminSendDeliveryFailedEmail), + }); await logAdminOperation( { @@ -420,32 +456,28 @@ export async function saveEmailPushSettings(input: { return true; } -export async function activateEmailProvider(provider: EmailChannel) { +// --------------------------------------------------------------------------- +// Public API: activate a specific mailbox config by id +// --------------------------------------------------------------------------- + +export async function activateEmailProvider(id: number) { const adminContext = getAdminContext(); const { prisma } = adminContext; const adminId = Number(adminContext.session?.user?.id); - await prisma.emailConfig.updateMany({ - where: { provider: { not: provider } }, - data: { isEnabled: false }, - }); - - const record = await prisma.emailConfig.findUnique({ where: { provider } }); - if (record) { - await prisma.emailConfig.update({ - where: { provider }, - data: { isEnabled: true }, - }); - } else { - throw badRequestError(`配置 ${provider} 不存在,请先保存配置`, "EMAIL_CONFIG_NOT_FOUND"); + const record = await getEmailConfigRecordById(prisma, id); + if (!record) { + throw badRequestError("邮局配置不存在", "EMAIL_CONFIG_NOT_FOUND"); } + await activateEmailConfigById(prisma, id); + await logAdminOperation( { action: "ACTIVATE_EMAIL_PROVIDER", targetType: "EmailConfig", - targetId: provider, - detail: `activated`, + targetId: String(id), + detail: `activated ${record.name} (${record.provider})`, }, { prisma, @@ -456,16 +488,14 @@ export async function activateEmailProvider(provider: EmailChannel) { return true; } -export async function saveEmailConfig(input: EmailConfigValue) { - const adminContext = getAdminContext(); - const { prisma } = adminContext; - const adminId = Number(adminContext.session?.user?.id); - const validated = validateEmailConfigInput(input); +// --------------------------------------------------------------------------- +// Public API: save (create or update) an email config +// --------------------------------------------------------------------------- - let config: Record; - if (validated.provider === "API") { +function buildConfigJson(input: EmailConfigValue): Record { + if (input.provider === "API") { const apiInput = input as EmailApiConfigValue; - config = { + return { apiProvider: apiInput.apiProvider, fromEmail: apiInput.fromEmail.trim(), fromName: apiInput.fromName?.trim() || "", @@ -481,9 +511,11 @@ export async function saveEmailConfig(input: EmailConfigValue) { adminSendDeliverySuccessEmail: Boolean(apiInput.adminSendDeliverySuccessEmail), adminSendDeliveryFailedEmail: Boolean(apiInput.adminSendDeliveryFailedEmail), }; - } else if (validated.provider === "SMTP") { + } + + if (input.provider === "SMTP") { const smtpInput = input as EmailSmtpConfigValue; - config = { + return { fromEmail: smtpInput.fromEmail.trim(), fromName: smtpInput.fromName?.trim() || "", replyTo: smtpInput.replyTo?.trim() || "", @@ -499,40 +531,90 @@ export async function saveEmailConfig(input: EmailConfigValue) { adminSendDeliverySuccessEmail: Boolean(smtpInput.adminSendDeliverySuccessEmail), adminSendDeliveryFailedEmail: Boolean(smtpInput.adminSendDeliveryFailedEmail), }; - } else { - const cloudflareInput = input as EmailCloudflareConfigValue; - config = { - fromEmail: cloudflareInput.fromEmail.trim(), - fromName: cloudflareInput.fromName?.trim() || "", - replyTo: cloudflareInput.replyTo?.trim() || "", - cloudflareBindingName: cloudflareInput.cloudflareBindingName?.trim() || "", - cloudflareDestinationAddress: cloudflareInput.cloudflareDestinationAddress?.trim() || "", - cloudflareAllowedDestinationAddresses: cloudflareInput.cloudflareAllowedDestinationAddresses ?? [], - customerSendOrderPaidEmail: Boolean(cloudflareInput.customerSendOrderPaidEmail), - customerSendDeliverySuccessEmail: Boolean(cloudflareInput.customerSendDeliverySuccessEmail), - customerSendDeliveryFailedEmail: Boolean(cloudflareInput.customerSendDeliveryFailedEmail), - adminSendOrderPaidEmail: Boolean(cloudflareInput.adminSendOrderPaidEmail), - adminSendDeliverySuccessEmail: Boolean(cloudflareInput.adminSendDeliverySuccessEmail), - adminSendDeliveryFailedEmail: Boolean(cloudflareInput.adminSendDeliveryFailedEmail), - }; } - const existingRecord = await prisma.emailConfig.findUnique({ - where: { provider: validated.provider }, - }); + const cloudflareInput = input as EmailCloudflareConfigValue; + return { + fromEmail: cloudflareInput.fromEmail.trim(), + fromName: cloudflareInput.fromName?.trim() || "", + replyTo: cloudflareInput.replyTo?.trim() || "", + cloudflareBindingName: cloudflareInput.cloudflareBindingName?.trim() || "", + cloudflareDestinationAddress: cloudflareInput.cloudflareDestinationAddress?.trim() || "", + cloudflareAllowedDestinationAddresses: cloudflareInput.cloudflareAllowedDestinationAddresses ?? [], + customerSendOrderPaidEmail: Boolean(cloudflareInput.customerSendOrderPaidEmail), + customerSendDeliverySuccessEmail: Boolean(cloudflareInput.customerSendDeliverySuccessEmail), + customerSendDeliveryFailedEmail: Boolean(cloudflareInput.customerSendDeliveryFailedEmail), + adminSendOrderPaidEmail: Boolean(cloudflareInput.adminSendOrderPaidEmail), + adminSendDeliverySuccessEmail: Boolean(cloudflareInput.adminSendDeliverySuccessEmail), + adminSendDeliveryFailedEmail: Boolean(cloudflareInput.adminSendDeliveryFailedEmail), + }; +} - const record = await upsertEmailConfigRecord(prisma, validated.provider, { - name: getChannelDisplayName(validated.provider), - isEnabled: existingRecord?.isEnabled ?? false, - configJson: JSON.stringify(config), +function getDefaultName(input: EmailConfigValue): string { + if (input.provider === "API") { + const apiInput = input as EmailApiConfigValue; + return `${apiInput.apiProvider} - ${apiInput.fromEmail || "未配置"}`; + } + if (input.provider === "SMTP") { + const smtpInput = input as EmailSmtpConfigValue; + return `SMTP - ${smtpInput.smtpHost || smtpInput.fromEmail || "未配置"}`; + } + const cfInput = input as EmailCloudflareConfigValue; + return `CloudFlare - ${cfInput.fromEmail || "未配置"}`; +} + +export async function saveEmailConfig(input: EmailConfigValue) { + const adminContext = getAdminContext(); + const { prisma } = adminContext; + const adminId = Number(adminContext.session?.user?.id); + const validated = validateEmailConfigInput(input); + + const configJson = buildConfigJson({ ...input, provider: validated.provider } as EmailConfigValue); + const name = input.name?.trim() || getDefaultName(input); + + // If input has an id, update existing record; otherwise create new + if (input.id && input.id > 0) { + const existingRecord = await getEmailConfigRecordById(prisma, input.id); + if (!existingRecord) { + throw badRequestError("邮局配置不存在", "EMAIL_CONFIG_NOT_FOUND"); + } + + const record = await updateEmailConfigRecord(prisma, input.id, { + provider: validated.provider as EmailChannel, + name, + configJson: JSON.stringify(configJson), + }); + + await logAdminOperation( + { + action: "SAVE_EMAIL_CONFIG", + targetType: "EmailConfig", + targetId: String(input.id), + detail: `updated: ${name}`, + }, + { + prisma, + adminId, + }, + ); + + return normalizeEmailConfigFromRecord(record); + } + + // Create new + const record = await createEmailConfigRecord(prisma, { + provider: validated.provider as EmailChannel, + name, + isEnabled: false, + configJson: JSON.stringify(configJson), }); await logAdminOperation( { - action: "SAVE_EMAIL_CONFIG", + action: "CREATE_EMAIL_CONFIG", targetType: "EmailConfig", - targetId: input.provider, - detail: `updated`, + targetId: String(record.id), + detail: `created: ${name}`, }, { prisma, @@ -540,9 +622,49 @@ export async function saveEmailConfig(input: EmailConfigValue) { }, ); - return normalizeEmailConfig(record, validated.provider); + return normalizeEmailConfigFromRecord(record); } +// --------------------------------------------------------------------------- +// Public API: delete an email config +// --------------------------------------------------------------------------- + +export async function deleteEmailConfig(id: number) { + const adminContext = getAdminContext(); + const { prisma } = adminContext; + const adminId = Number(adminContext.session?.user?.id); + + const record = await getEmailConfigRecordById(prisma, id); + if (!record) { + throw badRequestError("邮局配置不存在", "EMAIL_CONFIG_NOT_FOUND"); + } + + if (record.isEnabled) { + throw badRequestError("不能删除当前激活的邮局配置,请先激活其他配置", "EMAIL_CONFIG_IS_ACTIVE"); + } + + await deleteEmailConfigRecord(prisma, id); + + await logAdminOperation( + { + action: "DELETE_EMAIL_CONFIG", + targetType: "EmailConfig", + targetId: String(id), + detail: `deleted: ${record.name}`, + }, + { + prisma, + adminId, + }, + ); + + return true; +} + +// --------------------------------------------------------------------------- +// Public API: save template +// --------------------------------------------------------------------------- + export async function saveEmailTemplate(input: EmailTemplateValue) { const adminContext = getAdminContext(); const { prisma } = adminContext; @@ -576,25 +698,41 @@ export async function saveEmailTemplate(input: EmailTemplateValue) { return normalizeEmailTemplate(record, validated.scene as EmailScene); } +// --------------------------------------------------------------------------- +// Public API: send test email +// --------------------------------------------------------------------------- + export async function sendTestEmail(input: { toEmail: string; customContent?: string; + configId?: number; }) { const adminContext = getAdminContext(); const { prisma } = adminContext; const adminId = Number(adminContext.session?.user?.id); const validated = validateTestEmailInput(input); - const baseValues = await getEmailBaseValues(prisma); + const site = await getSiteSetting(prisma); + + // If a specific configId is provided, load that config directly instead of using the active one + let targetConfig: EmailConfigValue | undefined; + if (input.configId && input.configId > 0) { + const record = await getEmailConfigRecordById(prisma, input.configId); + if (!record) { + throw badRequestError("指定的邮局配置不存在", "EMAIL_CONFIG_NOT_FOUND"); + } + targetConfig = normalizeEmailConfigFromRecord(record); + } const result = await sendSceneEmail(prisma, { scene: "TEST", toEmail: validated.toEmail, values: { - siteName: baseValues.siteName, + siteName: site.siteName, sentAt: new Date().toLocaleString("zh-CN"), customContent: validated.customContent, }, triggeredBy: `admin:${adminId || 0}`, + config: targetConfig, }); await logAdminOperation( @@ -612,6 +750,29 @@ export async function sendTestEmail(input: { return result; } +// --------------------------------------------------------------------------- +// Public API: clear email logs +// --------------------------------------------------------------------------- + +export async function clearEmailLogs() { + const adminContext = getAdminContext(); + const { prisma } = adminContext; + const adminId = Number(adminContext.session?.user?.id); + + const { count } = await prisma.emailLog.deleteMany({}); + + await logAdminOperation( + { action: "CLEAR_EMAIL_LOGS", targetType: "EmailLog", detail: `deleted ${count} records` }, + { prisma, adminId }, + ); + + return { count }; +} + +// --------------------------------------------------------------------------- +// Public API: notification hooks (called from order/delivery flows) +// --------------------------------------------------------------------------- + export async function notifyOrderPaid(input: { prisma?: PrismaClient; orderId: number; @@ -786,4 +947,4 @@ export async function notifyDeliveryFailed(input: { await Promise.all(tasks); return { processed: true }; -} +} \ No newline at end of file diff --git a/modules/email/types.ts b/modules/email/types.ts index 347e4a4..0597e95 100644 --- a/modules/email/types.ts +++ b/modules/email/types.ts @@ -14,6 +14,8 @@ export interface EmailPushFlags { } export interface EmailApiConfigValue extends EmailPushFlags { + id?: number; + name?: string; provider: "API"; isEnabled: boolean; apiProvider: EmailApiProvider; @@ -27,6 +29,8 @@ export interface EmailApiConfigValue extends EmailPushFlags { } export interface EmailSmtpConfigValue extends EmailPushFlags { + id?: number; + name?: string; provider: "SMTP"; isEnabled: boolean; fromEmail: string; @@ -40,6 +44,8 @@ export interface EmailSmtpConfigValue extends EmailPushFlags { } export interface EmailCloudflareConfigValue extends EmailPushFlags { + id?: number; + name?: string; provider: "CLOUDFLARE"; isEnabled: boolean; fromEmail: string; diff --git a/pages/admin/email/+Page.vue b/pages/admin/email/+Page.vue index 40de68a..8fb0018 100644 --- a/pages/admin/email/+Page.vue +++ b/pages/admin/email/+Page.vue @@ -5,16 +5,16 @@

邮件管理

配置邮件发送通道、推送开关、日志列表和模板。

- +
@@ -26,6 +26,7 @@
+
@@ -40,39 +41,39 @@

发给客户

- +

发给管理员 (需在个人资料配置邮箱)

- @@ -82,96 +83,30 @@
- -
+ +
-

邮局配置

-

配置对应的发送通道参数,保存后可在下方列表中测试和激活。

+

邮局列表

+

支持添加多个邮局配置,可自由选择激活其中一个用于发信。

-
- - - -
- - -
- - - - - - - - -
- - -
-
- - - - - - - -
- -
- - -
-
- - - - - -
- -
- -
- - - {{ channelMessages[activeProviderFormType] }} -
-
-
- -
-
-

邮局列表

-
+
+ 暂无邮局配置,点击上方"新增邮局"按钮添加 +
+ +
+ + @@ -180,29 +115,41 @@ - - - + + + + + @@ -214,10 +161,158 @@ - - + + + + + + + + + + @@ -245,8 +338,46 @@ + + + + + + + + + + + + + + +
+
+ 共 {{ logList.length }} 条记录 + +
ID名称 类型 发件邮箱 服务商/地址
{{ getChannelLabel(channel) }}{{ getChannelForm(channel as any).fromEmail || '-' }}
{{ item.id }}{{ item.name || '-' }} - {{ getChannelForm(channel).apiProvider || '-' }} - {{ getChannelForm(channel).smtpHost || '-' }} - {{ getChannelForm(channel).cloudflareBindingName || '-' }} + {{ getChannelLabel(item.provider) }} + {{ item.fromEmail || '-' }} + {{ (item as any).apiProvider || '-' }} + {{ (item as any).smtpHost || '-' }} + {{ (item as any).cloudflareBindingName || '-' }} - - {{ getChannelForm(channel as any).isEnabled ? '已激活' : '未激活' }} + + {{ item.isEnabled ? '已激活' : '未激活' }}
- - - + + +
@@ -264,8 +395,8 @@ - - + + @@ -287,6 +418,7 @@ +
@@ -344,58 +476,235 @@ import { normalizeTelefuncError } from "../../../lib/app-error"; import { reactive, ref, computed } from "vue"; import { useData } from "vike-vue/useData"; -import { onSaveEmailConfig, onSaveEmailPushSettings, onActivateEmailProvider } from "./saveEmailConfig.telefunc"; +import { onSaveEmailConfig, onDeleteEmailConfig, onSaveEmailPushSettings, onActivateEmailProvider, onClearEmailLogs } from "./saveEmailConfig.telefunc"; import { onSaveEmailTemplate } from "./saveEmailTemplate.telefunc"; import { onSendTestEmail } from "./sendTestEmail.telefunc"; import type { Data } from "./+data"; -const { configs, templates, logs, metrics } = useData(); +type MailboxItem = { + id?: number; + name?: string; + provider: "API" | "SMTP" | "CLOUDFLARE"; + isEnabled: boolean; + fromEmail: string; + fromName?: string; + replyTo?: string; + // API fields + apiProvider?: string; + apiBaseUrl?: string; + apiKey?: string; + secretKey?: string; + timeoutMs?: number; + // SMTP fields + smtpHost?: string; + smtpPort?: number; + smtpSecure?: boolean; + smtpUsername?: string; + smtpPassword?: string; + // Cloudflare fields + cloudflareBindingName?: string; + cloudflareDestinationAddress?: string; + cloudflareAllowedDestinationAddresses?: string[]; + // push flags + customerSendOrderPaidEmail: boolean; + customerSendDeliverySuccessEmail: boolean; + customerSendDeliveryFailedEmail: boolean; + adminSendOrderPaidEmail: boolean; + adminSendDeliverySuccessEmail: boolean; + adminSendDeliveryFailedEmail: boolean; +}; + +const { configs, templates, logs: initialLogs, metrics, pushSettings: initialPushSettings } = useData(); const activeTab = ref<"stats" | "config" | "list" | "template">("stats"); -const apiForm = reactive({ ...(configs?.API as any) }); -const smtpForm = reactive({ ...(configs?.SMTP as any) }); -const cloudflareForm = reactive({ ...(configs?.CLOUDFLARE as any) }); -const templateList = reactive(templates.map((item) => ({ ...item }))); +// ===================== Mailbox list ===================== +const logList = reactive([...initialLogs]); -const activeTemplateScene = ref<"TEST" | "ORDER_PAID" | "DELIVERY_SUCCESS" | "DELIVERY_FAILED">(templateList[0]?.scene || "TEST"); -const activeTemplate = computed(() => { - return templateList.find((t) => t.scene === activeTemplateScene.value) || templateList[0]; -}); - -const activeProviderFormType = ref<"API" | "SMTP" | "CLOUDFLARE">("API"); +const mailboxList = reactive( + Array.isArray(configs) ? configs.map((c: any) => ({ ...c })) : [] +); +// ===================== Push settings ===================== const pushSettings = reactive({ - customerSendOrderPaidEmail: configs?.API?.customerSendOrderPaidEmail ?? configs?.SMTP?.customerSendOrderPaidEmail ?? configs?.CLOUDFLARE?.customerSendOrderPaidEmail ?? false, - customerSendDeliverySuccessEmail: configs?.API?.customerSendDeliverySuccessEmail ?? false, - customerSendDeliveryFailedEmail: configs?.API?.customerSendDeliveryFailedEmail ?? false, - adminSendOrderPaidEmail: configs?.API?.adminSendOrderPaidEmail ?? configs?.SMTP?.adminSendOrderPaidEmail ?? configs?.CLOUDFLARE?.adminSendOrderPaidEmail ?? false, - adminSendDeliverySuccessEmail: configs?.API?.adminSendDeliverySuccessEmail ?? false, - adminSendDeliveryFailedEmail: configs?.API?.adminSendDeliveryFailedEmail ?? false, + customerSendOrderPaidEmail: (initialPushSettings as any)?.customerSendOrderPaidEmail ?? false, + customerSendDeliverySuccessEmail: (initialPushSettings as any)?.customerSendDeliverySuccessEmail ?? false, + customerSendDeliveryFailedEmail: (initialPushSettings as any)?.customerSendDeliveryFailedEmail ?? false, + adminSendOrderPaidEmail: (initialPushSettings as any)?.adminSendOrderPaidEmail ?? false, + adminSendDeliverySuccessEmail: (initialPushSettings as any)?.adminSendDeliverySuccessEmail ?? false, + adminSendDeliveryFailedEmail: (initialPushSettings as any)?.adminSendDeliveryFailedEmail ?? false, }); const savingPushSettings = ref(false); const pushSettingsMessage = ref(""); const pushSettingsError = ref(false); -const testToEmail = ref(""); -const testContent = ref("嘿!API 跑通了\n\n这是从 Cloudflare 发出的第一封邮件。"); -const cloudflareAllowedText = ref(Array.isArray((cloudflareForm as any).cloudflareAllowedDestinationAddresses) ? (cloudflareForm as any).cloudflareAllowedDestinationAddresses.join("\n") : ""); - -const savingChannel = ref<"API" | "SMTP" | "CLOUDFLARE" | "">(""); -const testingChannel = ref<"API" | "SMTP" | "CLOUDFLARE" | "">(""); -const channelMessages = reactive>({ API: "", SMTP: "", CLOUDFLARE: "" }); -const channelErrors = reactive>({ API: false, SMTP: false, CLOUDFLARE: false }); - +// ===================== Template ===================== +const templateList = reactive(templates.map((item: any) => ({ ...item }))); +const activeTemplateScene = ref<"TEST" | "ORDER_PAID" | "DELIVERY_SUCCESS" | "DELIVERY_FAILED">(templateList[0]?.scene || "TEST"); +const activeTemplate = computed(() => { + return templateList.find((t: any) => t.scene === activeTemplateScene.value) || templateList[0]; +}); const savingTemplate = ref<"TEST" | "ORDER_PAID" | "DELIVERY_SUCCESS" | "DELIVERY_FAILED" | "">(""); const templateMessages = reactive>({ TEST: "", ORDER_PAID: "", DELIVERY_SUCCESS: "", DELIVERY_FAILED: "" }); const templateErrors = reactive>({ TEST: false, ORDER_PAID: false, DELIVERY_SUCCESS: false, DELIVERY_FAILED: false }); +// ===================== Config dialog ===================== +const showConfigDialog = ref(false); +const editingId = ref(null); +const savingConfig = ref(false); +const configDialogMessage = ref(""); +const configDialogError = ref(false); + +interface ConfigFormState { + name: string; + provider: "API" | "SMTP" | "CLOUDFLARE"; + fromEmail: string; + fromName: string; + replyTo: string; + // API + apiProvider: "BREVO" | "MAILJET"; + apiBaseUrl: string; + apiKey: string; + secretKey: string; + timeoutMs: number; + // SMTP + smtpHost: string; + smtpPort: number; + smtpSecure: boolean; + smtpUsername: string; + smtpPassword: string; + // Cloudflare + cloudflareBindingName: string; + cloudflareDestinationAddress: string; + cloudflareAllowedText: string; +} + +function createEmptyForm(): ConfigFormState { + return { + name: "", + provider: "API", + fromEmail: "", + fromName: "", + replyTo: "", + apiProvider: "BREVO", + apiBaseUrl: "https://api.brevo.com/v3/smtp/email", + apiKey: "", + secretKey: "", + timeoutMs: 10000, + smtpHost: "", + smtpPort: 587, + smtpSecure: false, + smtpUsername: "", + smtpPassword: "", + cloudflareBindingName: "", + cloudflareDestinationAddress: "", + cloudflareAllowedText: "", + }; +} + +const configForm = reactive(createEmptyForm()); + +function openCreateDialog() { + editingId.value = null; + Object.assign(configForm, createEmptyForm()); + configDialogMessage.value = ""; + configDialogError.value = false; + showConfigDialog.value = true; +} + +function openEditDialog(item: MailboxItem) { + editingId.value = item.id ?? null; + Object.assign(configForm, { + name: item.name || "", + provider: item.provider, + fromEmail: item.fromEmail || "", + fromName: item.fromName || "", + replyTo: item.replyTo || "", + apiProvider: (item as any).apiProvider || "BREVO", + apiBaseUrl: (item as any).apiBaseUrl || "", + apiKey: (item as any).apiKey || "", + secretKey: (item as any).secretKey || "", + timeoutMs: (item as any).timeoutMs || 10000, + smtpHost: (item as any).smtpHost || "", + smtpPort: (item as any).smtpPort || 587, + smtpSecure: (item as any).smtpSecure || false, + smtpUsername: (item as any).smtpUsername || "", + smtpPassword: (item as any).smtpPassword || "", + cloudflareBindingName: (item as any).cloudflareBindingName || "", + cloudflareDestinationAddress: (item as any).cloudflareDestinationAddress || "", + cloudflareAllowedText: Array.isArray((item as any).cloudflareAllowedDestinationAddresses) + ? (item as any).cloudflareAllowedDestinationAddresses.join("\n") + : "", + }); + configDialogMessage.value = ""; + configDialogError.value = false; + showConfigDialog.value = true; +} + +function closeConfigDialog() { + showConfigDialog.value = false; +} + +// ===================== Test modal ===================== const showTestModal = ref(false); +const testingMailboxId = ref(null); +const testingMailboxName = ref(""); +const testToEmail = ref(""); +const testContent = ref("嘿!API 跑通了\n\n这是一封测试邮件。"); const isTesting = ref(false); const testModalMessage = ref(""); const testModalError = ref(false); +function openTestModal(item: MailboxItem) { + testingMailboxId.value = item.id ?? null; + testingMailboxName.value = item.name || getChannelLabel(item.provider); + testModalMessage.value = ""; + testModalError.value = false; + showTestModal.value = true; +} + +function closeTestModal() { + showTestModal.value = false; +} + +// ===================== Delete confirm ===================== +const showDeleteConfirm = ref(false); +const deletingId = ref(null); +const deletingMailboxName = ref(""); +const deleting = ref(false); +const deleteMessage = ref(""); + +function handleDelete(item: MailboxItem) { + if (item.isEnabled) return; + deletingId.value = item.id ?? null; + deletingMailboxName.value = item.name || getChannelLabel(item.provider); + deleteMessage.value = ""; + showDeleteConfirm.value = true; +} + +function closeDeleteConfirm() { + showDeleteConfirm.value = false; +} + +// ===================== Clear logs ===================== +const showClearConfirm = ref(false); +const clearingLogs = ref(false); + +async function handleClearLogs() { + clearingLogs.value = true; + try { + await onClearEmailLogs(); + logList.splice(0); + showClearConfirm.value = false; + } catch (error) { + alert(normalizeTelefuncError(error, "清除失败")); + } finally { + clearingLogs.value = false; + } +} + +// ===================== Helpers ===================== function formatDate(value: string) { return new Date(value).toLocaleString("zh-CN"); } @@ -408,40 +717,13 @@ function getChannelLabel(provider: string) { return ({ API: "API", SMTP: "SMTP", CLOUDFLARE: "CloudFlare" } as Record)[provider] || provider; } -function getChannelForm(channel: "API" | "SMTP" | "CLOUDFLARE") { - if (channel === "API") return apiForm; - if (channel === "SMTP") return smtpForm; - return cloudflareForm; -} - +// ===================== Actions ===================== async function handleSavePushSettings() { savingPushSettings.value = true; pushSettingsMessage.value = ""; pushSettingsError.value = false; try { await onSaveEmailPushSettings({ ...pushSettings }); - // 同步到本地表单状态,避免下次保存邮局时覆盖 - apiForm.customerSendOrderPaidEmail = pushSettings.customerSendOrderPaidEmail; - apiForm.customerSendDeliverySuccessEmail = pushSettings.customerSendDeliverySuccessEmail; - apiForm.customerSendDeliveryFailedEmail = pushSettings.customerSendDeliveryFailedEmail; - apiForm.adminSendOrderPaidEmail = pushSettings.adminSendOrderPaidEmail; - apiForm.adminSendDeliverySuccessEmail = pushSettings.adminSendDeliverySuccessEmail; - apiForm.adminSendDeliveryFailedEmail = pushSettings.adminSendDeliveryFailedEmail; - - smtpForm.customerSendOrderPaidEmail = pushSettings.customerSendOrderPaidEmail; - smtpForm.customerSendDeliverySuccessEmail = pushSettings.customerSendDeliverySuccessEmail; - smtpForm.customerSendDeliveryFailedEmail = pushSettings.customerSendDeliveryFailedEmail; - smtpForm.adminSendOrderPaidEmail = pushSettings.adminSendOrderPaidEmail; - smtpForm.adminSendDeliverySuccessEmail = pushSettings.adminSendDeliverySuccessEmail; - smtpForm.adminSendDeliveryFailedEmail = pushSettings.adminSendDeliveryFailedEmail; - - cloudflareForm.customerSendOrderPaidEmail = pushSettings.customerSendOrderPaidEmail; - cloudflareForm.customerSendDeliverySuccessEmail = pushSettings.customerSendDeliverySuccessEmail; - cloudflareForm.customerSendDeliveryFailedEmail = pushSettings.customerSendDeliveryFailedEmail; - cloudflareForm.adminSendOrderPaidEmail = pushSettings.adminSendOrderPaidEmail; - cloudflareForm.adminSendDeliverySuccessEmail = pushSettings.adminSendDeliverySuccessEmail; - cloudflareForm.adminSendDeliveryFailedEmail = pushSettings.adminSendDeliveryFailedEmail; - pushSettingsMessage.value = "推送设置保存成功"; } catch (error) { pushSettingsError.value = true; @@ -451,76 +733,121 @@ async function handleSavePushSettings() { } } -async function handleSaveConfig(channel: "API" | "SMTP" | "CLOUDFLARE") { - savingChannel.value = channel; - channelMessages[channel] = ""; - channelErrors[channel] = false; +async function handleSaveConfig() { + savingConfig.value = true; + configDialogMessage.value = ""; + configDialogError.value = false; try { - const form = getChannelForm(channel) as any; - const payload = { - ...form, - provider: channel, - // 使用当前最新的推送设置 + const payload: Record = { + provider: configForm.provider, + name: configForm.name, + fromEmail: configForm.fromEmail, + fromName: configForm.fromName, + replyTo: configForm.replyTo, + // always carry current push settings customerSendOrderPaidEmail: pushSettings.customerSendOrderPaidEmail, customerSendDeliverySuccessEmail: pushSettings.customerSendDeliverySuccessEmail, customerSendDeliveryFailedEmail: pushSettings.customerSendDeliveryFailedEmail, adminSendOrderPaidEmail: pushSettings.adminSendOrderPaidEmail, adminSendDeliverySuccessEmail: pushSettings.adminSendDeliverySuccessEmail, adminSendDeliveryFailedEmail: pushSettings.adminSendDeliveryFailedEmail, - cloudflareAllowedDestinationAddresses: channel === "CLOUDFLARE" ? cloudflareAllowedText.value.split(/\r?\n/).map((item) => item.trim()).filter(Boolean) : undefined, }; - const result = await onSaveEmailConfig(payload); - Object.assign(form, result); - if (channel === "CLOUDFLARE") { - cloudflareAllowedText.value = Array.isArray((result as any).cloudflareAllowedDestinationAddresses) - ? (result as any).cloudflareAllowedDestinationAddresses.join("\n") - : ""; + + if (editingId.value) { + payload.id = editingId.value; } - channelMessages[channel] = "保存成功"; + + if (configForm.provider === "API") { + payload.apiProvider = configForm.apiProvider; + payload.apiBaseUrl = configForm.apiBaseUrl; + payload.apiKey = configForm.apiKey; + payload.secretKey = configForm.secretKey; + payload.timeoutMs = configForm.timeoutMs; + } else if (configForm.provider === "SMTP") { + payload.smtpHost = configForm.smtpHost; + payload.smtpPort = configForm.smtpPort; + payload.smtpSecure = configForm.smtpSecure; + payload.smtpUsername = configForm.smtpUsername; + payload.smtpPassword = configForm.smtpPassword; + } else { + payload.cloudflareBindingName = configForm.cloudflareBindingName; + payload.cloudflareDestinationAddress = configForm.cloudflareDestinationAddress; + payload.cloudflareAllowedDestinationAddresses = configForm.cloudflareAllowedText + .split(/\r?\n/) + .map((s) => s.trim()) + .filter(Boolean); + } + + const result = await onSaveEmailConfig(payload) as any; + + if (editingId.value) { + // Update existing entry in list + const idx = mailboxList.findIndex((m) => m.id === editingId.value); + if (idx >= 0) { + Object.assign(mailboxList[idx], result); + } + } else { + // Add to list + mailboxList.push({ ...result }); + } + + configDialogMessage.value = editingId.value ? "更新成功" : "创建成功"; + // Close dialog after short delay on success + setTimeout(() => { + showConfigDialog.value = false; + }, 600); } catch (error) { - channelErrors[channel] = true; - channelMessages[channel] = normalizeTelefuncError(error, "保存失败"); + configDialogError.value = true; + configDialogMessage.value = normalizeTelefuncError(error, "保存失败"); } finally { - savingChannel.value = ""; + savingConfig.value = false; } } -function editProvider(channel: "API" | "SMTP" | "CLOUDFLARE") { - activeProviderFormType.value = channel; - document.getElementById("provider-form")?.scrollIntoView({ behavior: "smooth" }); -} - -async function handleActivateProvider(channel: "API" | "SMTP" | "CLOUDFLARE") { +async function handleActivate(item: MailboxItem) { + if (!item.id || item.isEnabled) return; try { - await onActivateEmailProvider(channel); - apiForm.isEnabled = channel === "API"; - smtpForm.isEnabled = channel === "SMTP"; - cloudflareForm.isEnabled = channel === "CLOUDFLARE"; + await onActivateEmailProvider(item.id); + // Update all items in the list + for (const m of mailboxList) { + m.isEnabled = m.id === item.id; + } } catch (error) { alert(normalizeTelefuncError(error, "激活失败")); } } -function openTestModal(channel: "API" | "SMTP" | "CLOUDFLARE") { - testingChannel.value = channel; - showTestModal.value = true; - testModalMessage.value = ""; - testModalError.value = false; +async function confirmDelete() { + if (!deletingId.value) return; + deleting.value = true; + deleteMessage.value = ""; + try { + await onDeleteEmailConfig(deletingId.value); + const idx = mailboxList.findIndex((m) => m.id === deletingId.value); + if (idx >= 0) { + mailboxList.splice(idx, 1); + } + showDeleteConfirm.value = false; + } catch (error) { + deleteMessage.value = normalizeTelefuncError(error, "删除失败"); + } finally { + deleting.value = false; + } } -function closeTestModal() { - showTestModal.value = false; -} - -async function handleSendTest(channel: "API" | "SMTP" | "CLOUDFLARE") { +async function handleSendTest() { + if (!testingMailboxId.value) return; isTesting.value = true; testModalMessage.value = ""; testModalError.value = false; try { - await handleSaveConfig(channel); - await onSendTestEmail({ toEmail: testToEmail.value, customContent: testContent.value }); + await onSendTestEmail({ + toEmail: testToEmail.value, + customContent: testContent.value, + configId: testingMailboxId.value, + }); testModalMessage.value = "测试邮件发送成功"; } catch (error) { testModalError.value = true; @@ -536,7 +863,7 @@ async function handleSaveTemplate(scene: "TEST" | "ORDER_PAID" | "DELIVERY_SUCCE templateErrors[scene] = false; try { - const target = templateList.find((item) => item.scene === scene); + const target = templateList.find((item: any) => item.scene === scene); if (!target) return; const result = await onSaveEmailTemplate({ ...target }); Object.assign(target, result); @@ -548,4 +875,4 @@ async function handleSaveTemplate(scene: "TEST" | "ORDER_PAID" | "DELIVERY_SUCCE savingTemplate.value = ""; } } - + \ No newline at end of file diff --git a/pages/admin/email/+data.ts b/pages/admin/email/+data.ts index 744cae4..716b192 100644 --- a/pages/admin/email/+data.ts +++ b/pages/admin/email/+data.ts @@ -1,6 +1,6 @@ import { getEmailManagementData } from "../../../modules/email/service"; -export type Data = ReturnType; +export type Data = Awaited>; export async function data(pageContext: { prisma: import("../../../generated/prisma/client").PrismaClient; @@ -8,10 +8,18 @@ export async function data(pageContext: { }) { if (pageContext.session?.user?.role !== "admin") { return { - configs: null, + configs: [], templates: [], logs: [], metrics: [], + pushSettings: { + customerSendOrderPaidEmail: false, + customerSendDeliverySuccessEmail: false, + customerSendDeliveryFailedEmail: false, + adminSendOrderPaidEmail: false, + adminSendDeliverySuccessEmail: false, + adminSendDeliveryFailedEmail: false, + }, }; } diff --git a/pages/admin/email/saveEmailConfig.telefunc.ts b/pages/admin/email/saveEmailConfig.telefunc.ts index a2d3961..675e64e 100644 --- a/pages/admin/email/saveEmailConfig.telefunc.ts +++ b/pages/admin/email/saveEmailConfig.telefunc.ts @@ -1,11 +1,16 @@ import { assertAdminAccess } from "../../../modules/auth/service"; -import { activateEmailProvider, saveEmailConfig, saveEmailPushSettings } from "../../../modules/email/service"; +import { activateEmailProvider, clearEmailLogs, deleteEmailConfig, saveEmailConfig, saveEmailPushSettings } from "../../../modules/email/service"; export async function onSaveEmailConfig(input: Record) { assertAdminAccess(); return saveEmailConfig(input as any); } +export async function onDeleteEmailConfig(id: number) { + assertAdminAccess(); + return deleteEmailConfig(id); +} + export async function onSaveEmailPushSettings(input: { customerSendOrderPaidEmail: boolean; customerSendDeliverySuccessEmail: boolean; @@ -18,7 +23,12 @@ export async function onSaveEmailPushSettings(input: { return saveEmailPushSettings(input); } -export async function onActivateEmailProvider(provider: "API" | "SMTP" | "CLOUDFLARE") { +export async function onActivateEmailProvider(id: number) { assertAdminAccess(); - return activateEmailProvider(provider); + return activateEmailProvider(id); } + +export async function onClearEmailLogs() { + assertAdminAccess(); + return clearEmailLogs(); +} \ No newline at end of file diff --git a/pages/admin/email/sendTestEmail.telefunc.ts b/pages/admin/email/sendTestEmail.telefunc.ts index 73becf1..997bbec 100644 --- a/pages/admin/email/sendTestEmail.telefunc.ts +++ b/pages/admin/email/sendTestEmail.telefunc.ts @@ -4,6 +4,7 @@ import { sendTestEmail } from "../../../modules/email/service"; export async function onSendTestEmail(input: { toEmail: string; customContent?: string; + configId?: number; }) { assertAdminAccess(); return sendTestEmail(input); diff --git a/prisma/migrations/0001_init.sql b/prisma/migrations/0001_init.sql index b7c10c5..4d0a5bf 100644 --- a/prisma/migrations/0001_init.sql +++ b/prisma/migrations/0001_init.sql @@ -245,7 +245,7 @@ CREATE INDEX "PaymentLog_orderNo_idx" ON "PaymentLog"("orderNo"); CREATE INDEX "PaymentLog_orderId_idx" ON "PaymentLog"("orderId"); -- CreateIndex -CREATE UNIQUE INDEX "EmailConfig_provider_key" ON "EmailConfig"("provider"); +CREATE INDEX "EmailConfig_provider_idx" ON "EmailConfig"("provider"); -- CreateIndex CREATE UNIQUE INDEX "EmailTemplate_scene_key" ON "EmailTemplate"("scene"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml index e5e5c47..2a5a444 100644 --- a/prisma/migrations/migration_lock.toml +++ b/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "sqlite" \ No newline at end of file +# It should be added in your version-control system (e.g., Git) +provider = "sqlite" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1c07d4e..72ed4a8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -273,12 +273,14 @@ model PaymentLog { model EmailConfig { id Int @id @default(autoincrement()) - provider EmailChannel @unique + provider EmailChannel name String isEnabled Boolean @default(false) configJson String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + + @@index([provider]) } model EmailTemplate {
暂无邮件日志
暂无邮件日志
{{ index + 1 }} {{ formatDate(log.createdAt) }} {{ getChannelLabel(log.provider) }}