Your Name bc5838f9e1
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
新版首页文案可配置并部署侧边栏管理入口
2026-02-14 00:10:08 +08:00

561 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { db } from "@/lib/db";
import { siteSettings, systemSettings } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import { auth } from "@/app/(auth)/auth";
import {
defaultMembershipConfig,
defaultMembershipUserOverrides,
normalizeMembershipConfig,
normalizeMembershipUserOverrides,
} from "@/lib/membership/config";
const HOME_VARIANT_KEY = "home_variant";
const HOME_SURPRISE_CATEGORIES_KEY = "home_surprise_categories";
const LOVART_COMPOSER_PREVIEW_TEXTS_KEY = "lovart_composer_preview_texts";
const MEMBERSHIP_CONFIG_KEY = "membership_config";
const MEMBERSHIP_USER_OVERRIDES_KEY = "membership_user_plan_overrides";
function normalizeHomeVariant(value: unknown): "classic" | "lovart" {
return value === "lovart" ? "lovart" : "classic";
}
function normalizeSurpriseCategories(value: unknown): string[] {
if (!Array.isArray(value)) {
return ["全部", "惊喜"];
}
const normalized = value
.map((item) => String(item || "").trim())
.filter(Boolean)
.slice(0, 12);
if (normalized.length === 0) {
return ["全部", "惊喜"];
}
const unique = Array.from(new Set(normalized));
if (!unique.includes("全部")) {
unique.unshift("全部");
}
return unique;
}
function normalizeLovartComposerPreviewTexts(value: unknown): string[] {
const defaults = [
"让 Lovart 打造引人注目的社交媒体视觉",
"帮我把这张产品图做成高级电商海报",
"基于参考图延展 3 套同风格方案",
];
if (!Array.isArray(value)) {
return defaults;
}
const normalized = value
.map((item) => String(item || "").trim())
.filter(Boolean)
.slice(0, 20);
return normalized.length > 0 ? normalized : defaults;
}
// GET /api/site-settings - 获取网站设置 (公开)
export async function GET() {
try {
const [
settings,
notificationSetting,
homeVariantSetting,
surpriseCategoriesSetting,
lovartComposerPreviewTextsSetting,
membershipConfigSetting,
membershipOverridesSetting,
] = await Promise.all([
db.select().from(siteSettings).limit(1).then(rows => rows[0]),
db
.select()
.from(systemSettings)
.where(eq(systemSettings.key, "login_notification"))
.limit(1)
.then(rows => rows[0]),
db
.select()
.from(systemSettings)
.where(eq(systemSettings.key, HOME_VARIANT_KEY))
.limit(1)
.then(rows => rows[0]),
db
.select()
.from(systemSettings)
.where(eq(systemSettings.key, HOME_SURPRISE_CATEGORIES_KEY))
.limit(1)
.then(rows => rows[0]),
db
.select()
.from(systemSettings)
.where(eq(systemSettings.key, LOVART_COMPOSER_PREVIEW_TEXTS_KEY))
.limit(1)
.then(rows => rows[0]),
db
.select()
.from(systemSettings)
.where(eq(systemSettings.key, MEMBERSHIP_CONFIG_KEY))
.limit(1)
.then(rows => rows[0]),
db
.select()
.from(systemSettings)
.where(eq(systemSettings.key, MEMBERSHIP_USER_OVERRIDES_KEY))
.limit(1)
.then(rows => rows[0]),
]);
const notification = notificationSetting?.value || { enabled: false, title: "", content: "" };
const homeVariant = normalizeHomeVariant(homeVariantSetting?.value);
const surpriseCategories = normalizeSurpriseCategories(surpriseCategoriesSetting?.value);
const lovartComposerPreviewTexts = normalizeLovartComposerPreviewTexts(
lovartComposerPreviewTextsSetting?.value
);
const membershipConfig = normalizeMembershipConfig(membershipConfigSetting?.value);
const membershipUserOverrides = normalizeMembershipUserOverrides(
membershipOverridesSetting?.value
);
// 如果没有设置记录,返回默认值
const baseSettings = settings || {
siteName: "AI Chatbot",
siteLogo: null,
siteLogoDark: null,
favicon: null,
showAutoModel: true,
defaultTheme: "system",
usageTimeEnabled: false,
usageTimeStart: "08:00",
usageTimeEnd: "20:00",
usageTimeMessage: "由壹点累了也是需要休息的",
bannerEnabled: true,
bannerText: "升级会员, Nano Banana Pro 免费365天!",
bannerLink: null,
toolbox1Name: "工具箱",
toolbox1Enabled: true,
toolbox2Name: "工具箱2",
toolbox2Enabled: false,
tutorialVideoUrl: null,
tutorialEnabled: false,
rankingEnabled: true,
canvasEnabled: true,
canvasDefaultModelId: null,
canvasAutoModeDefaultModelId: null,
lovartPromoText: "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠",
lovartPromoActionText: "立即升级",
lovartHeroTitle: "由壹点AI 让设计更简单",
lovartHeroSubtitle: "懂你的设计代理,帮你搞定一切",
homeVariant,
surpriseCategories,
lovartComposerPreviewTexts,
membershipConfig,
membershipUserOverrides,
};
const response = NextResponse.json({
...baseSettings,
lovartPromoText:
baseSettings.lovartPromoText ||
"现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠",
lovartPromoActionText: baseSettings.lovartPromoActionText || "立即升级",
lovartHeroTitle: baseSettings.lovartHeroTitle || "由壹点AI 让设计更简单",
lovartHeroSubtitle: baseSettings.lovartHeroSubtitle || "懂你的设计代理,帮你搞定一切",
notification,
homeVariant,
surpriseCategories,
lovartComposerPreviewTexts,
membershipConfig,
membershipUserOverrides,
});
// 禁用缓存,确保总是获取最新设置
response.headers.set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
response.headers.set("Pragma", "no-cache");
response.headers.set("Expires", "0");
return response;
} catch (error) {
console.error("[Site Settings API] Get error:", error);
return NextResponse.json(
{ error: "Failed to fetch site settings" },
{ status: 500 }
);
}
}
const updateSchema = z.object({
siteName: z.string().min(1).max(128).optional(),
siteLogo: z.string().nullable().optional(),
siteLogoDark: z.string().nullable().optional(),
favicon: z.string().nullable().optional(),
showAutoModel: z.boolean().optional(),
defaultTheme: z.enum(["system", "light", "dark"]).optional(),
usageTimeEnabled: z.boolean().optional(),
usageTimeStart: z.string().length(5).optional(),
usageTimeEnd: z.string().length(5).optional(),
usageTimeMessage: z.string().optional(),
bannerEnabled: z.boolean().optional(),
bannerText: z.string().optional(),
bannerLink: z.string().nullable().optional(),
toolbox1Name: z.string().max(64).optional(),
toolbox1Enabled: z.boolean().optional(),
toolbox2Name: z.string().max(64).optional(),
toolbox2Enabled: z.boolean().optional(),
tutorialVideoUrl: z.string().nullable().optional(),
tutorialEnabled: z.boolean().optional(),
rankingEnabled: z.boolean().optional(),
canvasEnabled: z.boolean().optional(),
canvasDefaultModelId: z.string().nullable().optional(),
canvasAutoModeDefaultModelId: z.string().nullable().optional(),
lovartPromoText: z.string().max(300).optional(),
lovartPromoActionText: z.string().max(32).optional(),
lovartHeroTitle: z.string().max(300).optional(),
lovartHeroSubtitle: z.string().max(300).optional(),
homeVariant: z.enum(["classic", "lovart"]).optional(),
surpriseCategories: z.array(z.string().trim().min(1).max(20)).max(12).optional(),
lovartComposerPreviewTexts: z.array(z.string().trim().min(1).max(120)).max(20).optional(),
membershipConfig: z.unknown().optional(),
membershipUserOverrides: z.unknown().optional(),
});
// PUT /api/site-settings - 更新网站设置(仅管理员)
export async function PUT(request: NextRequest) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (session.user.type !== "admin") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const body = await request.json();
const data = updateSchema.parse(body);
const {
homeVariant,
surpriseCategories,
lovartComposerPreviewTexts,
membershipConfig,
membershipUserOverrides,
...siteSettingsPayload
} = data;
// 查找现有设置
const [existing] = await db.select().from(siteSettings).limit(1);
if (existing) {
// 更新现有设置
const [updated] = await db
.update(siteSettings)
.set({
...siteSettingsPayload,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.where(eq(siteSettings.id, existing.id))
.returning();
if (homeVariant !== undefined) {
await db
.insert(systemSettings)
.values({
key: HOME_VARIANT_KEY,
value: homeVariant,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: homeVariant,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
}
if (surpriseCategories !== undefined) {
const normalized = normalizeSurpriseCategories(surpriseCategories);
await db
.insert(systemSettings)
.values({
key: HOME_SURPRISE_CATEGORIES_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
}
if (lovartComposerPreviewTexts !== undefined) {
const normalized = normalizeLovartComposerPreviewTexts(lovartComposerPreviewTexts);
await db
.insert(systemSettings)
.values({
key: LOVART_COMPOSER_PREVIEW_TEXTS_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
}
if (membershipConfig !== undefined) {
const normalized = normalizeMembershipConfig(membershipConfig);
await db
.insert(systemSettings)
.values({
key: MEMBERSHIP_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
}
if (membershipUserOverrides !== undefined) {
const normalized = normalizeMembershipUserOverrides(membershipUserOverrides);
await db
.insert(systemSettings)
.values({
key: MEMBERSHIP_USER_OVERRIDES_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
}
return NextResponse.json({
...updated,
homeVariant: homeVariant ?? normalizeHomeVariant(undefined),
surpriseCategories:
surpriseCategories !== undefined
? normalizeSurpriseCategories(surpriseCategories)
: normalizeSurpriseCategories(undefined),
lovartComposerPreviewTexts:
lovartComposerPreviewTexts !== undefined
? normalizeLovartComposerPreviewTexts(lovartComposerPreviewTexts)
: normalizeLovartComposerPreviewTexts(undefined),
membershipConfig:
membershipConfig !== undefined
? normalizeMembershipConfig(membershipConfig)
: defaultMembershipConfig,
membershipUserOverrides:
membershipUserOverrides !== undefined
? normalizeMembershipUserOverrides(membershipUserOverrides)
: defaultMembershipUserOverrides,
});
} else {
// 创建新设置
const [created] = await db
.insert(siteSettings)
.values({
siteName: siteSettingsPayload.siteName || "AI Chatbot",
siteLogo: siteSettingsPayload.siteLogo,
siteLogoDark: siteSettingsPayload.siteLogoDark,
favicon: siteSettingsPayload.favicon,
showAutoModel: siteSettingsPayload.showAutoModel ?? true,
defaultTheme: siteSettingsPayload.defaultTheme ?? "system",
usageTimeEnabled: siteSettingsPayload.usageTimeEnabled ?? false,
usageTimeStart: siteSettingsPayload.usageTimeStart ?? "08:00",
usageTimeEnd: siteSettingsPayload.usageTimeEnd ?? "20:00",
usageTimeMessage: siteSettingsPayload.usageTimeMessage ?? "由壹点累了也是需要休息的",
bannerEnabled: siteSettingsPayload.bannerEnabled ?? true,
bannerText: siteSettingsPayload.bannerText ?? "升级会员, Nano Banana Pro 免费365天!",
bannerLink: siteSettingsPayload.bannerLink,
toolbox1Name: siteSettingsPayload.toolbox1Name ?? "工具箱",
toolbox1Enabled: siteSettingsPayload.toolbox1Enabled ?? true,
toolbox2Name: siteSettingsPayload.toolbox2Name ?? "工具箱2",
toolbox2Enabled: siteSettingsPayload.toolbox2Enabled ?? false,
tutorialVideoUrl: siteSettingsPayload.tutorialVideoUrl,
tutorialEnabled: siteSettingsPayload.tutorialEnabled ?? false,
rankingEnabled: siteSettingsPayload.rankingEnabled ?? true,
canvasEnabled: siteSettingsPayload.canvasEnabled ?? true,
canvasDefaultModelId: siteSettingsPayload.canvasDefaultModelId,
canvasAutoModeDefaultModelId: siteSettingsPayload.canvasAutoModeDefaultModelId,
lovartPromoText:
siteSettingsPayload.lovartPromoText ??
"现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠",
lovartPromoActionText: siteSettingsPayload.lovartPromoActionText ?? "立即升级",
lovartHeroTitle: siteSettingsPayload.lovartHeroTitle ?? "由壹点AI 让设计更简单",
lovartHeroSubtitle: siteSettingsPayload.lovartHeroSubtitle ?? "懂你的设计代理,帮你搞定一切",
updatedBy: session.user.id,
})
.returning();
if (homeVariant !== undefined) {
await db
.insert(systemSettings)
.values({
key: HOME_VARIANT_KEY,
value: homeVariant,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: homeVariant,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
}
if (surpriseCategories !== undefined) {
const normalized = normalizeSurpriseCategories(surpriseCategories);
await db
.insert(systemSettings)
.values({
key: HOME_SURPRISE_CATEGORIES_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
}
if (lovartComposerPreviewTexts !== undefined) {
const normalized = normalizeLovartComposerPreviewTexts(lovartComposerPreviewTexts);
await db
.insert(systemSettings)
.values({
key: LOVART_COMPOSER_PREVIEW_TEXTS_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
}
if (membershipConfig !== undefined) {
const normalized = normalizeMembershipConfig(membershipConfig);
await db
.insert(systemSettings)
.values({
key: MEMBERSHIP_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
}
if (membershipUserOverrides !== undefined) {
const normalized = normalizeMembershipUserOverrides(membershipUserOverrides);
await db
.insert(systemSettings)
.values({
key: MEMBERSHIP_USER_OVERRIDES_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
}
return NextResponse.json({
...created,
homeVariant: homeVariant ?? normalizeHomeVariant(undefined),
surpriseCategories:
surpriseCategories !== undefined
? normalizeSurpriseCategories(surpriseCategories)
: normalizeSurpriseCategories(undefined),
lovartComposerPreviewTexts:
lovartComposerPreviewTexts !== undefined
? normalizeLovartComposerPreviewTexts(lovartComposerPreviewTexts)
: normalizeLovartComposerPreviewTexts(undefined),
membershipConfig:
membershipConfig !== undefined
? normalizeMembershipConfig(membershipConfig)
: defaultMembershipConfig,
membershipUserOverrides:
membershipUserOverrides !== undefined
? normalizeMembershipUserOverrides(membershipUserOverrides)
: defaultMembershipUserOverrides,
});
}
} catch (error) {
console.error("[Site Settings API] Update error:", error);
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: "Invalid request data", details: error.errors },
{ status: 400 }
);
}
return NextResponse.json(
{ error: "Failed to update site settings" },
{ status: 500 }
);
}
}