Optimize terminal detail loading and Redis replay

This commit is contained in:
2569718930@qq.com
2026-05-31 04:32:48 +08:00
parent 2a6d8748f4
commit 46effcd45f
16 changed files with 948 additions and 80 deletions
+14 -1
View File
@@ -99,8 +99,21 @@ def add_signal(
def parse_utc_datetime(value: Any) -> Optional[datetime]:
raw = str(value or "").strip()
if not raw or "T" not in raw:
if not raw:
return None
if "T" not in raw:
try:
epoch = float(raw)
except Exception:
return None
if epoch <= 1_000_000_000:
return None
if epoch > 10_000_000_000:
epoch = epoch / 1000.0
try:
return datetime.fromtimestamp(epoch, tz=timezone.utc)
except Exception:
return None
try:
dt = datetime.fromisoformat(raw.replace("Z", "+00:00"))
except Exception:
+93 -1
View File
@@ -3,9 +3,10 @@
from __future__ import annotations
import os
import asyncio
import threading
import time
from typing import Any, Dict, Optional
from typing import Any, Dict, List, Optional, Tuple
from fastapi import HTTPException, Request
from fastapi.concurrency import run_in_threadpool
@@ -253,4 +254,95 @@ async def get_city_detail_aggregate_payload(
)
def _parse_batch_city_names(raw_cities: str, *, limit: int) -> List[str]:
seen = set()
out: List[str] = []
for item in str(raw_cities or "").split(","):
raw = item.strip()
if not raw:
continue
city = legacy_routes._normalize_city_or_404(raw)
if city in seen:
continue
seen.add(city)
out.append(city)
if len(out) >= limit:
break
return out
def _build_city_detail_batch_item(
city: str,
*,
force_refresh: bool,
market_slug: Optional[str],
target_date: Optional[str],
resolution: Optional[str],
) -> Tuple[str, Dict[str, Any]]:
if force_refresh:
data = legacy_routes._refresh_city_full_cache(city, True)
else:
cached_entry = legacy_routes._CACHE_DB.get_city_cache("full", city)
if cached_entry and legacy_routes._city_cache_is_fresh(
cached_entry,
legacy_routes.CITY_FULL_CACHE_TTL_SEC,
):
data = legacy_routes._overlay_latest_wunderground_current(
city,
cached_entry.get("payload") or {},
)
else:
data = legacy_routes._refresh_city_full_cache(city, False)
detail = legacy_routes._build_city_detail_payload(
data,
market_slug,
target_date,
resolution,
)
return city, detail
async def get_city_detail_batch_payload(
request: Request,
*,
cities: str,
force_refresh: bool = False,
market_slug: Optional[str] = None,
target_date: Optional[str] = None,
resolution: Optional[str] = "10m",
limit: int = 12,
) -> Dict[str, Any]:
legacy_routes._assert_entitlement(request)
city_names = _parse_batch_city_names(cities, limit=max(1, min(24, int(limit or 12))))
if not city_names:
return {"cities": [], "details": {}, "errors": {}}
tasks = [
run_in_threadpool(
_build_city_detail_batch_item,
city,
force_refresh=force_refresh,
market_slug=market_slug,
target_date=target_date,
resolution=resolution,
)
for city in city_names
]
results = await asyncio.gather(*tasks, return_exceptions=True)
details: Dict[str, Any] = {}
errors: Dict[str, str] = {}
for city, result in zip(city_names, results):
if isinstance(result, Exception):
errors[city] = str(result)
continue
result_city, payload = result
details[result_city] = payload
return {
"cities": city_names,
"details": details,
"errors": errors,
}
+158 -19
View File
@@ -380,6 +380,51 @@ def get_ops_memberships_overview(
}
def _normalize_payment_incident(item: Dict[str, Any]) -> Dict[str, Any]:
payload = item.get("payload") if isinstance(item, dict) else {}
payload = payload if isinstance(payload, dict) else {}
confirm_failure = (
payload.get("confirm_failure")
if isinstance(payload.get("confirm_failure"), dict)
else {}
)
reason = str(
payload.get("reason")
or confirm_failure.get("reason")
or payload.get("error")
or "unknown"
).strip().lower()
detail = str(
payload.get("detail")
or confirm_failure.get("detail")
or payload.get("message")
or payload.get("error")
or ""
).strip()
resolved_at = str(payload.get("resolved_at") or "").strip()
return {
**item,
"payload": payload,
"reason": reason or "unknown",
"detail": detail,
"intent_id": str(
payload.get("intent_id")
or payload.get("payment_intent_id")
or confirm_failure.get("intent_id")
or ""
).strip(),
"user_id": str(payload.get("user_id") or "").strip(),
"tx_hash": str(
payload.get("tx_hash")
or confirm_failure.get("tx_hash")
or ""
).strip(),
"resolved": bool(resolved_at),
"resolved_at": resolved_at,
"resolved_by": str(payload.get("resolved_by") or "").strip(),
}
def list_ops_payment_incidents(
request: Request,
limit: int = 50,
@@ -395,16 +440,15 @@ def list_ops_payment_incidents(
normalized_reason = str(reason or "").strip().lower()
filtered = []
for item in incidents:
payload = item.get("payload") if isinstance(item, dict) else {}
payload = payload if isinstance(payload, dict) else {}
item_reason = str(payload.get("reason") or "").strip().lower()
resolved_at = str(payload.get("resolved_at") or "").strip()
normalized_item = _normalize_payment_incident(item)
item_reason = str(normalized_item.get("reason") or "").strip().lower()
resolved = bool(normalized_item.get("resolved"))
if normalized_reason and item_reason != normalized_reason:
continue
if not include_resolved and resolved_at:
if not include_resolved and resolved:
continue
filtered.append(item)
return {"incidents": filtered}
filtered.append(normalized_item)
return {"incidents": filtered, "total": len(filtered)}
def resolve_ops_payment_incident(request: Request, event_id: int) -> Dict[str, Any]:
@@ -498,7 +542,28 @@ def get_ops_billing_risk(
{
"select": "id,user_id,email,telegram_user_id,claimed_at,created_at",
"order": "created_at.desc",
"limit": str(min(safe_limit, 80)),
"limit": str(max(safe_limit * 10, 500)),
},
)
subscription_rows = collect(
"subscriptions",
{
"select": (
"id,user_id,plan_code,source,status,starts_at,expires_at,"
"created_at,updated_at"
),
"or": "(source.eq.signup_trial,plan_code.eq.signup_trial_3d,status.eq.active)",
"order": "created_at.desc",
"limit": str(max(safe_limit * 20, 1000)),
},
)
entitlement_trial_events = collect(
"entitlement_events",
{
"select": "id,user_id,action,payload,created_at",
"action": "in.(signup_trial_claimed,signup_trial_granted)",
"order": "created_at.desc",
"limit": str(max(safe_limit * 10, 500)),
},
)
@@ -683,22 +748,58 @@ def get_ops_billing_risk(
if str(row.get("event_type") or "").strip().lower()
in {"signup_success", "signup_completed"}
]
def normalize_user_key(value: Any) -> str:
return str(value or "").strip().lower()
trial_actor_keys = {
_app_analytics_actor_key(row)
for row in events
if str(row.get("event_type") or "").strip().lower() == "trial_created"
}
trial_gaps: List[Dict[str, Any]] = []
for row in signup_rows[:300]:
actor_key = _app_analytics_actor_key(row)
if actor_key in trial_actor_keys:
continue
subscription_user_keys = {
normalize_user_key(row.get("user_id"))
for row in subscription_rows
if normalize_user_key(row.get("user_id"))
}
trial_subscription_user_keys = {
normalize_user_key(row.get("user_id"))
for row in subscription_rows
if normalize_user_key(row.get("user_id"))
and (
str(row.get("plan_code") or "").strip().lower() == "signup_trial_3d"
or str(row.get("source") or "").strip().lower() == "signup_trial"
)
}
trial_claim_user_keys = {
normalize_user_key(row.get("user_id"))
for row in trial_claims
if normalize_user_key(row.get("user_id"))
}
trial_event_user_keys: set[str] = set()
for row in entitlement_trial_events:
event_user_id = normalize_user_key(row.get("user_id"))
payload = row.get("payload") if isinstance(row.get("payload"), dict) else {}
payload_user_id = normalize_user_key(payload.get("user_id"))
if event_user_id:
trial_event_user_keys.add(event_user_id)
if payload_user_id:
trial_event_user_keys.add(payload_user_id)
backend_trial_user_keys = (
trial_subscription_user_keys | trial_claim_user_keys | trial_event_user_keys
)
trial_gaps: List[Dict[str, Any]] = []
for claim in trial_claims:
claim_user_id = normalize_user_key(claim.get("user_id"))
if not claim_user_id or claim_user_id in trial_subscription_user_keys:
continue
gap = {
"event_id": row.get("id"),
"actor_key": actor_key,
"user_id": row.get("user_id") or payload.get("user_id"),
"created_at": row.get("created_at"),
"claim_id": claim.get("id"),
"user_id": claim.get("user_id"),
"email": claim.get("email"),
"created_at": claim.get("created_at") or claim.get("claimed_at"),
"reason": "trial_claim_without_subscription",
}
trial_gaps.append(gap)
if len(trial_gaps) <= 20:
@@ -706,8 +807,46 @@ def get_ops_billing_risk(
_risk_issue(
category="signup_trial",
severity="high",
title="注册成功后未记录试用开通",
detail="该用户进入 signup_success,但同窗口内没有 trial_created 事件",
title="试用 claim 已写入但订阅缺失",
detail="trial_claims 已记录该用户领取试用,但 subscriptions 中没有 signup_trial_3d 记录",
user_id=gap.get("user_id"),
created_at=gap.get("created_at"),
reference=str(gap.get("claim_id") or ""),
payload=gap,
)
)
for row in signup_rows[:300]:
actor_key = _app_analytics_actor_key(row)
if actor_key in trial_actor_keys:
continue
payload = row.get("payload") if isinstance(row.get("payload"), dict) else {}
signup_user_id = normalize_user_key(row.get("user_id") or payload.get("user_id"))
if not signup_user_id:
continue
if (
signup_user_id in backend_trial_user_keys
or signup_user_id in subscription_user_keys
):
continue
gap = {
"event_id": row.get("id"),
"actor_key": actor_key,
"user_id": signup_user_id,
"created_at": row.get("created_at"),
"reason": "signup_without_backend_trial_evidence",
}
trial_gaps.append(gap)
if len(trial_gaps) <= 20:
issues.append(
_risk_issue(
category="signup_trial",
severity="high",
title="注册成功后未发现后端试用记录",
detail=(
"该用户进入 signup_success,但没有 trial_created、trial_claims、"
"signup_trial subscription 或其他有效订阅证据。"
),
user_id=gap.get("user_id"),
created_at=gap.get("created_at"),
reference=str(gap.get("event_id") or ""),