"use client"; import { useEffect, useState } from "react"; import dynamic from "next/dynamic"; import { RefreshCcw, CheckCircle2, ExternalLink, AlertTriangle, ShieldCheck } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { opsApi } from "@/lib/ops-api"; import type { BillingRiskIssue, BillingRiskPayload, PaymentRuntimePayload, PaymentIncident, PaymentRecord, } from "@/types/ops"; const PaymentIncidentPieChart = dynamic( () => import("./PaymentIncidentPieChart").then((mod) => mod.PaymentIncidentPieChart), { ssr: false, loading: () => 加载图表..., }, ); function paymentExplorerUrl(payment: PaymentRecord): string { const txHash = String(payment.tx_hash || "").trim(); if (!txHash) return ""; const chain = String(payment.chain || "").trim().toLowerCase(); const base = chain.includes("eth") ? "https://etherscan.io" : "https://polygonscan.com"; return `${base}/tx/${txHash}`; } function severityTone(severity?: string) { if (severity === "high") return "border-red-200 bg-red-50 text-red-700"; if (severity === "medium") return "border-amber-200 bg-amber-50 text-amber-700"; return "border-slate-200 bg-slate-50 text-slate-600"; } function severityLabel(severity?: string) { if (severity === "high") return "高"; if (severity === "medium") return "中"; if (severity === "low") return "低"; return severity || "未知"; } function compactDate(value?: string) { if (!value) return "—"; return value.slice(0, 19).replace("T", " "); } function paymentReasonLabel(reason?: string) { const key = String(reason || "").trim().toLowerCase(); if (key === "receiver_mismatch") return "收款地址不匹配"; if (key === "amount_mismatch") return "金额不匹配"; if (key === "chain_mismatch") return "支付网络不匹配"; if (key === "tx_not_found") return "链上交易未找到"; if (key === "tx_reverted") return "链上交易失败"; if (key === "expired") return "订单已过期"; if (key === "unknown") return "未知原因"; return key || "未知原因"; } function compactMono(value?: string, head = 10, tail = 6) { const text = String(value || "").trim(); if (!text) return "—"; if (text.length <= head + tail + 3) return text; return `${text.slice(0, head)}...${text.slice(-tail)}`; } function RiskStat({ label, value, sub, tone = "text-slate-950", }: { label: string; value: number; sub: string; tone?: string; }) { return (
{label}
{value}
{sub}
); } export function PaymentsPageClient() { const [loading, setLoading] = useState(true); const [runtime, setRuntime] = useState(null); const [incidents, setIncidents] = useState([]); const [payments, setPayments] = useState([]); const [risk, setRisk] = useState(null); const [resolving, setResolving] = useState>(new Set()); const load = async () => { setLoading(true); try { const [rt, inc, pay, riskPayload] = await Promise.all([ opsApi.paymentRuntime() as Promise, opsApi.incidents(50), opsApi.listPayments(50), opsApi.billingRisk(30, 80) as Promise, ]); setRuntime(rt); setIncidents((inc as unknown as { incidents?: PaymentIncident[] }).incidents ?? []); setPayments((pay as unknown as { payments?: PaymentRecord[] }).payments ?? []); setRisk(riskPayload); } catch { /* */ } setLoading(false); }; const handleResolve = async (id: number) => { setResolving((prev) => new Set(prev).add(id)); try { await opsApi.resolveIncident(id); setIncidents((prev) => prev.filter((i) => i.id !== id)); } catch { /* */ } setResolving((prev) => { const next = new Set(prev); next.delete(id); return next; }); }; useEffect(() => { void load(); }, []); if (loading) return
加载中...
; const reasonCounts: Record = {}; incidents.forEach((inc) => { const r = inc.reason || "unknown"; reasonCounts[r] = (reasonCounts[r] || 0) + 1; }); const incidentPieData = Object.entries(reasonCounts).map(([name, value]) => ({ name, value, })); const riskSummary = risk?.summary ?? {}; const riskIssues = risk?.issues ?? []; const referralRewards = risk?.recent_referral_rewards ?? []; const hasRisk = Number(riskSummary.issues ?? 0) > 0; return (

支付管理

{hasRisk ? : } 支付与邀请风控流水 最近 {risk?.window_days ?? 30} 天 · {compactDate(risk?.checked_at)}
{risk?.query_errors?.length ? (
部分 Supabase 表查询失败:{risk.query_errors.map((item) => item.table).filter(Boolean).join(", ")}
) : null} {riskIssues.length === 0 ? (
暂无试用、支付、推荐奖励或积分抵扣风险信号。
) : (
{riskIssues.slice(0, 30).map((issue: BillingRiskIssue, index) => ( ))}
级别 类型 问题 用户 时间 引用
{severityLabel(issue.severity)} {issue.category ?? "—"}
{issue.title ?? "—"}
{issue.detail ?? "—"}
{issue.user_id ? `${issue.user_id.slice(0, 10)}...` : "—"} {compactDate(issue.created_at)} {issue.reference ? String(issue.reference).slice(0, 14) : "—"}
)}
支付运行时
{runtime ? Object.entries({ chain_id: runtime.chain_id, last_scanned_block: runtime.last_scanned_block, audit_events_count: runtime.audit_events_count, }).map(([k, v]) => (
{k}
{String(v ?? "—")}
)) : 无数据}
{runtime?.receiver_contract ? (
receiver_contract
{runtime.receiver_contract}
) : null}
异常原因分布 {incidentPieData.length === 0 ? ( 暂无异常数据 ) : ( )}
推荐奖励结算 ({referralRewards.length}) {referralRewards.length === 0 ? ( 最近没有推荐奖励记录 ) : (
{referralRewards.slice(0, 20).map((row, index) => ( ))}
ID 邀请人 被邀请人 奖励 Intent 时间
{String(row.id ?? "—")} {String(row.referrer_user_id ?? "—").slice(0, 10)}... {String(row.referred_user_id ?? "—").slice(0, 10)}... +{Number(row.reward_points ?? 0).toLocaleString()} 分 {String(row.payment_intent_id ?? "—").slice(0, 14)} {compactDate(String(row.created_at ?? ""))}
)}
支付异常 ({incidents.length}) {incidents.length === 0 ? ( 暂无异常 ) : (
{incidents.map((inc) => ( ))}
ID 原因 / 详情 用户 / Intent 时间 操作
{inc.id}
{paymentReasonLabel(inc.reason)}
{inc.detail || inc.reason || "—"}
{compactMono(inc.user_id)}
{compactMono(inc.intent_id, 12, 6)}
{inc.created_at?.slice(0, 19) ?? "—"}
)}
成功支付记录 ({payments.length}) {payments.length === 0 ? ( 暂无记录 ) : (
{payments.map((p) => ( ))}
ID 用户 金额 Tx Hash 时间
{p.id} {p.user_id?.slice(0, 10) ?? "—"}... {p.amount} {p.currency} {p.chain ?? "—"} {p.tx_hash ? ( {p.tx_hash.slice(0, 8)}...{p.tx_hash.slice(-6)} ) : "—"} {p.created_at?.slice(0, 19) ?? "—"}
)}
); }