Compare commits

..

2 Commits

Author SHA1 Message Date
wuqi 1176a7a48b Merge remote-tracking branch 'origin/main' into feat/ad-reward/wysiwyg-display-coin 2026-06-27 23:15:47 +08:00
wuqi 2e38d4f340 feat: 信息流广告改"所见即所得"发奖——直接发客户端小球显示金币
原规则"看满 10 秒(1 份时长)才发整份、不逐份累加",客户端小球显示与
实发口径可能对不上。用户 2026-06-27 拍板「显示多少给多少」。

- schemas/ad.py:FeedRewardIn 新增 display_coin(客户端小球本条显示金币,缺省 0)。
- repositories/ad_feed_reward.py:grant_feed_reward 优先直接发 display_coin,
  钳到本条"1 份满额"(unit_cap = calculate_ad_reward_coin(ecpm, existing_ads+1))
  防刷——合法显示(实际因子2 × 进度 p ≤ 1 份)不被砍,只挡伪造天价值。
- 因子2(LT)改由客户端按 granted 行 COUNT 计入 display_coin,后端只记 granted
  行让计数自增,不再服务端重算份值。
- 旧客户端不传 display_coin → 退回"看满 1 份发整份"兼容,不断币。
- display_coin 为 0 且不足一份 → too_short 不发(不计 LT / 当日上限);
  closed_early / capped 留痕不变。
- api/v1/ad.py:feed_reward 端点透传 display_coin。
2026-06-27 23:12:03 +08:00
4 changed files with 8 additions and 48 deletions
+4 -5
View File
@@ -26,7 +26,6 @@ import httpx
from fastapi import APIRouter, HTTPException, Request, status
from app.core.config import settings
from app.core.pricebot_client import get_pricebot_client
from app.core.pricebot_router import pick_pricebot
logger = logging.getLogger("shagua.compare")
@@ -66,10 +65,10 @@ async def _passthrough(request: Request, upstream_path: str) -> dict[str, Any]:
)
try:
client = get_pricebot_client()
resp = await client.post(
url, content=raw, headers={"Content-Type": "application/json"}, timeout=timeout
)
async with httpx.AsyncClient(timeout=timeout) as client:
resp = await client.post(
url, content=raw, headers={"Content-Type": "application/json"}
)
except httpx.RequestError as e:
logger.error("[pricebot] request failed: %s", e)
raise HTTPException(
+4 -5
View File
@@ -20,7 +20,6 @@ from fastapi.concurrency import run_in_threadpool
from app.api.deps import CurrentUser, DbSession
from app.core.config import settings
from app.core.pricebot_client import get_pricebot_client
from app.core.pricebot_router import pick_pricebot
from app.db.session import SessionLocal
from app.repositories import coupon_state as coupon_repo
@@ -142,10 +141,10 @@ async def coupon_step(
)
try:
client = get_pricebot_client()
resp = await client.post(
url, content=raw, headers={"Content-Type": "application/json"}, timeout=timeout
)
async with httpx.AsyncClient(timeout=timeout) as client:
resp = await client.post(
url, content=raw, headers={"Content-Type": "application/json"}
)
except httpx.RequestError as e:
logger.error("[pricebot] request failed: %s", e)
raise HTTPException(
-35
View File
@@ -1,35 +0,0 @@
"""透传到 pricebot 的共享 httpx.AsyncClient 单例。
为什么不能每请求新建(coupon.py / compare.py 老写法 async with httpx.AsyncClient(...)):
① 每次构造都重建一套 SSL 上下文(httpx.create_ssl_context 加载 certifi CA),实测
~1s+/次;而 pricebot 是纯 http 透传,根本用不到 TLS → 纯浪费,且每帧重交一次。
② trust_env 默认 True 会读进程 HTTP_PROXY,把 http://localhost:8000 这条本地透传整个
塞进本机代理(如 Clash 7897),恒定再多几秒。
单例:启动只建一次(SSL/连接池一次性),keep-alive 复用 TCP,每帧降到个位数 ms。
trust_env=False:对齐 integrations/meituan.py 的既有约定,不被进程代理误导,直连 pricebot。
"""
from __future__ import annotations
import httpx
_client: httpx.AsyncClient | None = None
def get_pricebot_client() -> httpx.AsyncClient:
"""取透传单例。lifespan 启动会预热;未预热(如测试态)懒建兜底。
超时不在此固化(coupon 30s / compare 60s 不同),由调用点 client.post(timeout=...) 传。
懒建无 await,asyncio 单线程下不会有并发竞态。
"""
global _client
if _client is None:
_client = httpx.AsyncClient(trust_env=False)
return _client
async def aclose_pricebot_client() -> None:
"""lifespan 关停时调,优雅关连接池。"""
global _client
if _client is not None:
await _client.aclose()
_client = None
-3
View File
@@ -49,7 +49,6 @@ from app.core.daily_exchange_worker import (
stop_daily_exchange_worker,
)
from app.core.logging import setup_logging
from app.core.pricebot_client import aclose_pricebot_client, get_pricebot_client
from app.core.withdraw_reconcile_worker import (
start_withdraw_reconcile_worker,
stop_withdraw_reconcile_worker,
@@ -69,7 +68,6 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]:
settings.APP_DEBUG,
settings.DATABASE_URL.split("://", 1)[0],
)
get_pricebot_client() # 预热透传 client:把建 SSL 上下文的一次性成本付在启动,首个领券请求即热
reconcile_task = start_withdraw_reconcile_worker()
heartbeat_task = start_heartbeat_monitor()
daily_exchange_task = start_daily_exchange_worker()
@@ -79,7 +77,6 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]:
await stop_heartbeat_monitor(heartbeat_task)
await stop_withdraw_reconcile_worker(reconcile_task)
await stop_daily_exchange_worker(daily_exchange_task)
await aclose_pricebot_client()
logger.info("shutting down")