From 6914e6277bc208f9ffec4bb1691694dca4c8e2b4 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sun, 19 Apr 2026 20:14:07 +0800 Subject: [PATCH] feat: implement backend routing, dashboard components, and transaction reconciliation scripts --- frontend/components/account/AccountCenter.tsx | 9 +++++-- frontend/components/dashboard/HeaderBar.tsx | 5 ++++ frontend/components/ops/OpsDashboard.tsx | 17 ++++++++++-- scripts/reconcile_payment_tx.py | 6 ++++- web/routes.py | 26 ++++++++++++++++++- 5 files changed, 57 insertions(+), 6 deletions(-) diff --git a/frontend/components/account/AccountCenter.tsx b/frontend/components/account/AccountCenter.tsx index fd2a0dfd..3a380913 100644 --- a/frontend/components/account/AccountCenter.tsx +++ b/frontend/components/account/AccountCenter.tsx @@ -1501,8 +1501,13 @@ export function AccountCenter() { ? expiryFormatted : displayExpiryRaw || copy.proPendingSync : copy.noProSubscription; - const showExpiringSoon = - Boolean(isSubscribed && expiryInfo && !expiryInfo.expired && expiryInfo.daysLeft <= 3); + const showExpiringSoon = Boolean( + isSubscribed && + !hasQueuedExtension && + expiryInfo && + !expiryInfo.expired && + expiryInfo.daysLeft <= 3, + ); const showExpiredReminder = Boolean(!isSubscribed && expiryInfo && expiryInfo.expired); const paymentFeatureReady = paymentReadyForRecovery; const canOpenCheckoutOverlay = Boolean( diff --git a/frontend/components/dashboard/HeaderBar.tsx b/frontend/components/dashboard/HeaderBar.tsx index 9e3e92d3..60e34c8d 100644 --- a/frontend/components/dashboard/HeaderBar.tsx +++ b/frontend/components/dashboard/HeaderBar.tsx @@ -47,12 +47,17 @@ export function HeaderBar() { store.proAccess.subscriptionExpiresAt : store.proAccess.subscriptionExpiresAt; const expiryInfo = parseExpiryInfo(effectiveExpiry); + const hasQueuedExtension = Boolean( + store.proAccess.subscriptionActive && + store.proAccess.subscriptionQueuedDays > 0, + ); const isTrialPlan = /trial/i.test( String(store.proAccess.subscriptionPlanCode || ""), ); const showRenewReminder = isAuthenticated && !store.proAccess.loading && + !hasQueuedExtension && ((store.proAccess.subscriptionActive && expiryInfo && expiryInfo.daysLeft <= 3) || diff --git a/frontend/components/ops/OpsDashboard.tsx b/frontend/components/ops/OpsDashboard.tsx index 30d23432..3910eabd 100644 --- a/frontend/components/ops/OpsDashboard.tsx +++ b/frontend/components/ops/OpsDashboard.tsx @@ -286,7 +286,11 @@ type MembershipEntry = { registered_at?: string | null; plan_code?: string | null; starts_at?: string | null; + current_expires_at?: string | null; + total_expires_at?: string | null; expires_at?: string | null; + queued_days?: number | null; + queued_count?: number | null; }; function formatDateTime(value?: string | null) { @@ -301,6 +305,15 @@ function formatUnixDateTime(value?: number | null) { return formatDateTime(new Date(value * 1000).toISOString()); } +function formatMembershipExpiry(item: MembershipEntry) { + const total = item.total_expires_at || item.expires_at; + const current = item.current_expires_at || item.expires_at; + const queuedDays = Math.max(0, Number(item.queued_days || 0)); + const totalLabel = formatDateTime(total); + if (!queuedDays || !current || current === total) return totalLabel; + return `${totalLabel}(已续 +${queuedDays} 天)`; +} + function formatMetric(value?: number | null, digits = 3) { if (value === null || value === undefined || Number.isNaN(value)) return "-"; return Number(value).toFixed(digits); @@ -1535,7 +1548,7 @@ export function OpsDashboard() {
- +
))} @@ -1563,7 +1576,7 @@ export function OpsDashboard() { {item.username || "-"} {item.user_id || "-"} {formatDateTime(item.registered_at)} - {formatDateTime(item.expires_at)} + {formatMembershipExpiry(item)} ))} {!memberships.length ? ( diff --git a/scripts/reconcile_payment_tx.py b/scripts/reconcile_payment_tx.py index 7fbba81b..0b8320ad 100644 --- a/scripts/reconcile_payment_tx.py +++ b/scripts/reconcile_payment_tx.py @@ -15,11 +15,16 @@ from __future__ import annotations import argparse import json +import os import sys from typing import Any, Dict, List, Optional from web3 import Web3 +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if PROJECT_ROOT not in sys.path: + sys.path.insert(0, PROJECT_ROOT) + from src.payments.contract_checkout import PAYMENT_CHECKOUT, PaymentCheckoutError @@ -202,4 +207,3 @@ def main() -> int: if __name__ == "__main__": sys.exit(main()) - diff --git a/web/routes.py b/web/routes.py index 5abdaf6a..e45173a1 100644 --- a/web/routes.py +++ b/web/routes.py @@ -1080,6 +1080,26 @@ async def ops_memberships(request: Request, limit: int = 200): user_id = str(item.get("user_id") or "").strip().lower() local_user = user_map.get(user_id, {}) auth_user = auth_user_map.get(user_id, {}) + subscription_window = SUPABASE_ENTITLEMENT.get_subscription_window( + user_id, + respect_requirement=False, + ) + current_expires_at = item.get("expires_at") + total_expires_at = ( + subscription_window.get("total_expires_at") + if isinstance(subscription_window, dict) + else None + ) + queued_days = ( + int(subscription_window.get("queued_days") or 0) + if isinstance(subscription_window, dict) + else 0 + ) + queued_count = ( + int(subscription_window.get("queued_count") or 0) + if isinstance(subscription_window, dict) + else 0 + ) row = { "user_id": user_id, "email": str(auth_user.get("email") or local_user.get("supabase_email") or ""), @@ -1088,7 +1108,11 @@ async def ops_memberships(request: Request, limit: int = 200): "registered_at": local_user.get("created_at") or auth_user.get("created_at"), "plan_code": item.get("plan_code"), "starts_at": item.get("starts_at"), - "expires_at": item.get("expires_at"), + "current_expires_at": current_expires_at, + "total_expires_at": total_expires_at or current_expires_at, + "expires_at": total_expires_at or current_expires_at, + "queued_days": queued_days, + "queued_count": queued_count, } existing = deduped.get(user_id) existing_expires = str(existing.get("expires_at") or "") if existing else ""