feat: implement backend routing, dashboard components, and transaction reconciliation scripts

This commit is contained in:
2569718930@qq.com
2026-04-19 20:14:07 +08:00
parent 1883f96034
commit 6914e6277b
5 changed files with 57 additions and 6 deletions
@@ -1501,8 +1501,13 @@ export function AccountCenter() {
? expiryFormatted ? expiryFormatted
: displayExpiryRaw || copy.proPendingSync : displayExpiryRaw || copy.proPendingSync
: copy.noProSubscription; : copy.noProSubscription;
const showExpiringSoon = const showExpiringSoon = Boolean(
Boolean(isSubscribed && expiryInfo && !expiryInfo.expired && expiryInfo.daysLeft <= 3); isSubscribed &&
!hasQueuedExtension &&
expiryInfo &&
!expiryInfo.expired &&
expiryInfo.daysLeft <= 3,
);
const showExpiredReminder = Boolean(!isSubscribed && expiryInfo && expiryInfo.expired); const showExpiredReminder = Boolean(!isSubscribed && expiryInfo && expiryInfo.expired);
const paymentFeatureReady = paymentReadyForRecovery; const paymentFeatureReady = paymentReadyForRecovery;
const canOpenCheckoutOverlay = Boolean( const canOpenCheckoutOverlay = Boolean(
@@ -47,12 +47,17 @@ export function HeaderBar() {
store.proAccess.subscriptionExpiresAt store.proAccess.subscriptionExpiresAt
: store.proAccess.subscriptionExpiresAt; : store.proAccess.subscriptionExpiresAt;
const expiryInfo = parseExpiryInfo(effectiveExpiry); const expiryInfo = parseExpiryInfo(effectiveExpiry);
const hasQueuedExtension = Boolean(
store.proAccess.subscriptionActive &&
store.proAccess.subscriptionQueuedDays > 0,
);
const isTrialPlan = /trial/i.test( const isTrialPlan = /trial/i.test(
String(store.proAccess.subscriptionPlanCode || ""), String(store.proAccess.subscriptionPlanCode || ""),
); );
const showRenewReminder = const showRenewReminder =
isAuthenticated && isAuthenticated &&
!store.proAccess.loading && !store.proAccess.loading &&
!hasQueuedExtension &&
((store.proAccess.subscriptionActive && ((store.proAccess.subscriptionActive &&
expiryInfo && expiryInfo &&
expiryInfo.daysLeft <= 3) || expiryInfo.daysLeft <= 3) ||
+15 -2
View File
@@ -286,7 +286,11 @@ type MembershipEntry = {
registered_at?: string | null; registered_at?: string | null;
plan_code?: string | null; plan_code?: string | null;
starts_at?: string | null; starts_at?: string | null;
current_expires_at?: string | null;
total_expires_at?: string | null;
expires_at?: string | null; expires_at?: string | null;
queued_days?: number | null;
queued_count?: number | null;
}; };
function formatDateTime(value?: string | null) { function formatDateTime(value?: string | null) {
@@ -301,6 +305,15 @@ function formatUnixDateTime(value?: number | null) {
return formatDateTime(new Date(value * 1000).toISOString()); 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) { function formatMetric(value?: number | null, digits = 3) {
if (value === null || value === undefined || Number.isNaN(value)) return "-"; if (value === null || value === undefined || Number.isNaN(value)) return "-";
return Number(value).toFixed(digits); return Number(value).toFixed(digits);
@@ -1535,7 +1548,7 @@ export function OpsDashboard() {
<div className="mt-3 grid gap-2"> <div className="mt-3 grid gap-2">
<MobileField label="User ID" value={item.user_id || "-"} mono /> <MobileField label="User ID" value={item.user_id || "-"} mono />
<MobileField label="注册时间" value={formatDateTime(item.registered_at)} /> <MobileField label="注册时间" value={formatDateTime(item.registered_at)} />
<MobileField label="到期时间" value={formatDateTime(item.expires_at)} /> <MobileField label="到期时间" value={formatMembershipExpiry(item)} />
</div> </div>
</div> </div>
))} ))}
@@ -1563,7 +1576,7 @@ export function OpsDashboard() {
<td className="px-4 py-3">{item.username || "-"}</td> <td className="px-4 py-3">{item.username || "-"}</td>
<td className="px-4 py-3 font-mono text-xs text-slate-300">{item.user_id || "-"}</td> <td className="px-4 py-3 font-mono text-xs text-slate-300">{item.user_id || "-"}</td>
<td className="px-4 py-3">{formatDateTime(item.registered_at)}</td> <td className="px-4 py-3">{formatDateTime(item.registered_at)}</td>
<td className="px-4 py-3">{formatDateTime(item.expires_at)}</td> <td className="px-4 py-3">{formatMembershipExpiry(item)}</td>
</tr> </tr>
))} ))}
{!memberships.length ? ( {!memberships.length ? (
+5 -1
View File
@@ -15,11 +15,16 @@ from __future__ import annotations
import argparse import argparse
import json import json
import os
import sys import sys
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from web3 import Web3 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 from src.payments.contract_checkout import PAYMENT_CHECKOUT, PaymentCheckoutError
@@ -202,4 +207,3 @@ def main() -> int:
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(main()) sys.exit(main())
+25 -1
View File
@@ -1080,6 +1080,26 @@ async def ops_memberships(request: Request, limit: int = 200):
user_id = str(item.get("user_id") or "").strip().lower() user_id = str(item.get("user_id") or "").strip().lower()
local_user = user_map.get(user_id, {}) local_user = user_map.get(user_id, {})
auth_user = auth_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 = { row = {
"user_id": user_id, "user_id": user_id,
"email": str(auth_user.get("email") or local_user.get("supabase_email") or ""), "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"), "registered_at": local_user.get("created_at") or auth_user.get("created_at"),
"plan_code": item.get("plan_code"), "plan_code": item.get("plan_code"),
"starts_at": item.get("starts_at"), "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 = deduped.get(user_id)
existing_expires = str(existing.get("expires_at") or "") if existing else "" existing_expires = str(existing.get("expires_at") or "") if existing else ""