diff --git a/src/app/(admin)/admin/subscription-risk/_components/subscription-risk-geo-details.tsx b/src/app/(admin)/admin/subscription-risk/_components/subscription-risk-geo-details.tsx index 532f09c..ab7dd78 100644 --- a/src/app/(admin)/admin/subscription-risk/_components/subscription-risk-geo-details.tsx +++ b/src/app/(admin)/admin/subscription-risk/_components/subscription-risk-geo-details.tsx @@ -1,4 +1,4 @@ -import { ChevronDown, Globe2, MapPin } from "lucide-react"; +import { ChevronDown, Globe2, MapPin, ScrollText } from "lucide-react"; import { StatusBadge } from "@/components/shared/status-badge"; import { WORLD_COUNTRY_PATHS } from "@/components/shared/world-map-paths"; import { formatDate } from "@/lib/utils"; @@ -102,6 +102,62 @@ function RiskMetric({ label, value }: { label: string; value: number }) { ); } +function AnalysisLogDetails({ summary }: { summary: SubscriptionRiskGeoSummary }) { + const logs = summary.analysisLogs; + + return ( +
+ + + + 分析日志 + + + {logs.length} 条 + + + +
+ {logs.length === 0 ? ( +

暂无分析日志。

+ ) : ( +
+ {logs.map((log) => ( +
+
+
+

{log.ip}

+

{formatDate(log.createdAt)}

+
+
+ {log.source} + {log.allowed ? "放行" : "拦截"} +
+
+

{log.location}

+ {log.detailLines.length > 0 ? ( +
    + {log.detailLines.map((line, index) => ( +
  • + {line} +
  • + ))} +
+ ) : ( +

+ 系统未写入额外分析详情,仅保留 IP、地区和访问结果。 +

+ )} + {log.userAgent &&

{log.userAgent}

} +
+ ))} +
+ )} +
+
+ ); +} + export function SubscriptionRiskGeoDetails({ summary }: { summary: SubscriptionRiskGeoSummary }) { return (
@@ -188,6 +244,8 @@ export function SubscriptionRiskGeoDetails({ summary }: { summary: SubscriptionR )} + +
); } diff --git a/src/services/subscription-risk-review.ts b/src/services/subscription-risk-review.ts index 5e74018..9b49345 100644 --- a/src/services/subscription-risk-review.ts +++ b/src/services/subscription-risk-review.ts @@ -81,6 +81,11 @@ export type SubscriptionRiskRecentAccess = { createdAt: string; }; +export type SubscriptionRiskAnalysisLog = SubscriptionRiskRecentAccess & { + source: string; + detailLines: string[]; +}; + export type SubscriptionRiskGeoSummary = { totalLogs: number; allowedLogs: number; @@ -92,6 +97,7 @@ export type SubscriptionRiskGeoSummary = { countries: SubscriptionRiskCountrySummary[]; points: SubscriptionRiskGeoPoint[]; recentAccesses: SubscriptionRiskRecentAccess[]; + analysisLogs: SubscriptionRiskAnalysisLog[]; }; type RiskEventScope = Pick< @@ -122,6 +128,22 @@ function parseCoordinate(value: string | null | undefined, min: number, max: num return parsed; } +function analysisSource(log: Pick) { + if (log.userAgent === "jboard-agent/xray-access-log" || log.reason?.includes("节点 Xray access log")) { + return "节点 Xray 日志"; + } + if (log.kind === "AGGREGATE") return "总订阅访问"; + return "订阅访问"; +} + +function splitAnalysisReason(reason: string | null | undefined) { + return (reason ?? "") + .split(";") + .map((line) => line.trim()) + .filter(Boolean) + .slice(0, 12); +} + function uniquePreview(values: Iterable, limit = 4) { const list = Array.from(new Set(Array.from(values).filter(Boolean))); if (list.length <= limit) return list; @@ -300,6 +322,17 @@ export function buildSubscriptionRiskGeoSummary(logs: SubscriptionRiskAccessLog[ userAgent: log.userAgent, createdAt: log.createdAt.toISOString(), })), + analysisLogs: logs.slice(0, 40).map((log) => ({ + id: log.id, + ip: log.ip, + location: locationLabel(log), + allowed: log.allowed, + reason: log.reason, + userAgent: log.userAgent, + createdAt: log.createdAt.toISOString(), + source: analysisSource(log), + detailLines: splitAnalysisReason(log.reason), + })), }; }