diff --git a/app/api/v1/device.py b/app/api/v1/device.py index 56a5a85..cf53930 100644 --- a/app/api/v1/device.py +++ b/app/api/v1/device.py @@ -14,18 +14,24 @@ import logging from fastapi import APIRouter from app.api.deps import CurrentUser, DbSession +from app.core import wx_poc_signal from app.repositories import device as device_repo from app.schemas.device import ( DeviceOut, DeviceRegisterRequest, HeartbeatRequest, + HeartbeatResponse, LivenessAckRequest, LivenessOut, OkResponse, + PendingCompare, ) logger = logging.getLogger("shagua.device") +# PoC:源平台代号 → Android 包名(前端 launch 用)。当前只用美团。 +_SOURCE_PACKAGES = {"meituan": "com.sankuai.meituan"} + router = APIRouter(prefix="/api/v1/device", tags=["device"]) @@ -52,12 +58,12 @@ def register_device( return DeviceOut.model_validate(device) -@router.post("/heartbeat", response_model=OkResponse, summary="上报心跳") +@router.post("/heartbeat", response_model=HeartbeatResponse, summary="上报心跳") def report_heartbeat( req: HeartbeatRequest, user: CurrentUser, db: DbSession, -) -> OkResponse: +) -> HeartbeatResponse: device_repo.touch_heartbeat( db, user_id=user.id, @@ -65,7 +71,16 @@ def report_heartbeat( accessibility_enabled=req.accessibility_enabled, registration_id=req.registration_id, ) - return OkResponse() + # PoC:该设备有"待比价"信号则带回(只对写死的测试设备生效, 取走即清、只弹一次) + source = wx_poc_signal.pop_pending(req.device_id) + pending = None + if source: + pending = PendingCompare( + source_platform=source, + source_package=_SOURCE_PACKAGES.get(source, ""), + ) + logger.info("heartbeat 下发比价信号 device=%s source=%s", req.device_id, source) + return HeartbeatResponse(pending_compare=pending) @router.get("/liveness", response_model=LivenessOut, summary="查询本机掉线告警(后置检测)") diff --git a/app/api/v1/wx_mp.py b/app/api/v1/wx_mp.py index f0a6d93..d9eadc2 100644 --- a/app/api/v1/wx_mp.py +++ b/app/api/v1/wx_mp.py @@ -19,6 +19,7 @@ import httpx from fastapi import APIRouter, Request from fastapi.responses import PlainTextResponse +from app.core import wx_poc_signal from app.core.config import settings from app.integrations import wx_mp_crypto @@ -85,6 +86,11 @@ async def _handle_message(root: ET.Element) -> None: logger.info( "wx_mp 收到图片: openid=%s media_id=%s pic_url=%s", openid, media_id, pic_url ) + # PoC: 写死"该测试设备要从美团比价", 下次心跳带回前端弹选平台窗 + poc_dev = settings.WX_POC_TEST_DEVICE_ID + if poc_dev: + wx_poc_signal.set_pending(poc_dev, "meituan") + logger.info("wx_mp PoC: 已给测试设备 %s 打比价信号 source=meituan", poc_dev) await _download(pic_url, openid, media_id) else: logger.info("wx_mp 收到消息(暂忽略): openid=%s type=%s", openid, msg_type) diff --git a/app/core/config.py b/app/core/config.py index d9e4f1e..88777ab 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -142,6 +142,11 @@ class Settings(BaseSettings): WX_MP_TOKEN: str = "" # 公众平台"服务器配置"的 Token(URL 验签 + 消息验签) WX_MP_AES_KEY: str = "" # EncodingAESKey(43 位, 消息 AES-256-CBC 加解密) + # ===== 微信截图比价 PoC(临时验证链路, 验证后删) ===== + # 只对这台写死的测试设备下发"从美团比价"信号;空=不触发任何设备(部署上线零风险)。 + # 收图 → 给此 device_id 打 pending → 该设备下次心跳带回 → 前端弹选平台窗。 + WX_POC_TEST_DEVICE_ID: str = "" + @property def wx_mp_configured(self) -> bool: """服务号网页授权凭证齐全(缺则落地页不发起授权,降级为无 openid)。""" diff --git a/app/core/wx_poc_signal.py b/app/core/wx_poc_signal.py new file mode 100644 index 0000000..1bcefd7 --- /dev/null +++ b/app/core/wx_poc_signal.py @@ -0,0 +1,27 @@ +"""微信截图比价 PoC:进程内存的"待比价"信号(单 worker 够用, 重启即失效, PoC 可接受)。 + +收图端点 set_pending(device_id, source) → 该设备下次心跳 pop_pending 取走并清除 → +心跳响应带回 → 前端弹选平台窗。只对 settings.WX_POC_TEST_DEVICE_ID 写入, 不碰其他设备。 +后续接真识别 / openid↔device 绑定时整体替换本模块。 +""" +from __future__ import annotations + +import threading + +_lock = threading.Lock() +_pending: dict[str, str] = {} # device_id -> source_platform(如 "meituan") + + +def set_pending(device_id: str, source_platform: str) -> None: + if not device_id: + return + with _lock: + _pending[device_id] = source_platform + + +def pop_pending(device_id: str) -> str | None: + """取出并清除该设备的待比价信号;无则 None。心跳每帧调, 取到即消费(只弹一次)。""" + if not device_id: + return None + with _lock: + return _pending.pop(device_id, None) diff --git a/app/schemas/device.py b/app/schemas/device.py index dd7a7c4..8f8bd4b 100644 --- a/app/schemas/device.py +++ b/app/schemas/device.py @@ -36,6 +36,20 @@ class OkResponse(BaseModel): ok: bool = True +class PendingCompare(BaseModel): + """PoC:后端通过心跳下发的"用户要从某源平台比价"信号。前端据此弹选平台窗 + launch 源平台。""" + source_platform: str # pricebot 源平台代号(PoC 写死 "meituan") + source_package: str # 源平台 Android 包名(前端 launch 用) + + +class HeartbeatResponse(BaseModel): + """心跳响应。常规只回 ok;PoC 期带回 pending_compare 触发比价(exclude_none 省略 null)。""" + model_config = ConfigDict() + + ok: bool = True + pending_compare: PendingCompare | None = None + + class LivenessOut(BaseModel): """本机掉线告警状态(后置检测 pull 版)。客户端只需这一个布尔判断要不要弹「开启自启动」引导, 故只返回 kill_alert_pending(不暴露设备详情 / 内部 liveness_state 等)。从未注册过 → 默认 False(无告警)。"""