Files
shaguabijia-app-server/app/integrations/jpush.py
T
陈世睿 8adad30ff2 feat(device): 无障碍保护存活心跳检测 + 掉线终端告警
- 新增 device 表 + /api/v1/device/{register,heartbeat} + 迁移 device_table
- heartbeat_monitor_worker 周期扫描心跳超时(App 被杀/无障碍停)→ 服务器终端打印告警
  (推送本期未接,先用 logger 终端打印代替真实通知;integrations/jpush.py 已备,后续直接替换)
- config / .env.example 增 JPUSH_* / HEARTBEAT_*
- 见 spec(仓库外 e:\codes\spec\accessibility-liveness-push.md)

注:本提交同时快照了并行 session 未提交的 coupon_state 改动 + 相关 migration
(与本功能同迁移链耦合,无法单独拆分,经确认一并提交)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:16:39 +08:00

95 lines
2.9 KiB
Python

"""极光推送 JPush(无障碍保护掉线告警)。
调极光 Push REST v3 /v3/push 给指定 registration_id 推一条通知。鉴权复用极光 Basic Auth
(appKey:masterSecret),与一键登录/短信同模式(见 integrations/jiguang.py、sms.py)。
凭证:settings.jpush_app_key / jpush_master_secret(JPUSH_* 留空时自动回退到 JG_*,
若推送与一键登录是同一极光应用)。客户端 push appkey 已知 = 966b451a8d9cfe12d173ea9d。
厂商通道(App 被杀也能到达)由极光后台 + 客户端插件负责,本服务只管调 push 接口。
"""
from __future__ import annotations
import base64
import logging
import httpx
from app.core.config import settings
logger = logging.getLogger("shagua.jpush")
class JPushError(Exception):
"""极光推送调用失败。"""
class JPushNotConfiguredError(JPushError):
"""缺 appKey / masterSecret。"""
def push_to_registration_ids(
registration_ids: list[str],
*,
title: str,
alert: str,
extras: dict | None = None,
) -> dict:
"""给一批 registration_id 推送通知 + 透传消息。失败抛 JPushError。
Returns: 极光响应 JSON(含 sendno / msg_id)。
"""
if not settings.jpush_configured:
raise JPushNotConfiguredError(
"JPush 未配置(缺 JPUSH_APP_KEY/JPUSH_MASTER_SECRET,且 JG_* 也为空)"
)
reg_ids = [r for r in registration_ids if r]
if not reg_ids:
raise JPushError("registration_ids 为空")
auth_b64 = base64.b64encode(
f"{settings.jpush_app_key}:{settings.jpush_master_secret}".encode()
).decode()
payload = {
"platform": ["android"],
"audience": {"registration_id": reg_ids},
"notification": {
"android": {
"alert": alert,
"title": title,
"priority": 1,
"extras": extras or {},
},
},
"message": {
"msg_content": alert,
"title": title,
"content_type": "text",
"extras": extras or {},
},
"options": {"time_to_live": 86400, "apns_production": True},
}
try:
resp = httpx.post(
settings.JPUSH_PUSH_ENDPOINT,
json=payload,
headers={
"Content-Type": "application/json",
"Authorization": f"Basic {auth_b64}",
},
timeout=settings.JG_REQUEST_TIMEOUT_SEC,
)
except httpx.HTTPError as e:
raise JPushError(f"jpush 网络错误: {e}") from e
if resp.status_code != 200:
body = resp.text[:300]
logger.error("[JPush] http=%s body=%s", resp.status_code, body)
raise JPushError(f"jpush http {resp.status_code}")
data = resp.json()
if not data.get("sendno") and not data.get("msg_id"):
logger.error("[JPush] unexpected response: %s", data)
raise JPushError(f"jpush 响应异常: {data}")
return data