Implement ops operational closure

This commit is contained in:
2569718930@qq.com
2026-06-23 17:07:25 +08:00
parent 24415ee427
commit 0c76874418
27 changed files with 2196 additions and 24 deletions
@@ -12,6 +12,7 @@ import type {
PaymentRuntimePayload,
PaymentIncident,
PaymentRecord,
RefundCase,
} from "@/types/ops";
const PaymentIncidentPieChart = dynamic(
@@ -60,6 +61,8 @@ function paymentReasonLabel(reason?: string) {
if (key === "expired") return "订单已过期";
if (key === "event_mismatch") return "支付事件不匹配";
if (key === "direct_transfer_mismatch") return "直接转账不匹配";
if (key === "refund_required") return "需要退款处理";
if (key === "duplicate_payment") return "重复付款";
if (key === "unknown") return "未知原因";
return key || "未知原因";
}
@@ -96,21 +99,24 @@ export function PaymentsPageClient() {
const [runtime, setRuntime] = useState<PaymentRuntimePayload | null>(null);
const [incidents, setIncidents] = useState<PaymentIncident[]>([]);
const [payments, setPayments] = useState<PaymentRecord[]>([]);
const [refunds, setRefunds] = useState<RefundCase[]>([]);
const [risk, setRisk] = useState<BillingRiskPayload | null>(null);
const [resolving, setResolving] = useState<Set<number>>(new Set());
const load = async () => {
setLoading(true);
try {
const [rt, inc, pay, riskPayload] = await Promise.all([
const [rt, inc, pay, refundPayload, riskPayload] = await Promise.all([
opsApi.paymentRuntime() as Promise<PaymentRuntimePayload>,
opsApi.incidents(50),
opsApi.listPayments(50),
opsApi.refunds(50),
opsApi.billingRisk(30, 80) as Promise<BillingRiskPayload>,
]);
setRuntime(rt);
setIncidents((inc as unknown as { incidents?: PaymentIncident[] }).incidents ?? []);
setPayments((pay as unknown as { payments?: PaymentRecord[] }).payments ?? []);
setRefunds((refundPayload as unknown as { refunds?: RefundCase[] }).refunds ?? []);
setRisk(riskPayload);
} catch { /* */ }
setLoading(false);
@@ -328,6 +334,11 @@ export function PaymentsPageClient() {
<div className="mt-0.5 max-w-xl truncate text-xs text-slate-500" title={inc.detail || inc.reason || ""}>
{inc.detail || inc.reason || "—"}
</div>
{inc.refund_case_id ? (
<div className="mt-1 text-[11px] font-semibold text-blue-700">
退 #{inc.refund_case_id} · {inc.refund_status || "open"}
</div>
) : null}
</td>
<td className="py-2 pr-4 text-xs text-slate-500">
<div className="font-mono" title={inc.user_id || ""}>{compactMono(inc.user_id)}</div>
@@ -355,6 +366,54 @@ export function PaymentsPageClient() {
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle>退 ({refunds.length})</CardTitle></CardHeader>
<CardContent>
{refunds.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"> / Intent</th>
<th className="py-2 pr-4 font-medium">Tx Hash</th>
<th className="py-2 pr-4 font-medium"></th>
<th className="py-2 pr-4 font-medium"></th>
</tr>
</thead>
<tbody>
{refunds.map((refund) => (
<tr key={refund.id} className="border-b border-white/5">
<td className="py-2 pr-4 font-mono text-xs text-slate-500">{refund.id}</td>
<td className="py-2 pr-4">
<div className="font-bold text-slate-900">{refund.status || "open"}</div>
<div className="text-xs text-amber-600">{paymentReasonLabel(refund.reason)}</div>
</td>
<td className="py-2 pr-4 text-xs text-slate-500">
<div className="font-mono" title={refund.user_id || ""}>{compactMono(refund.user_id)}</div>
<div className="mt-0.5 font-mono text-blue-700" title={refund.intent_id || ""}>{compactMono(refund.intent_id, 12, 6)}</div>
</td>
<td className="py-2 pr-4 font-mono text-xs text-slate-500" title={refund.tx_hash || ""}>
{compactMono(refund.tx_hash)}
</td>
<td className="py-2 pr-4 text-xs text-slate-500">
{refund.handled_by || refund.created_by || "—"}
</td>
<td className="py-2 pr-4 whitespace-nowrap text-xs text-slate-500">
{compactDate(refund.updated_at || refund.created_at)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle> ({payments.length})</CardTitle></CardHeader>
<CardContent>