mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
feat: polish internal value displays
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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<string> {
|
||||
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 });
|
||||
|
||||
@@ -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[] }) {
|
||||
<DataTableCell>
|
||||
<div className="space-y-1">
|
||||
<p>{log.actorEmail || "系统"}</p>
|
||||
<p className="text-xs text-muted-foreground">{log.actorRole || "—"}</p>
|
||||
<p className="text-xs text-muted-foreground">{formatAuditActorRole(log.actorRole)}</p>
|
||||
</div>
|
||||
</DataTableCell>
|
||||
<DataTableCell className="whitespace-nowrap font-medium">{log.action}</DataTableCell>
|
||||
<DataTableCell className="whitespace-nowrap font-medium">
|
||||
{formatAuditAction(log.action)}
|
||||
</DataTableCell>
|
||||
<DataTableCell>
|
||||
<div className="space-y-1">
|
||||
<p>{log.targetType}</p>
|
||||
<p>{formatAuditTargetType(log.targetType)}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{log.targetLabel || log.targetId || "—"}
|
||||
{formatAuditTargetLabel(log)}
|
||||
</p>
|
||||
</div>
|
||||
</DataTableCell>
|
||||
<DataTableCell className="max-w-xl whitespace-pre-wrap break-words text-muted-foreground">
|
||||
{log.message}
|
||||
{formatAuditMessage(log.message)}
|
||||
</DataTableCell>
|
||||
</DataTableRow>
|
||||
))}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -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={
|
||||
<StatusBadge tone={node.status === "active" ? "success" : "neutral"}>
|
||||
{node.status}
|
||||
{getNodeStatusLabel(node.status)}
|
||||
</StatusBadge>
|
||||
}
|
||||
className="flex-1"
|
||||
|
||||
@@ -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
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<StatusBadge tone={node.status === "active" ? "success" : "neutral"}>
|
||||
{node.status}
|
||||
{getNodeStatusLabel(node.status)}
|
||||
</StatusBadge>
|
||||
<NodeForm
|
||||
node={{
|
||||
|
||||
@@ -16,6 +16,10 @@ import {
|
||||
} from "@/actions/admin/settings";
|
||||
import { toast } from "sonner";
|
||||
import { getErrorMessage } from "@/lib/errors";
|
||||
import {
|
||||
booleanAppSettingLabels,
|
||||
type BooleanAppSettingField,
|
||||
} from "@/lib/domain-labels";
|
||||
|
||||
interface AppConfig {
|
||||
siteName: string;
|
||||
@@ -72,23 +76,9 @@ interface CouponOption {
|
||||
|
||||
const selectClassName = "premium-input w-full appearance-none px-3.5 py-2 text-sm outline-none";
|
||||
|
||||
type ToggleValues = Record<BooleanSettingField, boolean>;
|
||||
type ToggleValues = Record<BooleanAppSettingField, boolean>;
|
||||
|
||||
const booleanSettingLabels: Record<BooleanSettingField, string> = {
|
||||
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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"',
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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<OrderStatus, string> = {
|
||||
PENDING: "待确认",
|
||||
PAID: "已支付",
|
||||
CANCELLED: "已取消",
|
||||
REFUNDED: "已退款",
|
||||
};
|
||||
|
||||
export const orderKindLabels: Record<OrderKind, string> = {
|
||||
NEW_PURCHASE: "新购",
|
||||
RENEWAL: "续费",
|
||||
TRAFFIC_TOPUP: "增流量",
|
||||
};
|
||||
|
||||
export const orderReviewStatusLabels: Record<OrderReviewStatus, string> = {
|
||||
NORMAL: "正常",
|
||||
FLAGGED: "异常",
|
||||
RESOLVED: "已解决",
|
||||
};
|
||||
|
||||
export const subscriptionStatusLabels: Record<SubscriptionStatus, string> = {
|
||||
ACTIVE: "活跃",
|
||||
EXPIRED: "已过期",
|
||||
CANCELLED: "已取消",
|
||||
SUSPENDED: "已暂停",
|
||||
};
|
||||
|
||||
export const subscriptionTypeLabels: Record<SubscriptionType, string> = {
|
||||
PROXY: "代理",
|
||||
STREAMING: "流媒体",
|
||||
};
|
||||
|
||||
export const userRoleLabels: Record<Role, string> = {
|
||||
ADMIN: "管理员",
|
||||
USER: "用户",
|
||||
};
|
||||
|
||||
export const userStatusLabels: Record<UserStatus, string> = {
|
||||
ACTIVE: "正常",
|
||||
PENDING_EMAIL: "待邮箱验证",
|
||||
DISABLED: "禁用",
|
||||
BANNED: "封禁",
|
||||
};
|
||||
|
||||
export const taskKindLabels: Record<TaskKind, string> = {
|
||||
REMINDER_DISPATCH: "提醒派发",
|
||||
ORDER_PROVISION_RETRY: "订单重试",
|
||||
};
|
||||
|
||||
export const taskStatusLabels: Record<TaskStatus, string> = {
|
||||
PENDING: "待执行",
|
||||
RUNNING: "运行中",
|
||||
SUCCESS: "成功",
|
||||
FAILED: "失败",
|
||||
};
|
||||
|
||||
export const announcementAudienceLabels: Record<AnnouncementAudience, string> = {
|
||||
PUBLIC: "公开",
|
||||
USERS: "全部用户",
|
||||
ADMINS: "全部管理员",
|
||||
SPECIFIC_USER: "指定用户",
|
||||
};
|
||||
|
||||
export const announcementDisplayTypeLabels: Record<AnnouncementDisplayType, string> = {
|
||||
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";
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="text-sm font-semibold text-pretty">{item.message}</p>
|
||||
<p className="text-sm font-semibold text-pretty">{formatAuditMessage(item.message)}</p>
|
||||
<span className="shrink-0 text-xs text-muted-foreground">{item.createdAt}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{item.action}
|
||||
{formatAuditAction(item.action)}
|
||||
{item.actorEmail ? ` · ${item.actorEmail}` : " · 系统"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -60,13 +60,15 @@ export function TrafficTrendChart({
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis unit=" GB" width={60} />
|
||||
<Tooltip
|
||||
labelFormatter={(label) => `日期:${label}`}
|
||||
formatter={(value) =>
|
||||
`${Number(typeof value === "number" ? value : 0).toFixed(2)} GB`
|
||||
[`${Number(typeof value === "number" ? value : 0).toFixed(2)} GB`, "流量"]
|
||||
}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="valueGb"
|
||||
name="流量"
|
||||
stroke={color}
|
||||
fill={color}
|
||||
fillOpacity={0.15}
|
||||
|
||||
198
src/lib/audit-display.ts
Normal file
198
src/lib/audit-display.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import {
|
||||
booleanAppSettingLabels,
|
||||
getBooleanAppSettingLabel,
|
||||
getPaymentProviderLabel,
|
||||
getTaskKindLabel,
|
||||
getUserRoleLabel,
|
||||
nodeStatusLabels,
|
||||
orderStatusLabels,
|
||||
paymentProviderLabels,
|
||||
subscriptionStatusLabels,
|
||||
subscriptionTypeLabels,
|
||||
taskKindLabels,
|
||||
userStatusLabels,
|
||||
} from "@/lib/domain-labels";
|
||||
|
||||
export const auditActionFilterOptions = [
|
||||
{ label: "全部动作", value: "" },
|
||||
{ label: "用户操作", value: "user." },
|
||||
{ label: "订单操作", value: "order." },
|
||||
{ label: "订阅操作", value: "subscription." },
|
||||
{ label: "套餐操作", value: "plan." },
|
||||
{ label: "服务操作", value: "service." },
|
||||
{ label: "节点操作", value: "node." },
|
||||
{ label: "入站操作", value: "inbound." },
|
||||
{ label: "任务操作", value: "task." },
|
||||
{ label: "风控操作", value: "risk." },
|
||||
{ label: "系统设置", value: "settings." },
|
||||
{ label: "支付配置", value: "payment." },
|
||||
{ label: "公告操作", value: "announcement." },
|
||||
{ label: "工单操作", value: "support." },
|
||||
{ label: "优惠规则", value: "coupon." },
|
||||
{ label: "满减规则", value: "promotion." },
|
||||
{ label: "备份恢复", value: "backup." },
|
||||
{ label: "流量同步", value: "traffic." },
|
||||
{ label: "流媒体槽位", value: "streaming-slot." },
|
||||
];
|
||||
|
||||
const auditActionLabels: Record<string, string> = {
|
||||
"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<string, string> = {
|
||||
Announcement: "公告",
|
||||
AppConfig: "系统设置",
|
||||
Coupon: "优惠券",
|
||||
Database: "数据库",
|
||||
NodeInbound: "线路入口",
|
||||
NodeServer: "节点",
|
||||
Order: "订单",
|
||||
PaymentConfig: "支付配置",
|
||||
PromotionRule: "满减规则",
|
||||
StreamingService: "流媒体服务",
|
||||
StreamingSlot: "流媒体槽位",
|
||||
SubscriptionPlan: "套餐",
|
||||
SupportTicket: "工单",
|
||||
TaskRun: "任务",
|
||||
TrafficSync: "流量同步",
|
||||
User: "用户",
|
||||
UserSubscription: "订阅",
|
||||
};
|
||||
|
||||
const tokenLabels: Record<string, string> = {
|
||||
...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);
|
||||
}
|
||||
168
src/lib/domain-labels.ts
Normal file
168
src/lib/domain-labels.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import type {
|
||||
AnnouncementAudience,
|
||||
AnnouncementDisplayType,
|
||||
OrderKind,
|
||||
OrderReviewStatus,
|
||||
OrderStatus,
|
||||
Role,
|
||||
SubscriptionStatus,
|
||||
SubscriptionType,
|
||||
TaskKind,
|
||||
TaskStatus,
|
||||
UserStatus,
|
||||
} from "@prisma/client";
|
||||
|
||||
export const orderStatusLabels: Record<OrderStatus, string> = {
|
||||
PENDING: "待确认",
|
||||
PAID: "已支付",
|
||||
CANCELLED: "已取消",
|
||||
REFUNDED: "已退款",
|
||||
};
|
||||
|
||||
export const orderKindLabels: Record<OrderKind, string> = {
|
||||
NEW_PURCHASE: "新购",
|
||||
RENEWAL: "续费",
|
||||
TRAFFIC_TOPUP: "增流量",
|
||||
};
|
||||
|
||||
export const orderReviewStatusLabels: Record<OrderReviewStatus, string> = {
|
||||
NORMAL: "正常",
|
||||
FLAGGED: "异常",
|
||||
RESOLVED: "已解决",
|
||||
};
|
||||
|
||||
export const subscriptionStatusLabels: Record<SubscriptionStatus, string> = {
|
||||
ACTIVE: "活跃",
|
||||
EXPIRED: "已过期",
|
||||
CANCELLED: "已取消",
|
||||
SUSPENDED: "已暂停",
|
||||
};
|
||||
|
||||
export const subscriptionTypeLabels: Record<SubscriptionType, string> = {
|
||||
PROXY: "代理",
|
||||
STREAMING: "流媒体",
|
||||
};
|
||||
|
||||
export const userRoleLabels: Record<Role, string> = {
|
||||
ADMIN: "管理员",
|
||||
USER: "用户",
|
||||
};
|
||||
|
||||
export const userStatusLabels: Record<UserStatus, string> = {
|
||||
ACTIVE: "正常",
|
||||
PENDING_EMAIL: "待邮箱验证",
|
||||
DISABLED: "禁用",
|
||||
BANNED: "封禁",
|
||||
};
|
||||
|
||||
export const taskKindLabels: Record<TaskKind, string> = {
|
||||
REMINDER_DISPATCH: "提醒派发",
|
||||
ORDER_PROVISION_RETRY: "订单重试",
|
||||
};
|
||||
|
||||
export const taskStatusLabels: Record<TaskStatus, string> = {
|
||||
PENDING: "待执行",
|
||||
RUNNING: "运行中",
|
||||
SUCCESS: "成功",
|
||||
FAILED: "失败",
|
||||
};
|
||||
|
||||
export const announcementAudienceLabels: Record<AnnouncementAudience, string> = {
|
||||
PUBLIC: "公开",
|
||||
USERS: "全部用户",
|
||||
ADMINS: "全部管理员",
|
||||
SPECIFIC_USER: "指定用户",
|
||||
};
|
||||
|
||||
export const announcementDisplayTypeLabels: Record<AnnouncementDisplayType, string> = {
|
||||
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<string, string> = {
|
||||
active: "已启用",
|
||||
inactive: "已停用",
|
||||
disabled: "已停用",
|
||||
error: "异常",
|
||||
offline: "离线",
|
||||
};
|
||||
|
||||
export const paymentProviderLabels: Record<string, string> = {
|
||||
epay: "易支付",
|
||||
alipay_f2f: "支付宝当面付",
|
||||
usdt_trc20: "USDT (TRC20)",
|
||||
};
|
||||
|
||||
export const paymentChannelLabels: Record<string, string> = {
|
||||
alipay: "支付宝",
|
||||
wxpay: "微信支付",
|
||||
};
|
||||
|
||||
function labelFromMap(map: Partial<Record<string, string>>, 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, "系统开关");
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function getPaymentAdapter(provider: string): Promise<PaymentAdapte
|
||||
});
|
||||
|
||||
if (!config || !config.enabled) {
|
||||
throw new Error(`Payment provider "${provider}" is not configured or disabled`);
|
||||
throw new Error(`${getPaymentProviderName(realProvider)}未配置或未启用`);
|
||||
}
|
||||
|
||||
const cfg = parsePaymentConfig(
|
||||
@@ -34,7 +34,7 @@ export async function getPaymentAdapter(provider: string): Promise<PaymentAdapte
|
||||
case "usdt_trc20":
|
||||
return new UsdtTrc20Adapter(cfg as UsdtTrc20Config);
|
||||
default:
|
||||
throw new Error(`Unknown payment provider: ${provider}`);
|
||||
throw new Error("未知支付方式");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { recordAuditLog } from "@/services/audit";
|
||||
import { createNotification } from "@/services/notifications";
|
||||
import { generateNodeClientCredential } from "@/services/node-client-credential";
|
||||
import { createPanelAdapter } from "@/services/node-panel/factory";
|
||||
import { getOrderKindLabel, getSubscriptionStatusLabel } from "@/lib/domain-labels";
|
||||
import type {
|
||||
NodeServer,
|
||||
Order,
|
||||
@@ -42,7 +43,7 @@ export async function provisionSubscriptionWithDb(
|
||||
return applyTrafficTopup(order, db);
|
||||
}
|
||||
|
||||
throw new Error(`开通订阅失败:不支持的订单类型 ${String(order.kind)}`);
|
||||
throw new Error(`开通订阅失败:不支持的订单类型${getOrderKindLabel(order.kind)}`);
|
||||
}
|
||||
|
||||
async function getNewPurchaseItems(order: PaidOrder, db: DbClient): Promise<NewOrderItem[]> {
|
||||
@@ -191,7 +192,7 @@ async function applyRenewal(order: PaidOrder, db: DbClient): Promise<string[]> {
|
||||
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();
|
||||
|
||||
@@ -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)}`;
|
||||
|
||||
Reference in New Issue
Block a user