From d6016c12f913863e98cd1e1cb4e2b3db42ffb581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B7=A6=E8=BE=B0=E5=8B=87?= Date: Tue, 14 Jul 2026 21:22:25 +0800 Subject: [PATCH] =?UTF-8?q?OPPO=20=E6=8E=A8=E9=80=81=E8=A1=A5=E6=96=B0?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=88=86=E7=B1=BB(channel=5Fid/category/noti?= =?UTF-8?q?fy=5Flevel)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2024-11-20 后创建的 OPPO 应用发送通知必须携带 category(新规), 配置项 OPPO_PUSH_CHANNEL_ID / OPPO_PUSH_CATEGORY / OPPO_PUSH_NOTIFY_LEVEL(0=不传), 均留空时 payload 与原先完全一致。凭据已到位:荣耀/小米(含 channel 154219)/OPPO/vivo 四家配齐(.env,不入库),仅剩华为待补。 Co-Authored-By: Claude Fable 5 --- .env.example | 5 +++ app/core/config.py | 6 ++++ app/integrations/vendor_push.py | 26 ++++++++++------ tests/test_push_center.py | 54 +++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 542cce5..c9d1339 100644 --- a/.env.example +++ b/.env.example @@ -69,6 +69,11 @@ OPPO_PUSH_APP_KEY= OPPO_PUSH_MASTER_SECRET= OPPO_PUSH_AUTH_ENDPOINT=https://api.push.oppomobile.com/server/v1/auth OPPO_PUSH_SEND_ENDPOINT=https://api.push.oppomobile.com/server/v1/message/notification/unicast +# OPPO 新消息分类(2024-11-20 后创建的应用必须携带 category;channel_id 为后台「通道ID」; +# notify_level 0=不传走默认,内容营销类仅支持 1/2) +OPPO_PUSH_CHANNEL_ID= +OPPO_PUSH_CATEGORY= +OPPO_PUSH_NOTIFY_LEVEL=0 # ===== 无障碍保护存活监控(推送 + pull 后置兜底)===== HEARTBEAT_MONITOR_ENABLED=true diff --git a/app/core/config.py b/app/core/config.py index d295acc..cdcc5e6 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -111,6 +111,12 @@ class Settings(BaseSettings): OPPO_PUSH_SEND_ENDPOINT: str = ( "https://api.push.oppomobile.com/server/v1/message/notification/unicast" ) + # OPPO 新消息分类(2024-11-20 后创建的应用必须携带,否则可能被拒/限): + # channel_id=通知栏通道(OPPO 后台「通道ID」),category=消息分类 code(如 MARKETING 内容营销)。 + # notify_level=提醒方式(0=不传走 OPPO 默认;内容营销类仅支持 1 通知栏/2 通知栏+锁屏)。 + OPPO_PUSH_CHANNEL_ID: str = "" + OPPO_PUSH_CATEGORY: str = "" + OPPO_PUSH_NOTIFY_LEVEL: int = 0 # 无障碍保护存活监控后台任务(推送 + pull 后置兜底) HEARTBEAT_MONITOR_ENABLED: bool = True # 总开关 diff --git a/app/integrations/vendor_push.py b/app/integrations/vendor_push.py index 20c7973..fd4c8a0 100644 --- a/app/integrations/vendor_push.py +++ b/app/integrations/vendor_push.py @@ -484,18 +484,26 @@ def _oppo_auth_token() -> str: def _send_oppo(token: str, title: str, body: str, extras: dict[str, str]) -> dict[str, Any]: auth_token = _oppo_auth_token() ttl_hours = max(1, min(72, settings.PUSH_TIME_TO_LIVE_SEC // 3600)) + notification: dict[str, Any] = { + "app_message_id": f"{extras.get('type', 'notify')}_{uuid.uuid4().hex}", + "title": title, + "content": body, + "click_action_type": 0, + "off_line": True, + "off_line_ttl": ttl_hours, + "action_parameters": json.dumps(extras, ensure_ascii=False), + } + # 新消息分类(2024-11-20 后创建的 OPPO 应用必须带 category,否则可能被拒收/降级) + if settings.OPPO_PUSH_CHANNEL_ID.strip(): + notification["channel_id"] = settings.OPPO_PUSH_CHANNEL_ID.strip() + if settings.OPPO_PUSH_CATEGORY.strip(): + notification["category"] = settings.OPPO_PUSH_CATEGORY.strip() + if settings.OPPO_PUSH_NOTIFY_LEVEL: + notification["notify_level"] = settings.OPPO_PUSH_NOTIFY_LEVEL message = { "target_type": 2, "target_value": token, - "notification": { - "app_message_id": f"{extras.get('type', 'notify')}_{uuid.uuid4().hex}", - "title": title, - "content": body, - "click_action_type": 0, - "off_line": True, - "off_line_ttl": ttl_hours, - "action_parameters": json.dumps(extras, ensure_ascii=False), - }, + "notification": notification, } data = _request_form( "POST", diff --git a/tests/test_push_center.py b/tests/test_push_center.py index 4f116be..491c5a5 100644 --- a/tests/test_push_center.py +++ b/tests/test_push_center.py @@ -155,6 +155,60 @@ def test_accessibility_wrapper_keeps_legacy_extras(monkeypatch) -> None: assert json.loads(captured["data"]["payload"]) == {"type": "accessibility_disabled"} +def test_oppo_payload_includes_new_message_category(monkeypatch) -> None: + """OPPO 新消息分类:配置了 channel_id/category 时随通知体下发(2024-11 新规必带)。""" + vendor_push._token_cache.clear() + calls: list[dict] = [] + + def _fake_request(method, url, **kwargs): # noqa: ANN001 + calls.append({"method": method, "url": url, **kwargs}) + if url == vendor_push.settings.OPPO_PUSH_AUTH_ENDPOINT: + return _Resp({"code": 0, "data": {"auth_token": "oppo-auth"}}) + return _Resp({"code": 0, "data": {"message_id": "oppo-msg"}}) + + monkeypatch.setattr(vendor_push.settings, "OPPO_PUSH_APP_KEY", "oppo-key") + monkeypatch.setattr(vendor_push.settings, "OPPO_PUSH_MASTER_SECRET", "oppo-master") + monkeypatch.setattr(vendor_push.settings, "OPPO_PUSH_CHANNEL_ID", "push_oplus_category_content") + monkeypatch.setattr(vendor_push.settings, "OPPO_PUSH_CATEGORY", "MARKETING") + monkeypatch.setattr(vendor_push.settings, "OPPO_PUSH_NOTIFY_LEVEL", 0) + monkeypatch.setattr(vendor_push.httpx, "request", _fake_request) + + vendor_push.send_notification( + "oppo", "oppo-regid", title="标题", body="正文", extras={"type": "push_test"} + ) + + notification = json.loads(calls[1]["data"]["message"])["notification"] + assert notification["channel_id"] == "push_oplus_category_content" + assert notification["category"] == "MARKETING" + assert "notify_level" not in notification # 0=不传,走 OPPO 默认 + + +def test_oppo_payload_omits_category_when_unconfigured(monkeypatch) -> None: + vendor_push._token_cache.clear() + calls: list[dict] = [] + + def _fake_request(method, url, **kwargs): # noqa: ANN001 + calls.append({"method": method, "url": url, **kwargs}) + if url == vendor_push.settings.OPPO_PUSH_AUTH_ENDPOINT: + return _Resp({"code": 0, "data": {"auth_token": "oppo-auth"}}) + return _Resp({"code": 0, "data": {"message_id": "oppo-msg"}}) + + monkeypatch.setattr(vendor_push.settings, "OPPO_PUSH_APP_KEY", "oppo-key") + monkeypatch.setattr(vendor_push.settings, "OPPO_PUSH_MASTER_SECRET", "oppo-master") + monkeypatch.setattr(vendor_push.settings, "OPPO_PUSH_CHANNEL_ID", "") + monkeypatch.setattr(vendor_push.settings, "OPPO_PUSH_CATEGORY", "") + monkeypatch.setattr(vendor_push.settings, "OPPO_PUSH_NOTIFY_LEVEL", 0) + monkeypatch.setattr(vendor_push.httpx, "request", _fake_request) + + vendor_push.send_notification( + "oppo", "oppo-regid", title="标题", body="正文", extras={"type": "push_test"} + ) + + notification = json.loads(calls[1]["data"]["message"])["notification"] + assert "channel_id" not in notification + assert "category" not in notification + + # --------------------------------------------------------------------------- # /api/v1/push 三件套 # ---------------------------------------------------------------------------