添加后台支付成功记录列表

This commit is contained in:
2569718930@qq.com
2026-05-20 14:12:35 +08:00
parent 452dfe2218
commit 05f5d3c2dd
5 changed files with 138 additions and 3 deletions
@@ -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<PaymentRuntimePayload | null>(null);
const [incidents, setIncidents] = useState<PaymentIncident[]>([]);
const [payments, setPayments] = useState<PaymentRecord[]>([]);
const [resolving, setResolving] = useState<Set<number>>(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<PaymentRuntimePayload>,
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() {
)}
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle> ({payments.length})</CardTitle></CardHeader>
<CardContent>
{payments.length === 0 ? (
<span className="text-sm text-slate-500"></span>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-white/10 text-left text-slate-400">
<th className="py-2 pr-4 font-medium">ID</th>
<th className="py-2 pr-4 font-medium"></th>
<th className="py-2 pr-4 font-medium"></th>
<th className="py-2 pr-4 font-medium"></th>
<th className="py-2 pr-4 font-medium">Tx Hash</th>
<th className="py-2 pr-4 font-medium"></th>
</tr>
</thead>
<tbody>
{payments.map((p) => (
<tr key={p.id} className="border-b border-white/5">
<td className="py-2 pr-4 text-slate-500 font-mono text-xs">{p.id}</td>
<td className="py-2 pr-4 text-slate-400 font-mono text-xs" title={p.user_id}>{p.user_id?.slice(0, 10) ?? "—"}...</td>
<td className="py-2 pr-4 text-emerald-300 font-mono">{p.amount} {p.currency}</td>
<td className="py-2 pr-4 text-slate-400 text-xs">{p.chain ?? "—"}</td>
<td className="py-2 pr-4 text-xs">
{p.tx_hash ? (
<a
href={`https://polygonscan.com/tx/${p.tx_hash}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300 font-mono inline-flex items-center gap-1"
>
{p.tx_hash.slice(0, 8)}...{p.tx_hash.slice(-6)}
<ExternalLink className="h-3 w-3" />
</a>
) : "—"}
</td>
<td className="py-2 pr-4 text-slate-400 text-xs whitespace-nowrap">{p.created_at?.slice(0, 19) ?? "—"}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</div>
);
}
+21
View File
@@ -19,6 +19,9 @@ export const opsApi = {
paymentRuntime() {
return opsFetch<Record<string, unknown>>("/api/payments/runtime");
},
listPayments(limit = 50) {
return opsFetch<{ payments?: Array<Record<string, unknown>>; total?: number }>(`/api/ops/payments?limit=${limit}`);
},
async funnel(days = 30) {
const raw = await opsFetch<{
events?: Record<string, { total?: number; unique_users?: number }>;
@@ -84,4 +87,22 @@ export const opsApi = {
const qs = new URLSearchParams(params).toString();
return opsFetch<Record<string, unknown>>(`/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)}`);
},
};
+16
View File
@@ -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;
+12
View File
@@ -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")
+35
View File
@@ -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()