"""领券业务透传端点。 把客户端的 POST /api/v1/coupon/step 请求原样转发给 pricebot-backend 的 /api/coupon/step。MVP 阶段**不鉴权**(见 docs/待办与技术债.md P1: device_id 透传给 pricebot 区分设备,待补 JWT + device_id↔user_id 绑定后 才能做用户级画像)。 pricebot 协议文档: pricebot-backend/docs/projects/领券-客户端对接协议.md """ from __future__ import annotations import json import logging from typing import Any import httpx from fastapi import APIRouter, HTTPException, Request, status from app.core.config import settings from app.core.pricebot_router import pick_pricebot logger = logging.getLogger("shagua.coupon") router = APIRouter(prefix="/api/v1/coupon", tags=["coupon"]) @router.post("/step", summary="领券任务步进 (透传到 pricebot)") async def coupon_step( request: Request, ) -> dict[str, Any]: """把请求体原样转发给 pricebot-backend 的 /api/coupon/step。 - 鉴权: MVP 阶段不鉴权(待补 JWT,见 docs/待办与技术债.md P1) - 透传: 不做 schema 校验,pricebot 自己校验 - 失败: 网络不可达 / pricebot 5xx → 502 + 友好 message """ # 读原始字节,避免"反序列化→再序列化"的双重 JSON(省透传 CPU)。只 loads 一次拿 # trace_id 做亲和 + 打日志,转发时直接发原始 bytes,不重新 dumps。 raw = await request.body() try: meta = json.loads(raw) except Exception as e: raise HTTPException(status_code=400, detail=f"invalid json body: {e}") from e if not isinstance(meta, dict): meta = {} # 按 trace_id 一致性 hash 选 pricebot 实例(同一领券任务的所有帧落同一进程) base = pick_pricebot(meta.get("trace_id")) url = f"{base.rstrip('/')}/api/coupon/step" timeout = settings.PRICEBOT_REQUEST_TIMEOUT_SEC logger.info( "coupon_step device_id=%s trace_id=%s step=%s", meta.get("device_id"), meta.get("trace_id"), meta.get("step"), ) try: 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( status_code=status.HTTP_502_BAD_GATEWAY, detail=f"pricebot upstream unreachable: {e}", ) from e if resp.status_code >= 500: logger.error( "[pricebot] 5xx status=%d body=%s", resp.status_code, resp.text[:500], ) raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail=f"pricebot upstream returned {resp.status_code}", ) return resp.json()