From 05f5d3c2dd19376d01a02efc50ecdee7120a31ad Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Wed, 20 May 2026 14:12:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=90=8E=E5=8F=B0=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E6=88=90=E5=8A=9F=E8=AE=B0=E5=BD=95=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ops/payments/PaymentsPageClient.tsx | 57 ++++++++++++++++++- frontend/lib/ops-api.ts | 21 +++++++ frontend/types/ops.ts | 16 ++++++ web/routers/ops.py | 12 ++++ web/services/ops_api.py | 35 ++++++++++++ 5 files changed, 138 insertions(+), 3 deletions(-) diff --git a/frontend/components/ops/payments/PaymentsPageClient.tsx b/frontend/components/ops/payments/PaymentsPageClient.tsx index 0e654dc1..95ab9a08 100644 --- a/frontend/components/ops/payments/PaymentsPageClient.tsx +++ b/frontend/components/ops/payments/PaymentsPageClient.tsx @@ -1,11 +1,11 @@ "use client"; import { useEffect, useState } from "react"; -import { RefreshCcw, CheckCircle2 } from "lucide-react"; +import { RefreshCcw, CheckCircle2, ExternalLink } 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 { PaymentRuntimePayload, PaymentIncident } from "@/types/ops"; +import type { PaymentRuntimePayload, PaymentIncident, PaymentRecord } from "@/types/ops"; import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, } from "recharts"; @@ -14,17 +14,20 @@ export function PaymentsPageClient() { const [loading, setLoading] = useState(true); const [runtime, setRuntime] = useState(null); const [incidents, setIncidents] = useState([]); + const [payments, setPayments] = useState([]); const [resolving, setResolving] = useState>(new Set()); const load = async () => { setLoading(true); try { - const [rt, inc] = await Promise.all([ + const [rt, inc, pay] = await Promise.all([ opsApi.paymentRuntime() as Promise, opsApi.incidents(50), + opsApi.listPayments(50), ]); setRuntime(rt); setIncidents((inc as unknown as { incidents?: PaymentIncident[] }).incidents ?? []); + setPayments((pay as unknown as { payments?: PaymentRecord[] }).payments ?? []); } catch { /* */ } setLoading(false); }; @@ -177,6 +180,54 @@ export function PaymentsPageClient() { )} + + + 成功支付记录 ({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) ?? "—"}
+
+ )} +
+
); } diff --git a/frontend/lib/ops-api.ts b/frontend/lib/ops-api.ts index 853b688d..6649aca1 100644 --- a/frontend/lib/ops-api.ts +++ b/frontend/lib/ops-api.ts @@ -19,6 +19,9 @@ export const opsApi = { paymentRuntime() { return opsFetch>("/api/payments/runtime"); }, + listPayments(limit = 50) { + return opsFetch<{ payments?: Array>; total?: number }>(`/api/ops/payments?limit=${limit}`); + }, async funnel(days = 30) { const raw = await opsFetch<{ events?: Record; @@ -84,4 +87,22 @@ export const opsApi = { const qs = new URLSearchParams(params).toString(); return opsFetch>(`/api/ops/truth-history?${qs}`); }, + userSubscriptions(email: string) { + return opsFetch<{ + email: string; + user_id: string; + subscriptions: Array<{ + id?: string; + user_id?: string; + status?: string; + plan_code?: string; + source?: string; + starts_at?: string; + expires_at?: string; + created_at?: string; + updated_at?: string; + }>; + count: number; + }>(`/api/ops/subscriptions/user?email=${encodeURIComponent(email)}`); + }, }; diff --git a/frontend/types/ops.ts b/frontend/types/ops.ts index 20523a29..420bd75e 100644 --- a/frontend/types/ops.ts +++ b/frontend/types/ops.ts @@ -75,6 +75,22 @@ export type IncidentsPayload = { total?: number; }; +export type PaymentRecord = { + id: number; + user_id?: string; + amount?: number; + currency?: string; + chain?: string; + tx_hash?: string; + status?: string; + created_at?: string; +}; + +export type PaymentsPayload = { + payments?: PaymentRecord[]; + total?: number; +}; + export type OpsUser = { telegram_id?: number; username?: string; diff --git a/web/routers/ops.py b/web/routers/ops.py index 3acff0f6..a0ae9675 100644 --- a/web/routers/ops.py +++ b/web/routers/ops.py @@ -12,11 +12,13 @@ from web.services.ops_api import ( get_ops_logs, get_ops_truth_history, get_ops_weekly_leaderboard, + get_ops_user_subscriptions, grant_ops_points, grant_ops_subscription, list_ops_memberships, list_ops_payment_incidents, resolve_ops_payment_incident, + list_ops_payments, search_ops_users, update_ops_config, ) @@ -64,6 +66,11 @@ async def ops_resolve_payment_incident(request: Request, event_id: int): return resolve_ops_payment_incident(request, event_id) +@router.get("/api/ops/payments") +async def ops_payments(request: Request, limit: int = 50): + return list_ops_payments(request, limit=limit) + + @router.post("/api/ops/users/grant-points") async def ops_grant_points(request: Request, body: GrantPointsRequest): return grant_ops_points(request, body) @@ -134,6 +141,11 @@ async def ops_subscription_extend(request: Request): return extend_ops_subscription(request, email=email, additional_days=days) +@router.get("/api/ops/subscriptions/user") +async def ops_user_subscriptions(request: Request, email: str = ""): + return get_ops_user_subscriptions(request, email=email) + + # ── Logs ──────────────────────────────────────────────────────────── @router.get("/api/ops/logs") diff --git a/web/services/ops_api.py b/web/services/ops_api.py index 04e945f8..e2aa3a5b 100644 --- a/web/services/ops_api.py +++ b/web/services/ops_api.py @@ -191,6 +191,41 @@ def resolve_ops_payment_incident(request: Request, event_id: int) -> Dict[str, A return {"ok": True, "incident": resolved} +def list_ops_payments( + request: Request, + limit: int = 50, +) -> Dict[str, Any]: + """List successful payment records from Supabase.""" + _require_ops(request) + import os + import requests as _requests + + supabase_url = str(os.getenv("SUPABASE_URL") or "").strip().rstrip("/") + service_role_key = str(os.getenv("SUPABASE_SERVICE_ROLE_KEY") or "").strip() + if not supabase_url or not service_role_key: + raise HTTPException(status_code=503, detail="Supabase not configured") + + headers = { + "apikey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + } + safe_limit = max(1, min(int(limit or 50), 200)) + resp = _requests.get( + f"{supabase_url}/rest/v1/payments", + headers=headers, + params={ + "select": "id,user_id,amount,currency,chain,tx_hash,status,created_at", + "order": "created_at.desc", + "limit": str(safe_limit), + }, + timeout=10, + ) + rows = resp.json() if resp.ok and resp.content else [] + if not isinstance(rows, list): + rows = [] + return {"payments": rows, "total": len(rows)} + + def grant_ops_points(request: Request, body: GrantPointsRequest) -> Dict[str, Any]: admin = _require_ops(request) or {} db = DBManager()