fix: complete remaining 8 optimization items

- HTTP connection pooling: UpstreamClient & WebsiteClient reuse httpx.Client
- Deduplicate decimal_string into shared app/utils/number.py
- Split scheduler transaction: snapshot write → webhook/website sync in separate sessions
- Remove hardcoded 170.106.100.210 migration from database.py
- Reset consecutive_failures on upstream update
- Healthcheck: install curl, replace python -c with curl -f
- Add .dockerignore to reduce build context
- Frontend: add axios-retry with exponential backoff (5xx/network errors only)
This commit is contained in:
SmartUp Developer
2026-05-17 11:09:35 +08:00
parent ad16618406
commit 8a6ed249be
12 changed files with 211 additions and 89 deletions
+25 -34
View File
@@ -2,11 +2,12 @@
from __future__ import annotations
import json
from decimal import Decimal, InvalidOperation
from typing import Any, Optional
from urllib.parse import urljoin
import httpx
from app.utils.number import decimal_string
class UpstreamError(RuntimeError):
pass
@@ -66,19 +67,6 @@ def _unwrap_list(value: Any) -> Optional[list[dict[str, Any]]]:
return None
def _decimal_str(value: Any) -> str:
if value is None or value == "":
return ""
try:
d = Decimal(str(value))
except (InvalidOperation, ValueError):
return str(value)
n = d.normalize()
if n == n.to_integral():
return str(n.quantize(Decimal("1")))
return format(n, "f")
def _group_id(group: dict[str, Any]) -> str:
for key in ("id", "group_id", "groupId"):
v = group.get(key)
@@ -117,7 +105,7 @@ def _extract_rates_map(raw: Any) -> dict[str, str]:
if isinstance(parsed, dict):
result: dict[str, str] = {}
for k, v in parsed.items():
r = _decimal_str(v)
r = decimal_string(v)
if r:
result[str(k)] = r
return result
@@ -127,7 +115,7 @@ def _extract_rates_map(raw: Any) -> dict[str, str]:
# In case it's returned as dict directly
result = {}
for k, v in val.items():
r = _decimal_str(v)
r = decimal_string(v)
if r:
result[str(k)] = r
return result
@@ -153,13 +141,13 @@ def _extract_rates_map(raw: Any) -> dict[str, str]:
result: dict[str, str] = {}
for k, v in candidates.items():
if isinstance(v, dict):
r = _decimal_str(
r = decimal_string(
v.get("rate_multiplier") or v.get("rateMultiplier")
or v.get("user_rate_multiplier") or v.get("userRateMultiplier")
or v.get("ratio")
)
else:
r = _decimal_str(v)
r = decimal_string(v)
if r:
result[str(k)] = r
return result
@@ -221,6 +209,10 @@ class UpstreamClient:
self._token: str = ""
self._cookies: dict[str, str] = {}
self._new_api_user: str = ""
self._client = httpx.Client(timeout=timeout)
def close(self) -> None:
self._client.close()
def _url(self, path: str) -> str:
prefix = f"/{self.api_prefix}" if self.api_prefix else ""
@@ -250,22 +242,21 @@ class UpstreamClient:
def _request(self, method: str, path: str, body: Any = None, auth: bool = True) -> Any:
url = self._url(path)
with httpx.Client(timeout=self.timeout) as client:
if body is not None:
resp = client.request(
method,
url,
json=body,
headers=self._headers(auth),
cookies=self._cookies,
)
else:
resp = client.request(
method,
url,
headers=self._headers(auth),
cookies=self._cookies,
)
if body is not None:
resp = self._client.request(
method,
url,
json=body,
headers=self._headers(auth),
cookies=self._cookies,
)
else:
resp = self._client.request(
method,
url,
headers=self._headers(auth),
cookies=self._cookies,
)
self._cookies.update(dict(resp.cookies))
resp.raise_for_status()
ct = resp.headers.get("content-type", "")