From 66512d2623bccf1cd3b4bfdf2adb8dec23bd84a3 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Wed, 20 May 2026 16:50:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=90=8E=E5=8F=B0=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8A=9F=E8=83=BD=EF=BC=9A=E8=AE=A2=E9=98=85=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=A2=9E=E5=BC=BA=E3=80=81=E8=AE=AD=E7=BB=83=E7=B2=BE?= =?UTF-8?q?=E5=BA=A6=E9=9D=A2=E6=9D=BF=E3=80=81=E6=94=AF=E4=BB=98=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 4 +- .env.secrets.example | 2 +- .../ops/memberships/MembershipsPageClient.tsx | 148 +++++++++++++++++- .../ops/training/TrainingPageClient.tsx | 125 ++++++++++++++- frontend/lib/ops-api.ts | 21 +++ src/onchain/polygon_wallet_watcher.py | 5 +- web/routers/ops.py | 7 + web/services/ops_api.py | 53 +++++++ 8 files changed, 356 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index e1a06f27..e9f95c73 100644 --- a/.env.example +++ b/.env.example @@ -181,9 +181,9 @@ POLYWEATHER_PAYMENT_RPC_URL=https://polygon-rpc.com POLYWEATHER_PAYMENT_RPC_URLS=https://polygon-rpc.com POLYWEATHER_PAYMENT_RECEIVER_CONTRACT= POLYWEATHER_PAYMENT_DIRECT_RECEIVER_ADDRESS= -POLYWEATHER_PAYMENT_TOKEN_ADDRESS=0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 +POLYWEATHER_PAYMENT_TOKEN_ADDRESS=0x3c499c542cef5e3811e1192ce70d8cc03d5c3359 POLYWEATHER_PAYMENT_TOKEN_DECIMALS=6 -POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON=[{"code":"usdc_e","symbol":"USDC.e","name":"USDC.e (PoS)","address":"0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS","is_default":true},{"code":"usdc","symbol":"USDC","name":"Native USDC","address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS"}] +POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON=[{"code":"usdc","symbol":"USDC","name":"Native USDC","address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS","is_default":true},{"code":"usdt","symbol":"USDT","name":"USDT","address":"0xc2132d05d31c914a87c6611c10748aeb04b58e8f","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS"},{"code":"pusd","symbol":"pUSD","name":"Polymarket pUSD","address":"0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS"}] POLYWEATHER_PAYMENT_CONFIRMATIONS=2 POLYWEATHER_PAYMENT_INTENT_TTL_SEC=1800 POLYWEATHER_PAYMENT_WALLET_CHALLENGE_TTL_SEC=600 diff --git a/.env.secrets.example b/.env.secrets.example index 2a1f2f8d..a9cbcaec 100644 --- a/.env.secrets.example +++ b/.env.secrets.example @@ -36,7 +36,7 @@ METEOBLUE_API_KEY= NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= POLYWEATHER_PAYMENT_RECEIVER_CONTRACT= POLYWEATHER_PAYMENT_DIRECT_RECEIVER_ADDRESS= -POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON=[{"code":"usdc_e","symbol":"USDC.e","name":"USDC.e (PoS)","address":"0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS","is_default":true},{"code":"usdc","symbol":"USDC","name":"Native USDC","address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS"}] +POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON=[{"code":"usdc","symbol":"USDC","name":"Native USDC","address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS","is_default":true},{"code":"usdt","symbol":"USDT","name":"USDT","address":"0xc2132d05d31c914a87c6611c10748aeb04b58e8f","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS"},{"code":"pusd","symbol":"pUSD","name":"Polymarket pUSD","address":"0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS"}] POLYWEATHER_PAYMENT_PLAN_CATALOG_JSON= ######################################## diff --git a/frontend/components/ops/memberships/MembershipsPageClient.tsx b/frontend/components/ops/memberships/MembershipsPageClient.tsx index 8f934a4d..299e8534 100644 --- a/frontend/components/ops/memberships/MembershipsPageClient.tsx +++ b/frontend/components/ops/memberships/MembershipsPageClient.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useState } from "react"; -import { RefreshCcw } from "lucide-react"; +import { RefreshCcw, X, Search } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { opsApi } from "@/lib/ops-api"; @@ -13,12 +13,30 @@ import { type GrowthPoint = { date: string; trial: number; paid: number; total: number; cumulative: number }; +type SubRow = { + id?: string; + status?: string; + plan_code?: string; + source?: string; + starts_at?: string; + expires_at?: string; + created_at?: string; + updated_at?: string; +}; + export function MembershipsPageClient() { const [loading, setLoading] = useState(true); const [memberships, setMemberships] = useState([]); const [growth, setGrowth] = useState([]); const [filter, setFilter] = useState<"all" | "paid" | "trial">("all"); + // Detail modal state + const [detailEmail, setDetailEmail] = useState(null); + const [detailUserId, setDetailUserId] = useState(""); + const [detailRows, setDetailRows] = useState([]); + const [detailLoading, setDetailLoading] = useState(false); + const [detailError, setDetailError] = useState(""); + const load = async () => { setLoading(true); try { @@ -47,6 +65,46 @@ export function MembershipsPageClient() { return code; }; + const sourceLabel = (source?: string) => { + if (!source) return "—"; + if (source === "payment_contract") return "链上支付"; + if (source === "ops_manual_grant") return "后台赠送"; + if (source === "signup_trial") return "注册体验"; + if (source === "weekly_reward") return "周奖励"; + return source; + }; + + const statusBadge = (status?: string) => { + if (status === "active") return active; + if (status === "expired") return expired; + if (status === "cancelled") return cancelled; + return {status ?? "—"}; + }; + + const openDetail = async (email: string) => { + if (!email) return; + setDetailEmail(email); + setDetailLoading(true); + setDetailError(""); + setDetailRows([]); + setDetailUserId(""); + try { + const data = await opsApi.userSubscriptions(email); + setDetailUserId(data.user_id ?? ""); + setDetailRows(data.subscriptions ?? []); + } catch (e: unknown) { + setDetailError(e instanceof Error ? e.message : "查询失败"); + } + setDetailLoading(false); + }; + + const closeDetail = () => { + setDetailEmail(null); + setDetailRows([]); + setDetailUserId(""); + setDetailError(""); + }; + if (loading) return
加载中...
; return ( @@ -154,7 +212,7 @@ export function MembershipsPageClient() { {filtered.map((m, i) => ( - + {m.is_trial ? ( 体验 @@ -162,7 +220,16 @@ export function MembershipsPageClient() { 付费 )} - {m.email ?? "—"} + + + {planLabel(m.plan_code)} {m.starts_at?.slice(0, 10) ?? "—"} {m.expires_at?.slice(0, 10) ?? "—"} @@ -177,6 +244,81 @@ export function MembershipsPageClient() { + + {/* Subscription detail modal */} + {detailEmail && ( +
+
e.stopPropagation()} + > + {/* Header */} +
+
+

订阅记录详情

+

+ {detailEmail} + {detailUserId && ID: {detailUserId.slice(0, 8)}…} +

+
+ +
+ + {/* Content */} +
+ {detailLoading && ( +
查询中...
+ )} + {detailError && ( +
{detailError}
+ )} + {!detailLoading && !detailError && detailRows.length === 0 && ( +
未找到订阅记录
+ )} + {!detailLoading && detailRows.length > 0 && ( +
+ + + + + + + + + + + + + {detailRows.map((row, i) => { + const isExpired = row.expires_at && new Date(row.expires_at) < new Date(); + return ( + + + + + + + + + ); + })} + +
状态方案来源起始到期创建
{statusBadge(row.status)}{planLabel(row.plan_code)}{sourceLabel(row.source)}{row.starts_at?.slice(0, 19)?.replace("T", " ") ?? "—"} + + {row.expires_at?.slice(0, 19)?.replace("T", " ") ?? "—"} + + {row.created_at?.slice(0, 19)?.replace("T", " ") ?? "—"}
+
+ 共 {detailRows.length} 条记录 · 时间为 UTC +
+
+ )} +
+
+
+ )} ); } diff --git a/frontend/components/ops/training/TrainingPageClient.tsx b/frontend/components/ops/training/TrainingPageClient.tsx index ed77cfec..620143fc 100644 --- a/frontend/components/ops/training/TrainingPageClient.tsx +++ b/frontend/components/ops/training/TrainingPageClient.tsx @@ -18,15 +18,38 @@ function StatRow({ label, value }: { label: string; value: React.ReactNode }) { ); } +interface CityAccuracy { + city_id: string; + name: string; + deb?: { + hit_rate: number; + mae: number; + total_days: number; + details_str: string; + } | null; + mu?: { + mae: number; + hit_rate: number; + brier_score: number | null; + total_days: number; + details_str: string; + } | null; +} + export function TrainingPageClient() { const [loading, setLoading] = useState(true); const [status, setStatus] = useState(null); + const [accuracy, setAccuracy] = useState(null); const load = async () => { setLoading(true); try { - const s = await opsApi.systemStatus() as SystemStatusPayload; + const [s, accData] = await Promise.all([ + opsApi.systemStatus() as Promise, + opsApi.trainingAccuracy().catch(() => ({ accuracy: [] })) + ]); setStatus(s); + setAccuracy(accData.accuracy); } catch { /* */ } setLoading(false); }; @@ -110,6 +133,105 @@ export function TrainingPageClient() { ) : null} + + + 模型融合与预测准确率效果 (DEB & 概率引擎) + + +
+ + + + + + + + + + + + + + + {accuracy && accuracy.length > 0 ? ( + accuracy.map((row) => ( + + + + + + + + + + + )) + ) : ( + + + + )} + +
城市DEB 结算命中DEB MAEDEB 天数概率 μ 结算命中概率 μ MAEBrier Score概率天数
+ {row.name} + {row.city_id} + + {row.deb ? ( + = 80 + ? "bg-green-500/15 text-green-400" + : row.deb.hit_rate >= 60 + ? "bg-yellow-500/15 text-yellow-400" + : "bg-red-500/15 text-red-400" + }`}> + {row.deb.hit_rate.toFixed(0)}% + + ) : ( + + )} + + {row.deb ? `${row.deb.mae.toFixed(1)}°` : "—"} + + {row.deb ? row.deb.total_days : "—"} + + {row.mu ? ( + = 80 + ? "bg-green-500/15 text-green-400" + : row.mu.hit_rate >= 60 + ? "bg-yellow-500/15 text-yellow-400" + : "bg-red-500/15 text-red-400" + }`}> + {row.mu.hit_rate.toFixed(0)}% + + ) : ( + + )} + + {row.mu ? `${row.mu.mae.toFixed(1)}°` : "—"} + + {row.mu && row.mu.brier_score !== null ? ( + + {row.mu.brier_score.toFixed(3)} + + ) : ( + "—" + )} + + {row.mu ? row.mu.total_days : "—"} +
+ 无有效准确率记录,请确认 daily_records.json 中已有历史结算数据。 +
+
+
+
+ 真值历史浏览 @@ -124,3 +246,4 @@ export function TrainingPageClient() { ); } + diff --git a/frontend/lib/ops-api.ts b/frontend/lib/ops-api.ts index 6649aca1..1ba3fdd7 100644 --- a/frontend/lib/ops-api.ts +++ b/frontend/lib/ops-api.ts @@ -105,4 +105,25 @@ export const opsApi = { count: number; }>(`/api/ops/subscriptions/user?email=${encodeURIComponent(email)}`); }, + trainingAccuracy() { + return opsFetch<{ + accuracy: Array<{ + city_id: string; + name: string; + deb?: { + hit_rate: number; + mae: number; + total_days: number; + details_str: string; + } | null; + mu?: { + mae: number; + hit_rate: number; + brier_score: number | null; + total_days: number; + details_str: string; + } | null; + }>; + }>("/api/ops/training/accuracy"); + }, }; diff --git a/src/onchain/polygon_wallet_watcher.py b/src/onchain/polygon_wallet_watcher.py index c948225e..fc58a916 100644 --- a/src/onchain/polygon_wallet_watcher.py +++ b/src/onchain/polygon_wallet_watcher.py @@ -1,4 +1,4 @@ -import json +import json import os import threading import time @@ -33,7 +33,8 @@ ERC20_ABI = [ # Source: Polymarket official developer docs (Polygon contract addresses) # https://docs.polymarket.com/developers/market-makers/setup DEFAULT_POLYMARKET_CONTRACTS: Dict[str, str] = { - "USDC.e": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + "USDC": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "pUSD": "0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb", "CTF": "0x4d97dcd97ec945f40cf65f87097ace5ea0476045", "CTF_EXCHANGE": "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E", "NEG_RISK_CTF_EXCHANGE": "0xC5d563A36AE78145C45a50134d48A1215220f80a", diff --git a/web/routers/ops.py b/web/routers/ops.py index a0ae9675..a1196088 100644 --- a/web/routers/ops.py +++ b/web/routers/ops.py @@ -21,6 +21,7 @@ from web.services.ops_api import ( list_ops_payments, search_ops_users, update_ops_config, + get_ops_training_accuracy, ) router = APIRouter(tags=["ops"]) @@ -160,3 +161,9 @@ async def ops_logs( @router.get("/api/ops/health-check") async def ops_health_check(request: Request): return get_ops_health_check(request) + + +@router.get("/api/ops/training/accuracy") +async def ops_training_accuracy(request: Request): + return get_ops_training_accuracy(request) + diff --git a/web/services/ops_api.py b/web/services/ops_api.py index e2aa3a5b..c42e739d 100644 --- a/web/services/ops_api.py +++ b/web/services/ops_api.py @@ -783,3 +783,56 @@ def get_ops_health_check(request: Request) -> dict[str, Any]: all_ok = all(v.get("ok") for v in results.values()) return {"ok": all_ok, "checked_at": __import__("datetime").datetime.utcnow().isoformat() + "Z", "services": results} + + +def get_ops_training_accuracy(request: Request) -> Dict[str, Any]: + _require_ops(request) + from src.analysis.deb_algorithm import get_deb_accuracy, get_mu_accuracy + from src.data_collection.city_registry import CITY_REGISTRY + + accuracy_data = [] + for city_id, info in CITY_REGISTRY.items(): + name = info.get("name") or city_id + + # Calculate DEB accuracy + deb_acc = get_deb_accuracy(city_id) + deb_payload = None + if deb_acc: + deb_payload = { + "hit_rate": deb_acc[0], + "mae": deb_acc[1], + "total_days": deb_acc[2], + "details_str": deb_acc[3] + } + + # Calculate Mu accuracy + mu_acc = get_mu_accuracy(city_id) + mu_payload = None + if mu_acc: + mu_payload = { + "mae": mu_acc[0], + "hit_rate": mu_acc[1], + "brier_score": mu_acc[2], + "total_days": mu_acc[3], + "details_str": mu_acc[4] + } + + if deb_payload or mu_payload: + accuracy_data.append({ + "city_id": city_id, + "name": name, + "deb": deb_payload, + "mu": mu_payload + }) + + # Sort by total days of DEB or Mu + accuracy_data.sort( + key=lambda x: max( + x["deb"]["total_days"] if x["deb"] else 0, + x["mu"]["total_days"] if x["mu"] else 0 + ), + reverse=True + ) + + return {"accuracy": accuracy_data} +