2236a8b3ee
改动:新增厂商推送配置、设备 push_vendor/push_token 字段、device push-test 接口、心跳超时厂商直推发送逻辑和对应测试。 验证:python -m pytest tests/test_device_push.py tests/test_auth.py tests/test_health.py 通过。
186 lines
5.8 KiB
Python
186 lines
5.8 KiB
Python
"""设备注册 / 心跳 endpoint(无障碍保护存活检测)。
|
|
|
|
路由前缀 /api/v1/device,需 Bearer 鉴权(设备绑登录用户)。
|
|
POST /register 注册设备 / 更新厂商 push token(App 前台、拿到 push token 时调)
|
|
POST /heartbeat 上报心跳(无障碍服务存活时周期调,刷新存活)
|
|
POST /push-test 开发验收:延迟发送厂商通道测试推送
|
|
|
|
后端 heartbeat_monitor_worker 据此发现心跳超时的设备并厂商直推告警。
|
|
见 spec: spec/accessibility-liveness-push.md。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
|
|
from fastapi import APIRouter, BackgroundTasks, HTTPException, status
|
|
|
|
from app.api.deps import CurrentUser, DbSession
|
|
from app.integrations import vendor_push
|
|
from app.repositories import device as device_repo
|
|
from app.schemas.device import (
|
|
DeviceOut,
|
|
DeviceRegisterRequest,
|
|
HeartbeatRequest,
|
|
LivenessAckRequest,
|
|
LivenessOut,
|
|
OkResponse,
|
|
PushTestOut,
|
|
PushTestRequest,
|
|
)
|
|
|
|
logger = logging.getLogger("shagua.device")
|
|
|
|
router = APIRouter(prefix="/api/v1/device", tags=["device"])
|
|
|
|
|
|
def _send_push_test_after_delay(
|
|
push_vendor: str,
|
|
push_token: str,
|
|
delay_seconds: int,
|
|
user_id: int,
|
|
device_id: str,
|
|
) -> None:
|
|
if delay_seconds > 0:
|
|
time.sleep(delay_seconds)
|
|
try:
|
|
vendor_push.send_accessibility_disabled(
|
|
push_vendor,
|
|
push_token,
|
|
title="测试推送",
|
|
alert="这是一条厂商通道测试推送。收到它说明 App 被划掉后仍可通过系统通知栏触达。",
|
|
)
|
|
logger.info(
|
|
"push test sent user_id=%d device_id=%s delay=%ds",
|
|
user_id,
|
|
device_id,
|
|
delay_seconds,
|
|
)
|
|
except vendor_push.VendorPushError as e:
|
|
logger.warning(
|
|
"push test failed user_id=%d device_id=%s error=%s",
|
|
user_id,
|
|
device_id,
|
|
e,
|
|
)
|
|
|
|
|
|
@router.post("/register", response_model=DeviceOut, summary="注册设备/更新推送token")
|
|
def register_device(
|
|
req: DeviceRegisterRequest,
|
|
user: CurrentUser,
|
|
db: DbSession,
|
|
) -> DeviceOut:
|
|
device = device_repo.register_or_update(
|
|
db,
|
|
user_id=user.id,
|
|
device_id=req.device_id,
|
|
registration_id=req.registration_id,
|
|
push_vendor=req.push_vendor,
|
|
push_token=req.push_token,
|
|
platform=req.platform,
|
|
app_version=req.app_version,
|
|
)
|
|
logger.info(
|
|
"device register user_id=%d device_id=%s vendor=%s token=%s legacy_reg=%s",
|
|
user.id,
|
|
req.device_id,
|
|
req.push_vendor,
|
|
bool(req.push_token),
|
|
bool(req.registration_id),
|
|
)
|
|
return DeviceOut.model_validate(device)
|
|
|
|
|
|
@router.post("/heartbeat", response_model=OkResponse, summary="上报心跳")
|
|
def report_heartbeat(
|
|
req: HeartbeatRequest,
|
|
user: CurrentUser,
|
|
db: DbSession,
|
|
) -> OkResponse:
|
|
device_repo.touch_heartbeat(
|
|
db,
|
|
user_id=user.id,
|
|
device_id=req.device_id,
|
|
accessibility_enabled=req.accessibility_enabled,
|
|
registration_id=req.registration_id,
|
|
push_vendor=req.push_vendor,
|
|
push_token=req.push_token,
|
|
)
|
|
return OkResponse()
|
|
|
|
|
|
@router.post("/push-test", response_model=PushTestOut, summary="延迟发送厂商通道测试推送")
|
|
def request_push_test(
|
|
req: PushTestRequest,
|
|
background_tasks: BackgroundTasks,
|
|
user: CurrentUser,
|
|
db: DbSession,
|
|
) -> PushTestOut:
|
|
"""开发验收用:App 内点一次,服务端延迟发厂商直推,验证离线通道。"""
|
|
push_vendor = req.push_vendor.strip() if req.push_vendor else None
|
|
push_token = req.push_token.strip() if req.push_token else None
|
|
if push_vendor and push_token:
|
|
device_repo.register_or_update(
|
|
db,
|
|
user_id=user.id,
|
|
device_id=req.device_id,
|
|
registration_id=req.registration_id,
|
|
push_vendor=push_vendor,
|
|
push_token=push_token,
|
|
)
|
|
else:
|
|
device = device_repo.get_device(db, user_id=user.id, device_id=req.device_id)
|
|
push_vendor = device.push_vendor if device is not None else None
|
|
push_token = device.push_token if device is not None else None
|
|
|
|
if not push_vendor or not push_token:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail="push vendor token not ready",
|
|
)
|
|
|
|
background_tasks.add_task(
|
|
_send_push_test_after_delay,
|
|
push_vendor,
|
|
push_token,
|
|
req.delay_seconds,
|
|
user.id,
|
|
req.device_id,
|
|
)
|
|
logger.info(
|
|
"push test scheduled user_id=%d device_id=%s delay=%ds",
|
|
user.id,
|
|
req.device_id,
|
|
req.delay_seconds,
|
|
)
|
|
return PushTestOut(delay_seconds=req.delay_seconds, has_push_token=True)
|
|
|
|
|
|
@router.get("/liveness", response_model=LivenessOut, summary="查询本机掉线告警(后置检测)")
|
|
def get_liveness(
|
|
device_id: str,
|
|
user: CurrentUser,
|
|
db: DbSession,
|
|
) -> LivenessOut:
|
|
"""客户端进 App 时拉取: 本机是否被判定掉线过(kill_alert_pending)。
|
|
|
|
设备从未注册过 → 返回默认(无告警)。命中 → 客户端弹「开启自启动」引导, 之后调 /liveness/ack 清。
|
|
见 spec: spec/accessibility-liveness-pull-prompt.md。
|
|
"""
|
|
device = device_repo.get_device(db, user_id=user.id, device_id=device_id)
|
|
if device is None:
|
|
return LivenessOut()
|
|
return LivenessOut.model_validate(device)
|
|
|
|
|
|
@router.post("/liveness/ack", response_model=OkResponse, summary="确认已提醒(清掉线告警)")
|
|
def ack_liveness(
|
|
req: LivenessAckRequest,
|
|
user: CurrentUser,
|
|
db: DbSession,
|
|
) -> OkResponse:
|
|
"""客户端已弹过「开启自启动」引导 → 清掉「待提醒」标记(下次真掉线 worker 再置)。"""
|
|
device_repo.ack_kill_alert(db, user_id=user.id, device_id=req.device_id)
|
|
return OkResponse()
|