From 468a6f56458e04bdbec26ca1b454a4a1a25a58d6 Mon Sep 17 00:00:00 2001 From: oiov Date: Wed, 11 Jun 2025 15:06:36 +0800 Subject: [PATCH] feats: configurable plan quota --- app/(protected)/admin/domains/page.tsx | 2 +- .../admin/{domains => system}/domain-list.tsx | 4 +- app/(protected)/admin/system/loading.tsx | 12 + app/(protected)/admin/system/page.tsx | 47 +++ app/(protected)/admin/system/plan-list.tsx | 269 ++++++++++++++ app/(protected)/dashboard/page.tsx | 28 +- app/(protected)/dashboard/urls/meta-chart.tsx | 76 ++-- app/(protected)/dashboard/urls/meta.tsx | 73 ++-- app/(protected)/dashboard/urls/url-list.tsx | 2 +- app/api/admin/plan/route.ts | 137 +++++++ app/api/email/route.ts | 6 +- app/api/email/send/route.ts | 6 +- app/api/plan/names/route.ts | 25 ++ app/api/plan/route.ts | 30 ++ app/api/record/add/route.ts | 6 +- app/api/record/admin/add/route.ts | 6 +- app/api/url/add/route.ts | 5 +- app/api/v1/email/route.ts | 7 +- app/api/v1/short/route.ts | 6 +- components/forms/plan-form.tsx | 342 ++++++++++++++++++ components/forms/user-form.tsx | 55 ++- components/layout/site-footer.tsx | 4 +- components/sections/pricing.tsx | 101 +++--- config/dashboard.ts | 12 +- lib/dto/plan.ts | 159 ++++++++ lib/dto/systemConfig.ts | 301 +++++++++++++++ lib/validations/plan.ts | 19 + locales/en.json | 23 +- locales/zh.json | 25 +- .../migrations/20250610142211/migration.sql | 136 +++++++ prisma/schema.prisma | 54 ++- public/sw.js.map | 2 +- 32 files changed, 1781 insertions(+), 199 deletions(-) rename app/(protected)/admin/{domains => system}/domain-list.tsx (96%) create mode 100644 app/(protected)/admin/system/loading.tsx create mode 100644 app/(protected)/admin/system/page.tsx create mode 100644 app/(protected)/admin/system/plan-list.tsx create mode 100644 app/api/admin/plan/route.ts create mode 100644 app/api/plan/names/route.ts create mode 100644 app/api/plan/route.ts create mode 100644 components/forms/plan-form.tsx create mode 100644 lib/dto/plan.ts create mode 100644 lib/dto/systemConfig.ts create mode 100644 lib/validations/plan.ts create mode 100644 prisma/migrations/20250610142211/migration.sql diff --git a/app/(protected)/admin/domains/page.tsx b/app/(protected)/admin/domains/page.tsx index a781567..638e1f5 100644 --- a/app/(protected)/admin/domains/page.tsx +++ b/app/(protected)/admin/domains/page.tsx @@ -4,7 +4,7 @@ import { getCurrentUser } from "@/lib/session"; import { constructMetadata } from "@/lib/utils"; import { DashboardHeader } from "@/components/dashboard/header"; -import DomainList from "./domain-list"; +import DomainList from "../system/domain-list"; export const metadata = constructMetadata({ title: "Domains - WR.DO", diff --git a/app/(protected)/admin/domains/domain-list.tsx b/app/(protected)/admin/system/domain-list.tsx similarity index 96% rename from app/(protected)/admin/domains/domain-list.tsx rename to app/(protected)/admin/system/domain-list.tsx index 88adc77..19862e6 100644 --- a/app/(protected)/admin/domains/domain-list.tsx +++ b/app/(protected)/admin/system/domain-list.tsx @@ -306,7 +306,9 @@ export default function DomainList({ user, action }: DomainListProps) { setShowForm(!isShowForm); }} > -

{t("Edit")}

+

+ {t("Edit")} +

diff --git a/app/(protected)/admin/system/loading.tsx b/app/(protected)/admin/system/loading.tsx new file mode 100644 index 0000000..a4fbfcc --- /dev/null +++ b/app/(protected)/admin/system/loading.tsx @@ -0,0 +1,12 @@ +import { Skeleton } from "@/components/ui/skeleton"; +import { DashboardHeader } from "@/components/dashboard/header"; + +export default function DashboardRecordsLoading() { + return ( + <> + + + + + ); +} diff --git a/app/(protected)/admin/system/page.tsx b/app/(protected)/admin/system/page.tsx new file mode 100644 index 0000000..ead8a93 --- /dev/null +++ b/app/(protected)/admin/system/page.tsx @@ -0,0 +1,47 @@ +import { redirect } from "next/navigation"; + +import { getCurrentUser } from "@/lib/session"; +import { constructMetadata } from "@/lib/utils"; +import { DashboardHeader } from "@/components/dashboard/header"; + +import DomainList from "./domain-list"; +import PlanList from "./plan-list"; + +export const metadata = constructMetadata({ + title: "System Settings - WR.DO", + description: "", +}); + +export default async function DashboardPage() { + const user = await getCurrentUser(); + + if (!user?.id) redirect("/login"); + + return ( + <> + + + + + ); +} diff --git a/app/(protected)/admin/system/plan-list.tsx b/app/(protected)/admin/system/plan-list.tsx new file mode 100644 index 0000000..baa9859 --- /dev/null +++ b/app/(protected)/admin/system/plan-list.tsx @@ -0,0 +1,269 @@ +"use client"; + +import { useState } from "react"; +import { User } from "@prisma/client"; +import { PenLine, RefreshCwIcon } from "lucide-react"; +import { useTranslations } from "next-intl"; +import useSWR, { useSWRConfig } from "swr"; + +import { PlanQuotaFormData } from "@/lib/dto/plan"; +import { fetcher, nFormatter } from "@/lib/utils"; +import { useMediaQuery } from "@/hooks/use-media-query"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Modal } from "@/components/ui/modal"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Switch } from "@/components/ui/switch"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { PlanForm } from "@/components/forms/plan-form"; +import { FormType } from "@/components/forms/record-form"; +import { EmptyPlaceholder } from "@/components/shared/empty-placeholder"; +import { Icons } from "@/components/shared/icons"; +import { PaginationWrapper } from "@/components/shared/pagination"; +import { TimeAgoIntl } from "@/components/shared/time-ago"; + +export interface PlanListProps { + user: Pick; + action: string; +} + +function TableColumnSekleton() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +export default function PlanList({ user, action }: PlanListProps) { + const { isMobile } = useMediaQuery(); + const t = useTranslations("List"); + const [isShowForm, setShowForm] = useState(false); + const [formType, setFormType] = useState("add"); + const [currentEditPlan, setCurrentEditPlan] = + useState(null); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [searchParams, setSearchParams] = useState({ + slug: "", + target: "", + userName: "", + }); + + const { mutate } = useSWRConfig(); + const { data, isLoading } = useSWR<{ + total: number; + list: PlanQuotaFormData[]; + }>( + `${action}?page=${currentPage}&size=${pageSize}&target=${searchParams.target}`, + fetcher, + ); + + const handleRefresh = () => { + mutate( + `${action}?page=${currentPage}&size=${pageSize}&target=${searchParams.target}`, + undefined, + ); + }; + + return ( + <> + + +
+ {t("Quota Settings")} +
+ +
+ + +
+
+ + + + + + {t("Plan Name")} + + + {t("Short Limit")} + + + {t("Email Limit")} + + + {t("Send Limit")} + + + {t("Record Limit")} + + + {t("Active")} + + + {t("Updated")} + + + {t("Actions")} + + + + + {isLoading ? ( + <> + + + + + + + ) : data && data.list && data.list.length ? ( + data.list.map((plan) => ( +
+ + + {plan.name} + + + {nFormatter(plan.slNewLinks)} + + + {nFormatter(plan.emEmailAddresses)} + + + {nFormatter(plan.emSendEmails)} + + + {nFormatter(plan.rcNewRecords)} + + + + // handleChangeStatus(value, "active", domain) + // } + /> + + + + + + + + +
+ )) + ) : ( + + + + {t("No Plans")} + + + You don't have any plans yet. Start creating one. + + + )} +
+ {data && Math.ceil(data.total / pageSize) > 1 && ( + + )} +
+
+
+ + {/* form */} + + + + + ); +} diff --git a/app/(protected)/dashboard/page.tsx b/app/(protected)/dashboard/page.tsx index 6b835d4..b4f2f83 100644 --- a/app/(protected)/dashboard/page.tsx +++ b/app/(protected)/dashboard/page.tsx @@ -2,9 +2,9 @@ import { Suspense } from "react"; import { redirect } from "next/navigation"; import { UserRole } from "@prisma/client"; -import { TeamPlanQuota } from "@/config/team"; import { getUserRecordCount } from "@/lib/dto/cloudflare-dns-record"; import { getAllUserEmailsCount } from "@/lib/dto/email"; +import { getPlanQuota, PlanQuota } from "@/lib/dto/plan"; import { getUserShortUrlCount } from "@/lib/dto/short-urls"; import { getCurrentUser } from "@/lib/session"; import { constructMetadata } from "@/lib/utils"; @@ -25,10 +25,10 @@ export const metadata = constructMetadata({ async function EmailHeroCardSection({ userId, - team, + plan, }: { userId: string; - team: string; + plan: PlanQuota; }) { const email_count = await getAllUserEmailsCount(userId); @@ -36,17 +36,17 @@ async function EmailHeroCardSection({ ); } async function ShortUrlsCardSection({ userId, - team, + plan, }: { userId: string; - team: string; + plan: PlanQuota; }) { const url_count = await getUserShortUrlCount(userId); @@ -56,7 +56,7 @@ async function ShortUrlsCardSection({ title="Short URLs" total={url_count.total} monthTotal={url_count.month_total} - limit={TeamPlanQuota[team].SL_NewLinks} + limit={plan.slNewLinks} link="/dashboard/urls" icon="link" /> @@ -65,10 +65,10 @@ async function ShortUrlsCardSection({ async function DnsRecordsCardSection({ userId, - team, + plan, }: { userId: string; - team: string; + plan: PlanQuota; }) { const record_count = await getUserRecordCount(userId); @@ -78,7 +78,7 @@ async function DnsRecordsCardSection({ title="DNS Records" total={record_count.total} monthTotal={record_count.month_total} - limit={TeamPlanQuota[team].RC_NewRecords} + limit={plan.rcNewRecords} link="/dashboard/records" icon="globeLock" /> @@ -140,6 +140,8 @@ export default async function DashboardPage() { if (!user?.id) redirect("/login"); + const plan = await getPlanQuota(user.team); + return ( <>
@@ -150,7 +152,7 @@ export default async function DashboardPage() { } > - + } > - + } > - +
diff --git a/app/(protected)/dashboard/urls/meta-chart.tsx b/app/(protected)/dashboard/urls/meta-chart.tsx index 47c18ad..273a6fd 100644 --- a/app/(protected)/dashboard/urls/meta-chart.tsx +++ b/app/(protected)/dashboard/urls/meta-chart.tsx @@ -9,8 +9,8 @@ import { TopoJSONMap } from "@unovis/ts"; import { WorldMapTopoJSON } from "@unovis/ts/maps"; import { useTranslations } from "next-intl"; import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; +import useSWR from "swr"; -import { TeamPlanQuota } from "@/config/team"; import { getBotName, getCountryName, @@ -20,7 +20,7 @@ import { getRegionName, } from "@/lib/contries"; import { DATE_DIMENSION_ENUMS } from "@/lib/enums"; -import { isLink, removeUrlSuffix } from "@/lib/utils"; +import { fetcher, isLink, removeUrlSuffix } from "@/lib/utils"; import { useElementSize } from "@/hooks/use-element-size"; import { Button } from "@/components/ui/button"; import { @@ -179,6 +179,11 @@ export function DailyPVUVChart({ const t = useTranslations("Components"); + const { data: plan } = useSWR<{ slAnalyticsRetention: number }>( + `/api/plan?team=${user.team}`, + fetcher, + ); + const processedData = processUrlMeta(data).map((entry) => ({ date: entry.date, pv: entry.clicks, @@ -254,40 +259,39 @@ export function DailyPVUVChart({ {lastVisitorInfo}
- + {plan && ( + + )} {["pv", "uv"].map((key) => { const chart = key as keyof typeof chartConfig; return ( diff --git a/app/(protected)/dashboard/urls/meta.tsx b/app/(protected)/dashboard/urls/meta.tsx index 9e6806d..c68dfb7 100644 --- a/app/(protected)/dashboard/urls/meta.tsx +++ b/app/(protected)/dashboard/urls/meta.tsx @@ -5,7 +5,6 @@ import { UrlMeta, User } from "@prisma/client"; import { useTranslations } from "next-intl"; import useSWR from "swr"; -import { TeamPlanQuota } from "@/config/team"; import { DATE_DIMENSION_ENUMS } from "@/lib/enums"; import { fetcher } from "@/lib/utils"; import { @@ -37,6 +36,11 @@ export default function UserUrlMetaInfo({ user, action, urlId }: UrlMetaProps) { { focusThrottleInterval: 30000 }, // 30 seconds, ); + const { data: plan } = useSWR<{ slAnalyticsRetention: number }>( + `/api/plan?team=${user.team}`, + fetcher, + ); + if (isLoading) return (
@@ -55,40 +59,39 @@ export default function UserUrlMetaInfo({ user, action, urlId }: UrlMetaProps) { "", )} . - + {plan && ( + + )} ); diff --git a/app/(protected)/dashboard/urls/url-list.tsx b/app/(protected)/dashboard/urls/url-list.tsx index d56332b..d0dd340 100644 --- a/app/(protected)/dashboard/urls/url-list.tsx +++ b/app/(protected)/dashboard/urls/url-list.tsx @@ -410,7 +410,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) { setShowForm(!isShowForm); }} > -

{t("Edit")}

+

{t("Edit")}

+ )} + + +
+ +
+ ); +} diff --git a/components/forms/user-form.tsx b/components/forms/user-form.tsx index 283b195..4569b39 100644 --- a/components/forms/user-form.tsx +++ b/components/forms/user-form.tsx @@ -5,8 +5,10 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { User, UserRole } from "@prisma/client"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; +import useSWR from "swr"; import { ROLE_ENUM } from "@/lib/enums"; +import { fetcher } from "@/lib/utils"; import { updateUserSchema } from "@/lib/validations/auth"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -21,6 +23,7 @@ import { SelectTrigger, SelectValue, } from "../ui/select"; +import { Skeleton } from "../ui/skeleton"; import { Switch } from "../ui/switch"; export type FormData = User; @@ -64,6 +67,15 @@ export function UserForm({ }, }); + const { data: plans, isLoading } = useSWR( + "/api/plan/names", + fetcher, + { + revalidateOnFocus: false, + dedupingInterval: 10000, + }, + ); + const onSubmit = handleSubmit((data) => { if (type === "edit") { handleUpdate(data); @@ -175,24 +187,31 @@ export function UserForm({ - + {isLoading ? ( + + ) : ( + plans && + plans.length > 0 && ( + + ) + )}
diff --git a/components/layout/site-footer.tsx b/components/layout/site-footer.tsx index f63d1d5..7d63d02 100644 --- a/components/layout/site-footer.tsx +++ b/components/layout/site-footer.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import Link from "next/link"; -import { version } from "package.json"; +import pkg from "package.json"; import { footerLinks, siteConfig } from "@/config/site"; import { cn } from "@/lib/utils"; @@ -76,7 +76,7 @@ export function SiteFooter({ className }: React.HTMLAttributes) { rel="noreferrer" className="font-thin underline-offset-2 hover:underline" > - v{version} + v{pkg.version}
diff --git a/components/sections/pricing.tsx b/components/sections/pricing.tsx index d3fde69..a8e2c5b 100644 --- a/components/sections/pricing.tsx +++ b/components/sections/pricing.tsx @@ -6,68 +6,74 @@ import Link from "next/link"; import { motion } from "framer-motion"; import { X } from "lucide-react"; import { useTranslations } from "next-intl"; +import useSWR from "swr"; -import { TeamPlanQuota } from "@/config/team"; -import { cn, nFormatter } from "@/lib/utils"; +import { PlanQuotaFormData } from "@/lib/dto/plan"; +import { cn, fetcher, nFormatter } from "@/lib/utils"; import { Icons } from "../shared/icons"; import { Button } from "../ui/button"; -const getBenefits = (plan) => [ +const getBenefits = (plan: PlanQuotaFormData) => [ { - text: `${nFormatter(plan.SL_TrackedClicks)} tracked clicks/mo`, + text: `${nFormatter(plan.slTrackedClicks)} tracked clicks/mo`, checked: true, icon: , }, { - text: `${nFormatter(plan.SL_NewLinks)} new links/mo`, + text: `${nFormatter(plan.slNewLinks)} new links/mo`, checked: true, icon: , }, { - text: `${plan.SL_AnalyticsRetention}-day analytics retention`, + text: `${plan.slAnalyticsRetention}-day analytics retention`, checked: true, icon: , }, { text: `Customize short link QR code`, - checked: plan.SL_CustomQrCodeLogo, + checked: plan.slCustomQrCodeLogo, icon: , }, { - text: `${nFormatter(plan.EM_EmailAddresses)} email addresses/mo`, + text: `${nFormatter(plan.emEmailAddresses)} email addresses/mo`, checked: true, icon: , }, { - text: `${nFormatter(plan.EM_SendEmails)} send emails/mo`, + text: `${nFormatter(plan.emSendEmails)} send emails/mo`, checked: true, icon: , }, { - text: `${plan.SL_Domains === 1 ? "One" : plan.SL_Domains} domain${plan.SL_Domains > 1 ? "s" : ""}`, + text: `${plan.slDomains === 1 ? "One" : plan.slDomains} domain${plan.slDomains > 1 ? "s" : ""}`, checked: true, icon: , }, { text: "Advanced analytics", - checked: plan.SL_AdvancedAnalytics, + checked: plan.slAdvancedAnalytics, icon: , }, { - text: `${plan.APP_Support.charAt(0).toUpperCase() + plan.APP_Support.slice(1)} support`, + text: `${plan.appSupport.charAt(0).toUpperCase() + plan.appSupport.slice(1)} support`, checked: true, icon: , }, { text: "Open API Access", - checked: plan.APP_ApiAccess, + checked: plan.appApiAccess, icon: , }, ]; export const PricingSection = () => { const t = useTranslations("Landing"); + const { data: plan } = useSWR<{ + total: number; + list: PlanQuotaFormData[]; + }>(`/api/plan?all=1`, fetcher); + return (
{
- - - - } - benefits={getBenefits(TeamPlanQuota.free)} - /> - {/* - - - } - benefits={getBenefits(TeamPlanQuota.premium)} - /> */} - - - - } - benefits={getBenefits(TeamPlanQuota.business)} - /> + {plan && ( + + + + } + benefits={getBenefits(plan.list[0])} + /> + )} + {plan && ( + + + + } + benefits={getBenefits(plan.list[plan.list.length - 1])} + /> + )}
diff --git a/config/dashboard.ts b/config/dashboard.ts index 62b8ce2..69c847e 100644 --- a/config/dashboard.ts +++ b/config/dashboard.ts @@ -54,12 +54,6 @@ export const sidebarLinks: SidebarNavItem[] = [ title: "Admin Panel", authorizeOnly: UserRole.ADMIN, }, - { - href: "/admin/domains", - icon: "globeLock", - title: "Domains", - authorizeOnly: UserRole.ADMIN, - }, { href: "/admin/users", icon: "users", @@ -78,6 +72,12 @@ export const sidebarLinks: SidebarNavItem[] = [ title: "Records", authorizeOnly: UserRole.ADMIN, }, + { + href: "/admin/system", + icon: "settings", + title: "System Settings", + authorizeOnly: UserRole.ADMIN, + }, ], }, { diff --git a/lib/dto/plan.ts b/lib/dto/plan.ts new file mode 100644 index 0000000..3364644 --- /dev/null +++ b/lib/dto/plan.ts @@ -0,0 +1,159 @@ +import { prisma } from "../db"; + +export interface PlanQuota { + name: string; + slTrackedClicks: number; + slNewLinks: number; + slAnalyticsRetention: number; + slDomains: number; + slAdvancedAnalytics: boolean; + slCustomQrCodeLogo: boolean; + rcNewRecords: number; + emEmailAddresses: number; + emDomains: number; + emSendEmails: number; + appSupport: string; + appApiAccess: boolean; + isActive: boolean; +} + +export interface PlanQuotaFormData extends PlanQuota { + id?: string; + createdAt?: Date; + updatedAt?: Date; +} + +// 获取计划配额 +export async function getPlanQuota(planName: string) { + const plan = await prisma.plan.findUnique({ + where: { name: planName }, + }); + + if (!plan) { + return { + name: planName, + slTrackedClicks: 0, + slNewLinks: 0, + slAnalyticsRetention: 0, + slDomains: 0, + slAdvancedAnalytics: false, + slCustomQrCodeLogo: false, + rcNewRecords: 0, + emEmailAddresses: 0, + emDomains: 0, + emSendEmails: 0, + appSupport: "BASIC", + appApiAccess: true, + isActive: true, + }; + } + + return { + name: planName, + slTrackedClicks: plan.slTrackedClicks, + slNewLinks: plan.slNewLinks, + slAnalyticsRetention: plan.slAnalyticsRetention, + slDomains: plan.slDomains, + slAdvancedAnalytics: plan.slAdvancedAnalytics, + slCustomQrCodeLogo: plan.slCustomQrCodeLogo, + rcNewRecords: plan.rcNewRecords, + emEmailAddresses: plan.emEmailAddresses, + emDomains: plan.emDomains, + emSendEmails: plan.emSendEmails, + appSupport: plan.appSupport.toLowerCase(), + appApiAccess: plan.appApiAccess, + isActive: plan.isActive, + }; +} + +// 获取所有计划 +export async function getAllPlans(page = 1, size = 10, target: string = "") { + let option: any; + + if (target) { + option = { + name: { + contains: target, + }, + }; + } + + const [total, list] = await prisma.$transaction([ + prisma.plan.count({ + where: option, + }), + prisma.plan.findMany({ + where: option, + skip: (page - 1) * size, + take: size, + orderBy: { + slTrackedClicks: "asc", + }, + }), + ]); + return { list, total }; +} + +// 获取计划所有名称 +export async function getPlanNames() { + const data = await prisma.plan.findMany({ + where: { isActive: true }, + select: { name: true }, + orderBy: { name: "asc" }, + }); + + return data.map((item) => item.name); +} + +// 更新计划配额 +export async function updatePlanQuota(plan: PlanQuotaFormData) { + return await prisma.plan.update({ + where: { id: plan.id }, + data: { + // name: plan.name, + slTrackedClicks: plan.slTrackedClicks, + slNewLinks: plan.slNewLinks, + slAnalyticsRetention: plan.slAnalyticsRetention, + slDomains: plan.slDomains, + slAdvancedAnalytics: plan.slAdvancedAnalytics, + slCustomQrCodeLogo: plan.slCustomQrCodeLogo, + rcNewRecords: plan.rcNewRecords, + emEmailAddresses: plan.emEmailAddresses, + emDomains: plan.emDomains, + emSendEmails: plan.emSendEmails, + appSupport: plan.appSupport.toUpperCase() as any, + appApiAccess: plan.appApiAccess, + isActive: plan.isActive, + updatedAt: new Date(), + }, + }); +} + +// 创建新计划 +export async function createPlan(plan: PlanQuota) { + return await prisma.plan.create({ + data: { + name: plan.name, + slTrackedClicks: plan.slTrackedClicks, + slNewLinks: plan.slNewLinks, + slAnalyticsRetention: plan.slAnalyticsRetention, + slDomains: plan.slDomains, + slAdvancedAnalytics: plan.slAdvancedAnalytics, + slCustomQrCodeLogo: plan.slCustomQrCodeLogo, + rcNewRecords: plan.rcNewRecords, + emEmailAddresses: plan.emEmailAddresses, + emDomains: plan.emDomains, + emSendEmails: plan.emSendEmails, + appSupport: plan.appSupport.toUpperCase() as any, + appApiAccess: plan.appApiAccess, + isActive: true, + }, + }); +} + +// 删除计划(软删除) +export async function deletePlan(id: string) { + return await prisma.plan.delete({ + where: { id }, + }); +} diff --git a/lib/dto/systemConfig.ts b/lib/dto/systemConfig.ts new file mode 100644 index 0000000..b658bc0 --- /dev/null +++ b/lib/dto/systemConfig.ts @@ -0,0 +1,301 @@ +import { prisma } from "../db"; + +export type ConfigType = "BOOLEAN" | "STRING" | "NUMBER" | "OBJECT"; + +export interface SystemConfigData { + key: string; + value: any; // 解析后的实际值 + type: ConfigType; + description?: string; + version?: string; +} + +export interface CreateSystemConfigData { + key: string; + value: any; + type: ConfigType; + description?: string; + version?: string; +} + +export interface UpdateSystemConfigData { + value?: any; + type?: ConfigType; + description?: string; + version?: string; +} + +// 解析配置值 +function parseConfigValue(value: string, type: ConfigType): any { + switch (type) { + case "BOOLEAN": + return value === "true"; + case "NUMBER": + return Number(value); + case "OBJECT": + try { + return JSON.parse(value); + } catch { + return {}; + } + case "STRING": + default: + // 如果是 JSON 字符串格式的字符串,需要解析 + if (value.startsWith('"') && value.endsWith('"')) { + return JSON.parse(value); + } + return value; + } +} + +// 序列化配置值 +function serializeConfigValue(value: any, type: ConfigType): string { + switch (type) { + case "BOOLEAN": + return String(Boolean(value)); + case "NUMBER": + return String(Number(value)); + case "OBJECT": + return JSON.stringify(value); + case "STRING": + default: + return JSON.stringify(value); + } +} + +// 获取单个配置 +export async function getSystemConfig( + key: string, +): Promise { + const config = await prisma.systemConfig.findUnique({ + where: { key }, + }); + + if (!config) { + return null; + } + + return { + key: config.key, + value: parseConfigValue(config.value, config.type as ConfigType), + type: config.type as ConfigType, + description: config.description || undefined, + version: config.version, + }; +} + +// 获取配置值(简化版本,直接返回解析后的值) +export async function getConfigValue(key: string): Promise { + const config = await getSystemConfig(key); + return config ? config.value : null; +} + +// 获取所有配置 +export async function getAllSystemConfigs(): Promise { + const configs = await prisma.systemConfig.findMany({ + orderBy: { key: "asc" }, + }); + + return configs.map((config) => ({ + key: config.key, + value: parseConfigValue(config.value, config.type as ConfigType), + type: config.type as ConfigType, + description: config.description || undefined, + version: config.version, + })); +} + +// 获取配置的原始数据(包含元数据) +export async function getSystemConfigRaw(key: string) { + return await prisma.systemConfig.findUnique({ + where: { key }, + }); +} + +// 创建配置 +export async function createSystemConfig(data: CreateSystemConfigData) { + const serializedValue = serializeConfigValue(data.value, data.type); + + return await prisma.systemConfig.create({ + data: { + key: data.key, + value: serializedValue, + type: data.type, + description: data.description, + version: data.version || "0.5.0", + }, + }); +} + +// 更新配置 +export async function updateSystemConfig( + key: string, + data: UpdateSystemConfigData, +) { + const updateData: any = {}; + + if (data.value !== undefined && data.type) { + updateData.value = serializeConfigValue(data.value, data.type); + } + if (data.type !== undefined) { + updateData.type = data.type; + } + if (data.description !== undefined) { + updateData.description = data.description; + } + if (data.version !== undefined) { + updateData.version = data.version; + } + + return await prisma.systemConfig.update({ + where: { key }, + data: updateData, + }); +} + +// 设置配置值(upsert操作) +export async function setSystemConfig( + key: string, + value: any, + type: ConfigType, + description?: string, +) { + const serializedValue = serializeConfigValue(value, type); + + return await prisma.systemConfig.upsert({ + where: { key }, + update: { + value: serializedValue, + type, + description, + }, + create: { + key, + value: serializedValue, + type, + description, + }, + }); +} + +// 删除配置 +export async function deleteSystemConfig(key: string) { + return await prisma.systemConfig.delete({ + where: { key }, + }); +} + +// 批量获取配置 +export async function getMultipleConfigs( + keys: string[], +): Promise> { + const configs = await prisma.systemConfig.findMany({ + where: { + key: { + in: keys, + }, + }, + }); + + const result: Record = {}; + configs.forEach((config) => { + result[config.key] = parseConfigValue( + config.value, + config.type as ConfigType, + ); + }); + + return result; +} + +// 按类型获取配置 +export async function getConfigsByType( + type: ConfigType, +): Promise { + const configs = await prisma.systemConfig.findMany({ + where: { type }, + orderBy: { key: "asc" }, + }); + + return configs.map((config) => ({ + key: config.key, + value: parseConfigValue(config.value, config.type as ConfigType), + type: config.type as ConfigType, + description: config.description || undefined, + version: config.version, + })); +} + +// 搜索配置 +export async function searchConfigs( + searchTerm: string, +): Promise { + const configs = await prisma.systemConfig.findMany({ + where: { + OR: [ + { key: { contains: searchTerm, mode: "insensitive" } }, + { description: { contains: searchTerm, mode: "insensitive" } }, + ], + }, + orderBy: { key: "asc" }, + }); + + return configs.map((config) => ({ + key: config.key, + value: parseConfigValue(config.value, config.type as ConfigType), + type: config.type as ConfigType, + description: config.description || undefined, + version: config.version, + })); +} + +// 配置是否存在 +export async function configExists(key: string): Promise { + const count = await prisma.systemConfig.count({ + where: { key }, + }); + return count > 0; +} + +// 获取配置统计 +export async function getConfigStats() { + const total = await prisma.systemConfig.count(); + const byType = await prisma.systemConfig.groupBy({ + by: ["type"], + _count: { + type: true, + }, + }); + + return { + total, + byType: byType.reduce( + (acc, item) => { + acc[item.type] = item._count.type; + return acc; + }, + {} as Record, + ), + }; +} + +/** Usage Example */ + +/** + + // 取单个配置值 +const appName = await getConfigValue('app_name'); + +// 设置配置 +await setSystemConfig('maintenance_mode', true, 'BOOLEAN', 'Enable maintenance mode'); + +// 批量获取配置 +const configs = await getMultipleConfigs(['app_name', 'maintenance_mode', 'api_rate_limit']); + +// 搜索配置 +const emailConfigs = await searchConfigs('email'); + +// 获取统计信息 +const stats = await getConfigStats(); + + */ diff --git a/lib/validations/plan.ts b/lib/validations/plan.ts new file mode 100644 index 0000000..d2c3551 --- /dev/null +++ b/lib/validations/plan.ts @@ -0,0 +1,19 @@ +import * as z from "zod"; + +export const createPlanSchema = z.object({ + id: z.string().optional(), + name: z.string().min(3).max(32), + slTrackedClicks: z.number().optional().default(0), + slNewLinks: z.number().optional().default(0), + slAnalyticsRetention: z.number().optional().default(0), + slDomains: z.number().optional().default(0), + slAdvancedAnalytics: z.boolean().optional().default(true), + slCustomQrCodeLogo: z.boolean().optional().default(false), + rcNewRecords: z.number().optional().default(0), + emEmailAddresses: z.number().optional().default(0), + emDomains: z.number().optional().default(0), + emSendEmails: z.number().optional().default(0), + appSupport: z.string().optional().default("BASIC"), + appApiAccess: z.boolean().optional().default(true), + isActive: z.boolean().optional().default(true), +}); diff --git a/locales/en.json b/locales/en.json index a5f55f8..190cdd1 100644 --- a/locales/en.json +++ b/locales/en.json @@ -139,7 +139,22 @@ "send email service": "send email service", "How to get resend api key?": "How to get resend api key?", "Analytics": "Analytics", - "Edit URL": "Edit URL" + "Edit URL": "Edit URL", + "Plan Name": "Plan", + "Quota Settings": "Quota Settings", + "Short Limit": "Short Limit", + "Record Limit": "Record Limit", + "Email Limit": "Email Limit", + "Send Limit": "Send Limit", + "Domain Limit": "Domain Limit", + "Add Plan": "Add Plan", + "No Plans": "No Plans", + "Create Plan": "Create Plan", + "Edit Plan": "Edit Plan", + "Monthly limit of short links created": "Monthly limit of short links created", + "Monthly limit of subdomains created": "Monthly limit of subdomains created", + "Monthly limit of emails sent": "Monthly limit of emails sent", + "Monthly limit of email addresses created": "Monthly limit of email addresses created" }, "Components": { "Dashboard": "Dashboard", @@ -254,7 +269,8 @@ "Last 1 Year": "Last 1 Year", "All the time": "All the time", "No Visits": "No Visits", - "You don't have any visits yet in": "You don't have any visits yet in" + "You don't have any visits yet in": "You don't have any visits yet in", + "System Settings": "System Settings" }, "Landing": { "settings": "Settings", @@ -339,7 +355,8 @@ "System": "System", "Admin": "Admin", "Sign in": "Sign in", - "Log out": "Log out" + "Log out": "Log out", + "System Settings": "System Settings" }, "Email": { "Search emails": "Search emails", diff --git a/locales/zh.json b/locales/zh.json index 4441e28..477e5a1 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -110,7 +110,7 @@ "Time To Live": "生效时间", "Proxy": "代理记录", "Proxy status": "DNS 响应被 Cloudflare Anycast IP 替代", - "Total Domains": "总计", + "Total Domains": "域名管理", "Add Domain": "添加域名", "Domain Name": "域名", "Shorten Service": "短链服务", @@ -139,7 +139,22 @@ "send email service": "用于发送邮件服务", "How to get resend api key?": "如何获取 Resend API 密钥?", "Analytics": "访客分析", - "Edit URL": "编辑短链" + "Edit URL": "编辑短链", + "Plan Name": "计划名称", + "Quota Settings": "配额设置", + "Short Limit": "短链限制", + "Record Limit": "子域名限制", + "Email Limit": "邮箱限制", + "Send Limit": "发件限制", + "Domain Limit": "域名限制", + "Add Plan": "添加计划", + "No Plans": "暂无计划", + "Create Plan": "创建计划", + "Edit Plan": "编辑计划", + "Monthly limit of short links created": "每月创建短链数量限制", + "Monthly limit of subdomains created": "每月创建子域名数量限制", + "Monthly limit of emails sent": "每月发送邮件数量限制", + "Monthly limit of email addresses created": "每月接收邮件数量限制" }, "Components": { "Dashboard": "用户面板", @@ -254,7 +269,8 @@ "Last 1 Year": "最近 1 年", "All the time": "所有时间", "No Visits": "无访问记录", - "You don't have any visits yet in": "您还没有任何访问记录于" + "You don't have any visits yet in": "您还没有任何访问记录于", + "System Settings": "系统设置" }, "Landing": { "settings": "设置", @@ -339,7 +355,8 @@ "System": "跟随系统", "Admin": "管理面板", "Sign in": "登录", - "Log out": "退出登录" + "Log out": "退出登录", + "System Settings": "系统设置" }, "Email": { "Search emails": "搜索邮箱...", diff --git a/prisma/migrations/20250610142211/migration.sql b/prisma/migrations/20250610142211/migration.sql new file mode 100644 index 0000000..b6e1d0a --- /dev/null +++ b/prisma/migrations/20250610142211/migration.sql @@ -0,0 +1,136 @@ +CREATE TABLE "system_configs" +( + "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "key" TEXT NOT NULL, + "value" TEXT NOT NULL, + "type" TEXT NOT NULL, + "description" TEXT, + "version" TEXT NOT NULL DEFAULT '0.5.0', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- 1. 是否开启注册配置 +INSERT INTO "system_configs" + ( + "key", + "value", + "type", + "description" + ) +VALUES + ( + 'enable_user_registration', + 'true', + 'BOOLEAN', + '是否允许新用户注册' +); + +-- 2. 系统通知配置 +INSERT INTO "system_configs" + ( + "key", + "value", + "type", + "description" + ) +VALUES + ( + 'system_notification', + '', + 'STRING', + '系统全局通知消息' +); + +-- 创建计划表 +CREATE TABLE "plans" +( + "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "name" TEXT NOT NULL, + "slTrackedClicks" INTEGER NOT NULL, + "slNewLinks" INTEGER NOT NULL, + "slAnalyticsRetention" INTEGER NOT NULL, + "slDomains" INTEGER NOT NULL, + "slAdvancedAnalytics" BOOLEAN NOT NULL, + "slCustomQrCodeLogo" BOOLEAN NOT NULL, + "rcNewRecords" INTEGER NOT NULL, + "emEmailAddresses" INTEGER NOT NULL, + "emDomains" INTEGER NOT NULL, + "emSendEmails" INTEGER NOT NULL, + "appSupport" TEXT NOT NULL, + "appApiAccess" BOOLEAN NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- 创建唯一索引 +CREATE UNIQUE INDEX "plans_name_key" ON "plans"("name"); + +-- 插入初始数据 +INSERT INTO "plans" + ( + "id", + "name", + "slTrackedClicks", + "slNewLinks", + "slAnalyticsRetention", + "slDomains", + "slAdvancedAnalytics", + "slCustomQrCodeLogo", + "rcNewRecords", + "emEmailAddresses", + "emDomains", + "emSendEmails", + "appSupport", + "appApiAccess" + ) +VALUES + ( + '45fc1184-f7e7-4768-b28d-3f6e73d5a766', + 'free', + 100000, + 1000, + 180, + 2, + true, + false, + 3, + 1000, + 2, + 200, + 'BASIC', + true +), + ( + '45fc1184-f7e7-4768-b28f-3e6e73d5a769', + 'premium', + 1000000, + 5000, + 365, + 2, + true, + true, + 2, + 5000, + 2, + 1000, + 'LIVE', + true +), + ( + '45fc1184-f7e7-4768-b28d-3f6e73d5a678', + 'business', + 10000000, + 10000, + 1000, + 2, + true, + true, + 10, + 10000, + 2, + 2000, + 'LIVE', + true +); \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2ac6dd1..fd44f8c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -274,15 +274,47 @@ model Domain { @@map("domains") } -// model SystemConfig { -// id String @id @default(uuid()) -// key String @unique -// value String // JSON String -// type String // BOOLEAN, STRING, NUMBER, OBJECT -// description String? -// version String @default("0.5.0") -// createdAt DateTime @default(now()) -// updatedAt DateTime @default(now()) +model SystemConfig { + id String @id @default(uuid()) + key String @unique + value String // JSON String + type String // BOOLEAN, STRING, NUMBER, OBJECT + description String? + version String @default("0.5.0") + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) -// @@map("system_configs") -// } + @@map("system_configs") +} + +model Plan { + id String @id @default(uuid()) + name String @unique // "free", "premium", "business" + + // Short Link (SL) related quotas + slTrackedClicks Int + slNewLinks Int + slAnalyticsRetention Int // days + slDomains Int + slAdvancedAnalytics Boolean + slCustomQrCodeLogo Boolean + + // Record (RC) related quotas + rcNewRecords Int + + // Email (EM) related quotas + emEmailAddresses Int + emDomains Int + emSendEmails Int + + // App (APP) related settings + appSupport String // "BASIC", "LIVE" + appApiAccess Boolean + + // Metadata + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) + + @@map("plans") +} diff --git a/public/sw.js.map b/public/sw.js.map index d79d45b..f814335 100644 --- a/public/sw.js.map +++ b/public/sw.js.map @@ -1 +1 @@ -{"version":3,"file":"sw.js","sources":["../../../../../../private/var/folders/9b/3qmyp8zd2xvdspdrp149fyg00000gn/T/dc9e4fcb6944e0ab10b5262f978bd1f5/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-routing@6.6.0/node_modules/workbox-routing/registerRoute.mjs';\nimport {NetworkFirst as workbox_strategies_NetworkFirst} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkFirst.mjs';\nimport {NetworkOnly as workbox_strategies_NetworkOnly} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkOnly.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-core@6.6.0/node_modules/workbox-core/clientsClaim.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\nimportScripts(\n \n);\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n\nworkbox_routing_registerRoute(\"/\", new workbox_strategies_NetworkFirst({ \"cacheName\":\"start-url\", plugins: [{ cacheWillUpdate: async ({ request, response, event, state }) => { if (response && response.type === 'opaqueredirect') { return new Response(response.body, { status: 200, statusText: 'OK', headers: response.headers }) } return response } }] }), 'GET');\nworkbox_routing_registerRoute(/.*/i, new workbox_strategies_NetworkOnly({ \"cacheName\":\"dev\", plugins: [] }), 'GET');\n\n\n\n\n"],"names":["importScripts","self","skipWaiting","workbox_core_clientsClaim","workbox_routing_registerRoute","workbox_strategies_NetworkFirst","plugins","cacheWillUpdate","request","response","event","state","type","Response","body","status","statusText","headers","workbox_strategies_NetworkOnly"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"sw.js","sources":["../../../../../../private/var/folders/9b/3qmyp8zd2xvdspdrp149fyg00000gn/T/29f6a231f658fa58aad04224cddd7066/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-routing@6.6.0/node_modules/workbox-routing/registerRoute.mjs';\nimport {NetworkFirst as workbox_strategies_NetworkFirst} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkFirst.mjs';\nimport {NetworkOnly as workbox_strategies_NetworkOnly} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkOnly.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-core@6.6.0/node_modules/workbox-core/clientsClaim.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\nimportScripts(\n \n);\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n\nworkbox_routing_registerRoute(\"/\", new workbox_strategies_NetworkFirst({ \"cacheName\":\"start-url\", plugins: [{ cacheWillUpdate: async ({ request, response, event, state }) => { if (response && response.type === 'opaqueredirect') { return new Response(response.body, { status: 200, statusText: 'OK', headers: response.headers }) } return response } }] }), 'GET');\nworkbox_routing_registerRoute(/.*/i, new workbox_strategies_NetworkOnly({ \"cacheName\":\"dev\", plugins: [] }), 'GET');\n\n\n\n\n"],"names":["importScripts","self","skipWaiting","workbox_core_clientsClaim","workbox_routing_registerRoute","workbox_strategies_NetworkFirst","plugins","cacheWillUpdate","request","response","event","state","type","Response","body","status","statusText","headers","workbox_strategies_NetworkOnly"],"mappings":"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"} \ No newline at end of file