From 042c5b34ab635d1f4ccaacf09aba8cc738dbb7f1 Mon Sep 17 00:00:00 2001 From: JetSprow Date: Thu, 30 Apr 2026 16:38:38 +1000 Subject: [PATCH] feat: polish internal value displays --- src/actions/admin/payments.ts | 5 +- src/actions/admin/settings.ts | 25 +-- src/actions/admin/tasks.ts | 5 +- src/actions/admin/users.ts | 5 +- src/actions/user/cart.ts | 5 +- src/actions/user/purchase.ts | 5 +- .../_components/audit-logs-table.tsx | 19 +- src/app/(admin)/admin/audit-logs/page.tsx | 13 +- src/app/(admin)/admin/nodes/[id]/page.tsx | 3 +- .../nodes/_components/node-card-list.tsx | 3 +- .../(admin)/admin/settings/settings-form.tsx | 22 +- .../pay/[orderId]/use-payment-flow.ts | 5 +- src/app/api/admin/export/audit-logs/route.ts | 44 ++-- src/app/api/payment/create/route.ts | 3 +- src/app/api/subscription/[id]/route.ts | 9 +- src/app/api/subscription/all/route.ts | 9 +- src/components/shared/domain-badges.tsx | 92 ++------ .../shared/subscription-timeline.tsx | 5 +- src/components/shared/traffic-trend-chart.tsx | 4 +- src/lib/audit-display.ts | 198 ++++++++++++++++++ src/lib/domain-labels.ts | 168 +++++++++++++++ src/services/payment/catalog.ts | 2 +- src/services/payment/factory.ts | 4 +- src/services/provision.ts | 5 +- src/services/subscription-risk-review.ts | 3 +- 25 files changed, 498 insertions(+), 163 deletions(-) create mode 100644 src/lib/audit-display.ts create mode 100644 src/lib/domain-labels.ts diff --git a/src/actions/admin/payments.ts b/src/actions/admin/payments.ts index 2d42768..853f30a 100644 --- a/src/actions/admin/payments.ts +++ b/src/actions/admin/payments.ts @@ -5,6 +5,7 @@ import { requireAdmin } from "@/lib/require-auth"; import { revalidatePath } from "next/cache"; import { decryptPaymentConfigForUse, + getPaymentProviderName, normalizePaymentConfig, parsePaymentConfig, preparePaymentConfigForStorage, @@ -54,8 +55,8 @@ export async function savePaymentConfig( action: "payment.config", targetType: "PaymentConfig", targetId: provider, - targetLabel: provider, - message: `${enabled ? "启用并更新" : "更新"}支付配置 ${provider}`, + targetLabel: getPaymentProviderName(provider), + message: `${enabled ? "启用并更新" : "更新"}支付配置 ${getPaymentProviderName(provider)}`, }); revalidatePath("/admin/payments"); } diff --git a/src/actions/admin/settings.ts b/src/actions/admin/settings.ts index 0bbab3a..6e68152 100644 --- a/src/actions/admin/settings.ts +++ b/src/actions/admin/settings.ts @@ -11,24 +11,15 @@ import { normalizeSiteUrl } from "@/services/site-url"; import { encrypt, isEncryptedValue } from "@/lib/crypto"; import { getErrorMessage } from "@/lib/errors"; import { sendSmtpTestEmail } from "@/services/email"; +import { + booleanAppSettingFields, + getBooleanAppSettingLabel, + type BooleanAppSettingField, +} from "@/lib/domain-labels"; -const booleanSettingFields = [ - "allowRegistration", - "emailVerificationRequired", - "requireInviteCode", - "autoReminderDispatchEnabled", - "trafficSyncEnabled", - "networkRecommendationsEnabled", - "networkInsightsEnabled", - "subscriptionRiskEnabled", - "subscriptionRiskAutoSuspend", - "nodeAccessRiskEnabled", - "inviteRewardEnabled", - "smtpEnabled", - "smtpSecure", -] as const; +const booleanSettingFields = booleanAppSettingFields; -export type BooleanSettingField = (typeof booleanSettingFields)[number]; +export type BooleanSettingField = BooleanAppSettingField; const settingsSchema = z.object({ siteName: z.string().trim().min(1, "站点名称不能为空"), @@ -363,7 +354,7 @@ export async function saveBooleanAppSetting(input: { targetType: "AppConfig", targetId: current.id, targetLabel: current.siteName, - message: `${parsed.value ? "开启" : "关闭"}系统开关 ${parsed.field}`, + message: `${parsed.value ? "开启" : "关闭"}${getBooleanAppSettingLabel(parsed.field)}开关`, }); revalidateSettingsViews(); diff --git a/src/actions/admin/tasks.ts b/src/actions/admin/tasks.ts index 2ee97fc..320be02 100644 --- a/src/actions/admin/tasks.ts +++ b/src/actions/admin/tasks.ts @@ -7,6 +7,7 @@ import { dispatchSubscriptionReminders } from "@/services/notifications"; import { confirmPendingOrder } from "@/services/payment/process"; import { runTask, updateTaskRun } from "@/services/task-center"; import { prisma } from "@/lib/prisma"; +import { getTaskKindLabel } from "@/lib/domain-labels"; function revalidateTaskViews() { revalidatePath("/admin/tasks"); @@ -37,7 +38,7 @@ export async function runReminderTask() { action: "task.run", targetType: "TaskRun", targetId: outcome.taskId, - targetLabel: "REMINDER_DISPATCH", + targetLabel: getTaskKindLabel("REMINDER_DISPATCH"), message: "手动执行提醒派发任务", }); @@ -82,7 +83,7 @@ export async function retryTaskRun(taskId: string) { action: "task.retry", targetType: "TaskRun", targetId: task.id, - targetLabel: task.kind, + targetLabel: getTaskKindLabel(task.kind), message: `重试任务 ${task.title}`, }); } catch (error) { diff --git a/src/actions/admin/users.ts b/src/actions/admin/users.ts index dd0f14b..8c7deea 100644 --- a/src/actions/admin/users.ts +++ b/src/actions/admin/users.ts @@ -7,6 +7,7 @@ import bcrypt from "bcryptjs"; import { z } from "zod"; import { actorFromSession, recordAuditLog } from "@/services/audit"; import { createPanelAdapter } from "@/services/node-panel/factory"; +import { getUserStatusLabel } from "@/lib/domain-labels"; const createUserSchema = z.object({ email: z.string().email(), @@ -86,7 +87,7 @@ export async function updateUserStatus(id: string, status: "ACTIVE" | "DISABLED" targetType: "User", targetId: user.id, targetLabel: user.email, - message: `将用户 ${user.email} 状态改为 ${status}`, + message: `将用户 ${user.email} 状态改为${getUserStatusLabel(status)}`, }); revalidatePath("/admin/users"); revalidatePath(`/admin/users/${user.id}`); @@ -291,7 +292,7 @@ export async function batchUpdateUserStatus(formData: FormData) { actor: actorFromSession(session), action: "user.batch_status", targetType: "User", - message: `批量更新 ${userIds.length} 个用户状态为 ${status}`, + message: `批量更新 ${userIds.length} 个用户状态为${getUserStatusLabel(String(status))}`, metadata: { userIds, status: String(status), diff --git a/src/actions/user/cart.ts b/src/actions/user/cart.ts index 8819df6..7c13479 100644 --- a/src/actions/user/cart.ts +++ b/src/actions/user/cart.ts @@ -6,6 +6,7 @@ import { requireAuth } from "@/lib/require-auth"; import { buildUnavailableMessage, getPlanAvailability } from "@/services/plan-availability"; import { getPlanPurchasePrice, calculateCheckoutDiscounts } from "@/services/commerce"; import { ensurePlanTrafficPoolCapacity } from "@/services/plan-traffic-pool"; +import { getSubscriptionTypeLabel } from "@/lib/domain-labels"; async function assertNoPendingOrder(userId: string) { const pendingOrder = await prisma.order.findFirst({ @@ -33,7 +34,7 @@ async function getProxyPlanForCart(planId: string) { }, }); - if (plan.type !== "PROXY") throw new Error(`套餐类型不匹配:${plan.name} 是 ${plan.type},不能作为代理套餐加入购物车`); + if (plan.type !== "PROXY") throw new Error(`套餐类型不匹配:${plan.name} 是${getSubscriptionTypeLabel(plan.type)},不能作为代理套餐加入购物车`); if (!plan.isActive) throw new Error(`套餐已下架:${plan.name} 当前不可购买`); return plan; } @@ -115,7 +116,7 @@ export async function addProxyPlanToCart( export async function addStreamingPlanToCart(planId: string) { const session = await requireAuth(); const plan = await prisma.subscriptionPlan.findUniqueOrThrow({ where: { id: planId } }); - if (plan.type !== "STREAMING") throw new Error(`套餐类型不匹配:${plan.name} 是 ${plan.type},不能作为流媒体套餐加入购物车`); + if (plan.type !== "STREAMING") throw new Error(`套餐类型不匹配:${plan.name} 是${getSubscriptionTypeLabel(plan.type)},不能作为流媒体套餐加入购物车`); if (!plan.isActive) throw new Error(`套餐已下架:${plan.name} 当前不可购买`); const availability = await getPlanAvailability(plan, { userId: session.user.id }); diff --git a/src/actions/user/purchase.ts b/src/actions/user/purchase.ts index 4c24e92..f40fa26 100644 --- a/src/actions/user/purchase.ts +++ b/src/actions/user/purchase.ts @@ -13,6 +13,7 @@ import { getPlanTrafficPoolState, } from "@/services/plan-traffic-pool"; import { getPlanPurchasePrice, roundMoney } from "@/services/commerce"; +import { getSubscriptionTypeLabel } from "@/lib/domain-labels"; async function assertNoPendingOrder(userId: string) { const pendingOrder = await prisma.order.findFirst({ @@ -133,7 +134,7 @@ export async function purchaseProxy( }, }); - if (plan.type !== "PROXY") throw new Error(`套餐类型不匹配:${plan.name} 是 ${plan.type},不能作为代理套餐购买`); + if (plan.type !== "PROXY") throw new Error(`套餐类型不匹配:${plan.name} 是${getSubscriptionTypeLabel(plan.type)},不能作为代理套餐购买`); if (!plan.isActive) throw new Error(`套餐已下架:${plan.name} 当前不可购买`); const price = getPlanPurchasePrice(plan, trafficGb); @@ -213,7 +214,7 @@ export async function purchaseStreaming(planId: string): Promise { where: { id: planId }, }); - if (plan.type !== "STREAMING") throw new Error(`套餐类型不匹配:${plan.name} 是 ${plan.type},不能作为流媒体套餐购买`); + if (plan.type !== "STREAMING") throw new Error(`套餐类型不匹配:${plan.name} 是${getSubscriptionTypeLabel(plan.type)},不能作为流媒体套餐购买`); if (!plan.isActive) throw new Error(`套餐已下架:${plan.name} 当前不可购买`); const availability = await getPlanAvailability(plan, { userId: session.user.id }); diff --git a/src/app/(admin)/admin/audit-logs/_components/audit-logs-table.tsx b/src/app/(admin)/admin/audit-logs/_components/audit-logs-table.tsx index 9910c20..b7f73fa 100644 --- a/src/app/(admin)/admin/audit-logs/_components/audit-logs-table.tsx +++ b/src/app/(admin)/admin/audit-logs/_components/audit-logs-table.tsx @@ -9,6 +9,13 @@ import { DataTableHeaderRow, DataTableRow, } from "@/components/shared/data-table"; +import { + formatAuditAction, + formatAuditActorRole, + formatAuditMessage, + formatAuditTargetLabel, + formatAuditTargetType, +} from "@/lib/audit-display"; import { formatDate } from "@/lib/utils"; export function AuditLogsTable({ logs }: { logs: AuditLog[] }) { @@ -37,20 +44,22 @@ export function AuditLogsTable({ logs }: { logs: AuditLog[] }) {

{log.actorEmail || "系统"}

-

{log.actorRole || "—"}

+

{formatAuditActorRole(log.actorRole)}

- {log.action} + + {formatAuditAction(log.action)} +
-

{log.targetType}

+

{formatAuditTargetType(log.targetType)}

- {log.targetLabel || log.targetId || "—"} + {formatAuditTargetLabel(log)}

- {log.message} + {formatAuditMessage(log.message)} ))} diff --git a/src/app/(admin)/admin/audit-logs/page.tsx b/src/app/(admin)/admin/audit-logs/page.tsx index 72f9f9a..a3085c0 100644 --- a/src/app/(admin)/admin/audit-logs/page.tsx +++ b/src/app/(admin)/admin/audit-logs/page.tsx @@ -4,6 +4,7 @@ import { AdminFilterBar } from "@/components/admin/filter-bar"; import { PageHeader, PageShell } from "@/components/shared/page-shell"; import { Pagination } from "@/components/shared/pagination"; import { buttonVariants } from "@/components/ui/button"; +import { auditActionFilterOptions } from "@/lib/audit-display"; import { AuditLogsTable } from "./_components/audit-logs-table"; import { buildAuditLogExportHref, getAuditLogs } from "./audit-logs-data"; @@ -42,17 +43,7 @@ export default async function AuditLogsPage({ { name: "action", value: filters.action, - options: [ - { label: "全部动作前缀", value: "" }, - { label: "user.", value: "user." }, - { label: "order.", value: "order." }, - { label: "subscription.", value: "subscription." }, - { label: "plan.", value: "plan." }, - { label: "service.", value: "service." }, - { label: "node.", value: "node." }, - { label: "task.", value: "task." }, - { label: "risk.", value: "risk." }, - ], + options: auditActionFilterOptions, }, ]} /> diff --git a/src/app/(admin)/admin/nodes/[id]/page.tsx b/src/app/(admin)/admin/nodes/[id]/page.tsx index 9f071e2..c4c9393 100644 --- a/src/app/(admin)/admin/nodes/[id]/page.tsx +++ b/src/app/(admin)/admin/nodes/[id]/page.tsx @@ -4,6 +4,7 @@ import { ArrowLeft } from "lucide-react"; import { PageHeader, PageShell } from "@/components/shared/page-shell"; import { StatusBadge } from "@/components/shared/status-badge"; import { buttonVariants } from "@/components/ui/button"; +import { getNodeStatusLabel } from "@/lib/domain-labels"; import { getNodeDetail } from "./node-detail-data"; import { NodeDetailTabs } from "./_components/node-detail-tabs"; @@ -34,7 +35,7 @@ export default async function NodeDetailPage({ description={`3x-ui · ${node.panelUrl || "未配置面板"}`} actions={ - {node.status} + {getNodeStatusLabel(node.status)} } className="flex-1" diff --git a/src/app/(admin)/admin/nodes/_components/node-card-list.tsx b/src/app/(admin)/admin/nodes/_components/node-card-list.tsx index 4aea6d2..3fe353a 100644 --- a/src/app/(admin)/admin/nodes/_components/node-card-list.tsx +++ b/src/app/(admin)/admin/nodes/_components/node-card-list.tsx @@ -5,6 +5,7 @@ import { BatchActionBar, BatchActionButton } from "@/components/admin/batch-acti import { EmptyState } from "@/components/shared/page-shell"; import { StatusBadge } from "@/components/shared/status-badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { getNodeStatusLabel } from "@/lib/domain-labels"; import { InboundDeleteButton } from "../inbound-delete-button"; import { InboundDisplayNameForm } from "../inbound-display-name-form"; import { NodeActions } from "../node-actions"; @@ -52,7 +53,7 @@ function NodeCard({ node, siteUrl }: { node: NodeServerRow; siteUrl: string | nu
- {node.status} + {getNodeStatusLabel(node.status)} ; +type ToggleValues = Record; -const booleanSettingLabels: Record = { - allowRegistration: "开放注册", - emailVerificationRequired: "注册邮箱验证", - requireInviteCode: "邀请码注册", - autoReminderDispatchEnabled: "自动提醒派发", - trafficSyncEnabled: "3x-ui 流量定时同步", - networkRecommendationsEnabled: "三网推荐", - networkInsightsEnabled: "线路体验", - subscriptionRiskEnabled: "风控总控", - subscriptionRiskAutoSuspend: "自动暂停", - nodeAccessRiskEnabled: "节点日志风控", - inviteRewardEnabled: "自动发放奖励", - smtpEnabled: "邮件服务", - smtpSecure: "SSL 直连", -}; +const booleanSettingLabels = booleanAppSettingLabels; function initialToggleValues(config: AppConfig): ToggleValues { return { diff --git a/src/app/(payment)/pay/[orderId]/use-payment-flow.ts b/src/app/(payment)/pay/[orderId]/use-payment-flow.ts index 0cb1b66..95e7657 100644 --- a/src/app/(payment)/pay/[orderId]/use-payment-flow.ts +++ b/src/app/(payment)/pay/[orderId]/use-payment-flow.ts @@ -4,6 +4,7 @@ import { useEffect, useEffectEvent, useMemo, useState } from "react"; import { cancelOwnPendingOrder, resetOwnPendingPaymentChoice } from "@/actions/user/orders"; import { fetchJson } from "@/lib/fetch-json"; import { getErrorMessage } from "@/lib/errors"; +import { getOrderStatusLabel } from "@/lib/domain-labels"; import type { OrderPaymentSnapshot, PaymentInfo, @@ -111,7 +112,7 @@ export function usePaymentFlow(orderId: string) { if (order.status !== "PENDING") { setStatus("idle"); - setPageError(`这笔订单当前为 ${order.status},无法继续支付。`); + setPageError(`这笔订单当前为${getOrderStatusLabel(order.status)},无法继续支付。`); return; } @@ -161,7 +162,7 @@ export function usePaymentFlow(orderId: string) { setStatus("idle"); setPageError( - result.error || `订单状态更新:${orderStatusLabel[result.status] ?? result.status}`, + result.error || `订单状态更新:${orderStatusLabel[result.status] ?? "未知状态"}`, ); } catch (error) { setStatus("idle"); diff --git a/src/app/api/admin/export/audit-logs/route.ts b/src/app/api/admin/export/audit-logs/route.ts index a47ca13..6a06668 100644 --- a/src/app/api/admin/export/audit-logs/route.ts +++ b/src/app/api/admin/export/audit-logs/route.ts @@ -1,5 +1,12 @@ import { prisma } from "@/lib/prisma"; import { requireAdminApiSession } from "@/lib/admin-api"; +import { + formatAuditAction, + formatAuditActorRole, + formatAuditMessage, + formatAuditTargetLabel, + formatAuditTargetType, +} from "@/lib/audit-display"; export async function GET(req: Request) { const { errorResponse } = await requireAdminApiSession(); @@ -9,24 +16,37 @@ export async function GET(req: Request) { const { searchParams } = new URL(req.url); const q = searchParams.get("q")?.trim() ?? ""; + const action = searchParams.get("action") ?? ""; const logs = await prisma.auditLog.findMany({ - where: q - ? { - OR: [ - { action: { contains: q } }, - { targetType: { contains: q } }, - { targetLabel: { contains: q } }, - { actorEmail: { contains: q } }, - { message: { contains: q } }, - ], - } - : undefined, + where: { + ...(action ? { action: { startsWith: action } } : {}), + ...(q + ? { + OR: [ + { action: { contains: q } }, + { targetType: { contains: q } }, + { targetLabel: { contains: q } }, + { actorEmail: { contains: q } }, + { message: { contains: q } }, + ], + } + : {}), + }, orderBy: { createdAt: "desc" }, take: 5000, }); - return new Response(JSON.stringify(logs, null, 2), { + const rows = logs.map((log) => ({ + ...log, + actorRoleLabel: formatAuditActorRole(log.actorRole), + actionLabel: formatAuditAction(log.action), + targetTypeLabel: formatAuditTargetType(log.targetType), + targetLabelDisplay: formatAuditTargetLabel(log), + messageDisplay: formatAuditMessage(log.message), + })); + + return new Response(JSON.stringify(rows, null, 2), { headers: { "Content-Type": "application/json; charset=utf-8", "Content-Disposition": 'attachment; filename="jboard-audit-logs.json"', diff --git a/src/app/api/payment/create/route.ts b/src/app/api/payment/create/route.ts index bc1473e..5eb6ffb 100644 --- a/src/app/api/payment/create/route.ts +++ b/src/app/api/payment/create/route.ts @@ -6,6 +6,7 @@ import { getPaymentAdapter } from "@/services/payment/factory"; import { rateLimit } from "@/lib/rate-limit"; import { getSiteBaseUrl } from "@/services/site-url"; import { getActiveSubscriptionRiskRestriction } from "@/services/subscription-risk-review"; +import { getOrderStatusLabel } from "@/lib/domain-labels"; import { v4 as uuidv4 } from "uuid"; const createPaymentSchema = z.object({ @@ -66,7 +67,7 @@ export async function POST(req: Request) { } if (order.status !== "PENDING") { - return jsonError(`订单当前状态为 ${order.status},无法继续支付`, { + return jsonError(`订单当前状态为${getOrderStatusLabel(order.status)},无法继续支付`, { status: 400, }); } diff --git a/src/app/api/subscription/[id]/route.ts b/src/app/api/subscription/[id]/route.ts index c9e57e4..f670bec 100644 --- a/src/app/api/subscription/[id]/route.ts +++ b/src/app/api/subscription/[id]/route.ts @@ -11,6 +11,7 @@ import { rateLimit } from "@/lib/rate-limit"; import { getClientRequestContext } from "@/lib/request-context"; import { recordSubscriptionAccess } from "@/services/subscription-risk"; import { getAppConfig } from "@/services/app-config"; +import { getSubscriptionStatusLabel } from "@/lib/domain-labels"; const SUBSCRIPTION_RATE_WINDOW_SECONDS = 60 * 60; @@ -60,7 +61,7 @@ export async function GET( allowed: false, reason: "rate_limited", }); - return jsonError("Too many subscription requests", 429); + return jsonError("订阅请求过于频繁,请稍后再试", 429); } } @@ -104,7 +105,7 @@ export async function GET( allowed: false, reason: "rate_limited", }); - return jsonError("Too many subscription requests", 429); + return jsonError("订阅请求过于频繁,请稍后再试", 429); } } @@ -117,7 +118,7 @@ export async function GET( allowed: false, reason: "subscription_inactive", }); - return jsonError(`订阅当前状态为 ${sub.status},只有 ACTIVE 状态可以拉取配置`, 403); + return jsonError(`订阅当前状态为${getSubscriptionStatusLabel(sub.status)},只有活跃订阅可以拉取配置`, 403); } const risk = await recordSubscriptionAccess({ @@ -130,7 +131,7 @@ export async function GET( }); if (risk.suspended) { - return jsonError("Subscription suspended by risk control", 403); + return jsonError("订阅已被风控暂停,请联系管理员处理", 403); } const format = resolveSubscriptionFormat(url.searchParams, req.headers.get("user-agent")); diff --git a/src/app/api/subscription/all/route.ts b/src/app/api/subscription/all/route.ts index 654ef49..ce4e0d6 100644 --- a/src/app/api/subscription/all/route.ts +++ b/src/app/api/subscription/all/route.ts @@ -12,6 +12,7 @@ import { rateLimit } from "@/lib/rate-limit"; import { getClientRequestContext } from "@/lib/request-context"; import { recordSubscriptionAccess } from "@/services/subscription-risk"; import { getAppConfig } from "@/services/app-config"; +import { getUserStatusLabel } from "@/lib/domain-labels"; const SUBSCRIPTION_RATE_WINDOW_SECONDS = 60 * 60; @@ -41,7 +42,7 @@ export async function GET(req: Request) { allowed: false, reason: "rate_limited", }); - return jsonError("Too many subscription requests", 429); + return jsonError("订阅请求过于频繁,请稍后再试", 429); } } @@ -87,7 +88,7 @@ export async function GET(req: Request) { allowed: false, reason: "rate_limited", }); - return jsonError("Too many subscription requests", 429); + return jsonError("订阅请求过于频繁,请稍后再试", 429); } } @@ -99,7 +100,7 @@ export async function GET(req: Request) { allowed: false, reason: "user_inactive", }); - return jsonError("User inactive", 403); + return jsonError(`账户当前状态为${getUserStatusLabel(user.status)},暂时无法拉取总订阅`, 403); } const risk = await recordSubscriptionAccess({ @@ -111,7 +112,7 @@ export async function GET(req: Request) { }); if (risk.suspended) { - return jsonError("Subscriptions suspended by risk control", 403); + return jsonError("总订阅已被风控暂停,请联系管理员处理", 403); } const format = resolveSubscriptionFormat(url.searchParams, req.headers.get("user-agent")); diff --git a/src/components/shared/domain-badges.tsx b/src/components/shared/domain-badges.tsx index 73b8cff..7559027 100644 --- a/src/components/shared/domain-badges.tsx +++ b/src/components/shared/domain-badges.tsx @@ -1,85 +1,37 @@ import type { AnnouncementAudience, - AnnouncementDisplayType, - OrderKind, OrderReviewStatus, OrderStatus, Role, SubscriptionStatus, SubscriptionType, - TaskKind, TaskStatus, UserStatus, } from "@prisma/client"; import { StatusBadge, type StatusTone } from "@/components/shared/status-badge"; +import { + orderReviewStatusLabels, + orderStatusLabels, + subscriptionStatusLabels, + subscriptionTypeLabels, + taskStatusLabels, + userRoleLabels, + userStatusLabels, +} from "@/lib/domain-labels"; -export const orderStatusLabels: Record = { - PENDING: "待确认", - PAID: "已支付", - CANCELLED: "已取消", - REFUNDED: "已退款", -}; - -export const orderKindLabels: Record = { - NEW_PURCHASE: "新购", - RENEWAL: "续费", - TRAFFIC_TOPUP: "增流量", -}; - -export const orderReviewStatusLabels: Record = { - NORMAL: "正常", - FLAGGED: "异常", - RESOLVED: "已解决", -}; - -export const subscriptionStatusLabels: Record = { - ACTIVE: "活跃", - EXPIRED: "已过期", - CANCELLED: "已取消", - SUSPENDED: "已暂停", -}; - -export const subscriptionTypeLabels: Record = { - PROXY: "代理", - STREAMING: "流媒体", -}; - -export const userRoleLabels: Record = { - ADMIN: "管理员", - USER: "用户", -}; - -export const userStatusLabels: Record = { - ACTIVE: "正常", - PENDING_EMAIL: "待邮箱验证", - DISABLED: "禁用", - BANNED: "封禁", -}; - -export const taskKindLabels: Record = { - REMINDER_DISPATCH: "提醒派发", - ORDER_PROVISION_RETRY: "订单重试", -}; - -export const taskStatusLabels: Record = { - PENDING: "待执行", - RUNNING: "运行中", - SUCCESS: "成功", - FAILED: "失败", -}; - -export const announcementAudienceLabels: Record = { - PUBLIC: "公开", - USERS: "全部用户", - ADMINS: "全部管理员", - SPECIFIC_USER: "指定用户", -}; - -export const announcementDisplayTypeLabels: Record = { - INLINE: "普通公告", - BIG: "大公告", - POPUP: "弹窗公告", -}; +export { + announcementAudienceLabels, + announcementDisplayTypeLabels, + orderKindLabels, + orderReviewStatusLabels, + orderStatusLabels, + subscriptionStatusLabels, + subscriptionTypeLabels, + taskKindLabels, + taskStatusLabels, + userRoleLabels, + userStatusLabels, +} from "@/lib/domain-labels"; export function getOrderStatusTone(status: OrderStatus): StatusTone { if (status === "PAID") return "success"; diff --git a/src/components/shared/subscription-timeline.tsx b/src/components/shared/subscription-timeline.tsx index 4578dd1..bbee6e8 100644 --- a/src/components/shared/subscription-timeline.tsx +++ b/src/components/shared/subscription-timeline.tsx @@ -1,6 +1,7 @@ "use client"; import { CircleDot } from "lucide-react"; +import { formatAuditAction, formatAuditMessage } from "@/lib/audit-display"; interface TimelineItem { id: string; @@ -28,11 +29,11 @@ export function SubscriptionTimeline({
-

{item.message}

+

{formatAuditMessage(item.message)}

{item.createdAt}

- {item.action} + {formatAuditAction(item.action)} {item.actorEmail ? ` · ${item.actorEmail}` : " · 系统"}

diff --git a/src/components/shared/traffic-trend-chart.tsx b/src/components/shared/traffic-trend-chart.tsx index 1d4c826..869b333 100644 --- a/src/components/shared/traffic-trend-chart.tsx +++ b/src/components/shared/traffic-trend-chart.tsx @@ -60,13 +60,15 @@ export function TrafficTrendChart({ `日期:${label}`} formatter={(value) => - `${Number(typeof value === "number" ? value : 0).toFixed(2)} GB` + [`${Number(typeof value === "number" ? value : 0).toFixed(2)} GB`, "流量"] } /> = { + "announcement.create": "创建公告", + "announcement.update": "更新公告", + "announcement.enable": "启用公告", + "announcement.disable": "停用公告", + "announcement.delete": "删除公告", + "backup.restore": "恢复数据库", + "coupon.create": "创建优惠券", + "coupon.toggle": "切换优惠券状态", + "inbound.delete": "删除线路入口", + "inbound.display_name.update": "更新线路名称", + "node.create": "创建节点", + "node.update": "更新节点", + "node.delete": "删除节点", + "node.test": "同步节点入站", + "node.probe_token.generate": "生成探测 Token", + "node.probe_token.revoke": "撤销探测 Token", + "order.confirm": "确认订单", + "order.cancel": "取消订单", + "order.review": "更新订单审查", + "payment.config": "更新支付配置", + "plan.create": "创建套餐", + "plan.update": "更新套餐", + "plan.enable": "上架套餐", + "plan.disable": "下架套餐", + "plan.delete": "删除套餐", + "plan.batch_enable": "批量上架套餐", + "plan.batch_disable": "批量下架套餐", + "promotion.create": "创建满减规则", + "promotion.toggle": "切换满减规则状态", + "risk.node_access.suspend": "节点风控暂停订阅", + "risk.node_access.warning": "记录节点访问警告", + "risk.subscription.finalize": "完成风控处置", + "risk.subscription.report.generate": "生成风控报告", + "risk.subscription.report.send": "发送风控通知", + "risk.subscription.review": "更新订阅风控事件", + "risk.subscription.suspend": "风控暂停订阅", + "risk.subscription.warning": "记录订阅风险警告", + "service.create": "创建流媒体服务", + "service.update": "更新流媒体服务", + "service.delete": "删除流媒体服务", + "service.enable": "启用流媒体服务", + "service.disable": "停用流媒体服务", + "service.batch_enable": "批量启用流媒体服务", + "service.batch_disable": "批量停用流媒体服务", + "settings.toggle": "切换系统开关", + "settings.update": "更新系统设置", + "streaming-slot.reassign": "调配流媒体槽位", + "subscription.activate": "恢复订阅", + "subscription.auto_suspend": "自动暂停订阅", + "subscription.cancel": "取消订阅", + "subscription.create": "创建订阅", + "subscription.delete": "删除订阅", + "subscription.renew": "续费订阅", + "subscription.rotate_access": "重置订阅访问密钥", + "subscription.suspend": "暂停订阅", + "subscription.topup": "追加流量", + "support.close": "关闭工单", + "support.delete": "删除工单", + "support.reply": "回复工单", + "support.update": "更新工单", + "task.retry": "重试任务", + "task.run": "执行任务", + "traffic.sync": "同步流量视图", + "user.batch_status": "批量更新用户状态", + "user.create": "创建用户", + "user.force_delete": "强制删除用户", + "user.status": "更新用户状态", + "user.update": "更新用户", +}; + +const auditTargetTypeLabels: Record = { + Announcement: "公告", + AppConfig: "系统设置", + Coupon: "优惠券", + Database: "数据库", + NodeInbound: "线路入口", + NodeServer: "节点", + Order: "订单", + PaymentConfig: "支付配置", + PromotionRule: "满减规则", + StreamingService: "流媒体服务", + StreamingSlot: "流媒体槽位", + SubscriptionPlan: "套餐", + SupportTicket: "工单", + TaskRun: "任务", + TrafficSync: "流量同步", + User: "用户", + UserSubscription: "订阅", +}; + +const tokenLabels: Record = { + ...booleanAppSettingLabels, + ...nodeStatusLabels, + ...orderStatusLabels, + ...paymentProviderLabels, + ...subscriptionStatusLabels, + ...subscriptionTypeLabels, + ...taskKindLabels, + ...userStatusLabels, +}; + +function escapeRegExp(value: string) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function replaceKnownTokens(value: string) { + return Object.entries(tokenLabels) + .sort(([left], [right]) => right.length - left.length) + .reduce((text, [token, label]) => { + const pattern = new RegExp(`\\b${escapeRegExp(token)}\\b`, "g"); + return text.replace(pattern, label); + }, value); +} + +export function formatAuditAction(action: string) { + const exact = auditActionLabels[action]; + if (exact) return exact; + + const prefix = auditActionFilterOptions.find((option) => option.value && action.startsWith(option.value)); + return prefix?.label ?? "系统操作"; +} + +export function formatAuditTargetType(targetType: string | null | undefined) { + if (!targetType) return "系统"; + return auditTargetTypeLabels[targetType] ?? "业务对象"; +} + +export function formatAuditTargetLabel({ + targetType, + targetLabel, + targetId, +}: { + targetType: string | null | undefined; + targetLabel?: string | null; + targetId?: string | null; +}) { + if (targetLabel) { + if (targetType === "PaymentConfig") return getPaymentProviderLabel(targetLabel); + if (targetType === "TaskRun") return getTaskKindLabel(targetLabel); + return replaceKnownTokens(targetLabel); + } + + if (targetType === "Database") return "数据库"; + if (targetType === "TrafficSync") return "全站流量"; + if (targetId) return `ID ${targetId.slice(0, 8)}`; + return "—"; +} + +export function formatAuditActorRole(role: string | null | undefined) { + if (!role) return "系统"; + return getUserRoleLabel(role); +} + +export function formatAuditMessage(message: string) { + const withSettingLabels = message.replace( + /系统开关\s+([A-Za-z][A-Za-z0-9_]*)/g, + (_match, field: string) => `${getBooleanAppSettingLabel(field)}开关`, + ); + return replaceKnownTokens(withSettingLabels); +} diff --git a/src/lib/domain-labels.ts b/src/lib/domain-labels.ts new file mode 100644 index 0000000..10f3182 --- /dev/null +++ b/src/lib/domain-labels.ts @@ -0,0 +1,168 @@ +import type { + AnnouncementAudience, + AnnouncementDisplayType, + OrderKind, + OrderReviewStatus, + OrderStatus, + Role, + SubscriptionStatus, + SubscriptionType, + TaskKind, + TaskStatus, + UserStatus, +} from "@prisma/client"; + +export const orderStatusLabels: Record = { + PENDING: "待确认", + PAID: "已支付", + CANCELLED: "已取消", + REFUNDED: "已退款", +}; + +export const orderKindLabels: Record = { + NEW_PURCHASE: "新购", + RENEWAL: "续费", + TRAFFIC_TOPUP: "增流量", +}; + +export const orderReviewStatusLabels: Record = { + NORMAL: "正常", + FLAGGED: "异常", + RESOLVED: "已解决", +}; + +export const subscriptionStatusLabels: Record = { + ACTIVE: "活跃", + EXPIRED: "已过期", + CANCELLED: "已取消", + SUSPENDED: "已暂停", +}; + +export const subscriptionTypeLabels: Record = { + PROXY: "代理", + STREAMING: "流媒体", +}; + +export const userRoleLabels: Record = { + ADMIN: "管理员", + USER: "用户", +}; + +export const userStatusLabels: Record = { + ACTIVE: "正常", + PENDING_EMAIL: "待邮箱验证", + DISABLED: "禁用", + BANNED: "封禁", +}; + +export const taskKindLabels: Record = { + REMINDER_DISPATCH: "提醒派发", + ORDER_PROVISION_RETRY: "订单重试", +}; + +export const taskStatusLabels: Record = { + PENDING: "待执行", + RUNNING: "运行中", + SUCCESS: "成功", + FAILED: "失败", +}; + +export const announcementAudienceLabels: Record = { + PUBLIC: "公开", + USERS: "全部用户", + ADMINS: "全部管理员", + SPECIFIC_USER: "指定用户", +}; + +export const announcementDisplayTypeLabels: Record = { + INLINE: "普通公告", + BIG: "大公告", + POPUP: "弹窗公告", +}; + +export const booleanAppSettingLabels = { + allowRegistration: "开放注册", + emailVerificationRequired: "注册邮箱验证", + requireInviteCode: "邀请码注册", + autoReminderDispatchEnabled: "自动提醒派发", + trafficSyncEnabled: "3x-ui 流量定时同步", + networkRecommendationsEnabled: "三网推荐", + networkInsightsEnabled: "线路体验", + subscriptionRiskEnabled: "订阅访问风控", + subscriptionRiskAutoSuspend: "风控自动暂停", + nodeAccessRiskEnabled: "节点日志风控", + inviteRewardEnabled: "自动发放奖励", + smtpEnabled: "邮件服务", + smtpSecure: "SMTP SSL 直连", +} as const; + +export type BooleanAppSettingField = keyof typeof booleanAppSettingLabels; + +export const booleanAppSettingFields = Object.keys(booleanAppSettingLabels) as [ + BooleanAppSettingField, + ...BooleanAppSettingField[], +]; + +export const nodeStatusLabels: Record = { + active: "已启用", + inactive: "已停用", + disabled: "已停用", + error: "异常", + offline: "离线", +}; + +export const paymentProviderLabels: Record = { + epay: "易支付", + alipay_f2f: "支付宝当面付", + usdt_trc20: "USDT (TRC20)", +}; + +export const paymentChannelLabels: Record = { + alipay: "支付宝", + wxpay: "微信支付", +}; + +function labelFromMap(map: Partial>, value: string | null | undefined, fallback: string) { + if (!value) return fallback; + return map[value] ?? fallback; +} + +export function getOrderStatusLabel(status: string | null | undefined) { + return labelFromMap(orderStatusLabels, status, "未知订单状态"); +} + +export function getOrderKindLabel(kind: string | null | undefined) { + return labelFromMap(orderKindLabels, kind, "未知订单类型"); +} + +export function getSubscriptionStatusLabel(status: string | null | undefined) { + return labelFromMap(subscriptionStatusLabels, status, "未知订阅状态"); +} + +export function getSubscriptionTypeLabel(type: string | null | undefined) { + return labelFromMap(subscriptionTypeLabels, type, "未知套餐类型"); +} + +export function getUserStatusLabel(status: string | null | undefined) { + return labelFromMap(userStatusLabels, status, "未知用户状态"); +} + +export function getUserRoleLabel(role: string | null | undefined) { + return labelFromMap(userRoleLabels, role, "未知角色"); +} + +export function getTaskKindLabel(kind: string | null | undefined) { + return labelFromMap(taskKindLabels, kind, "任务"); +} + +export function getNodeStatusLabel(status: string | null | undefined) { + return labelFromMap(nodeStatusLabels, status, "未知状态"); +} + +export function getPaymentProviderLabel(provider: string | null | undefined) { + return labelFromMap(paymentProviderLabels, provider, "支付方式"); +} + +export function getBooleanAppSettingLabel(field: string | null | undefined) { + return labelFromMap(booleanAppSettingLabels, field, "系统开关"); +} diff --git a/src/services/payment/catalog.ts b/src/services/payment/catalog.ts index 962fd9e..9835865 100644 --- a/src/services/payment/catalog.ts +++ b/src/services/payment/catalog.ts @@ -200,7 +200,7 @@ export function parsePaymentConfig( const schema = paymentConfigSchemas[provider as keyof typeof paymentConfigSchemas]; if (!schema) { - throw new Error(`未知支付方式:${provider}`); + throw new Error("未知支付方式"); } return schema.parse(normalized); diff --git a/src/services/payment/factory.ts b/src/services/payment/factory.ts index 5a1e2a9..e8c013c 100644 --- a/src/services/payment/factory.ts +++ b/src/services/payment/factory.ts @@ -18,7 +18,7 @@ export async function getPaymentAdapter(provider: string): Promise { @@ -191,7 +192,7 @@ async function applyRenewal(order: PaidOrder, db: DbClient): Promise { throw new Error("续费目标订阅与订单不匹配"); } if (subscription.status !== "ACTIVE" || subscription.endDate <= new Date()) { - throw new Error(`续费失败:目标订阅状态为 ${subscription.status},到期时间为 ${subscription.endDate.toISOString()}`); + throw new Error(`续费失败:目标订阅状态为${getSubscriptionStatusLabel(subscription.status)},到期时间为 ${subscription.endDate.toISOString()}`); } const now = new Date(); diff --git a/src/services/subscription-risk-review.ts b/src/services/subscription-risk-review.ts index 9b49345..b41a832 100644 --- a/src/services/subscription-risk-review.ts +++ b/src/services/subscription-risk-review.ts @@ -6,6 +6,7 @@ import type { } from "@prisma/client"; import { prisma, type DbClient } from "@/lib/prisma"; import { formatDate } from "@/lib/utils"; +import { getSubscriptionStatusLabel, getSubscriptionTypeLabel } from "@/lib/domain-labels"; export const subscriptionRiskAccessLogSelect = { id: true, @@ -370,7 +371,7 @@ export function buildSubscriptionRiskReport(input: { const { event, logs, user, subscription } = input; const summary = buildSubscriptionRiskGeoSummary(logs); const target = subscription - ? `${subscription.plan.name}(${subscription.plan.type},当前状态:${subscription.status})` + ? `${subscription.plan.name}(${getSubscriptionTypeLabel(subscription.plan.type)},当前状态:${getSubscriptionStatusLabel(subscription.status)})` : "用户总订阅"; const userLabel = user ? `${user.email}${user.name ? `(${user.name})` : ""}` : event.userId ?? "未知用户"; const windowRange = `${formatDate(event.windowStartedAt)} 至 ${formatDate(event.createdAt)}`;