a8ac5dc0c7
- GET /api/v1/notifications(分页,时间倒序不分组)/unread-count /read(ids|all),
对外 camelCase(PRD 前端契约);数据为内存 mock(13 类型全覆盖,重启复位),
接真实数据只换 repositories/notification_mock.py
- vendor_push 补华为 Push Kit(OAuth+messages:send),抽通用 send_notification
(任意文案+extras+mock 模式),send_accessibility_disabled 改薄封装行为不变
- 新增 /api/v1/push/{vendors,templates,test}:凭据状态/13 类 PRD 文案模板/测试发送
(默认 mock,mock=false 真发,可联动插站内 mock 消息闭环验证已读)
- .env.example 补 HUAWEI_* 键;docs/api 新增 notifications.md + push-vendor-test.md
- alembic merge 1a924c274fce 收拢 direct_vendor_push_fields/feedback_type_reply
双 head,恢复 upgrade head 可用
- tests: test_notifications + test_push_center 共 35 例
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
100 lines
4.9 KiB
Python
100 lines
4.9 KiB
Python
"""厂商推送(测试/联调)接口契约。与消息中心同族,出参统一 camelCase。"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
from pydantic.alias_generators import to_camel
|
|
|
|
|
|
class _CamelModel(BaseModel):
|
|
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
|
|
|
|
class PushVendorStatus(_CamelModel):
|
|
vendor: str = Field(description="厂商 key:honor / huawei / xiaomi / oppo / vivo")
|
|
label: str = Field(description="厂商中文名")
|
|
configured: bool = Field(description="服务端凭据是否齐全(齐全才能真发,mock 不受影响)")
|
|
missing_keys: list[str] = Field(description="缺失的 .env 配置键;configured=true 时为空")
|
|
|
|
|
|
class PushVendorsOut(_CamelModel):
|
|
vendors: list[PushVendorStatus] = Field(description="5 个厂商的配置状态")
|
|
|
|
|
|
class PushTemplateOut(_CamelModel):
|
|
type: str = Field(description="通知类型 key(13 种,与消息中心 type 一致)")
|
|
category: str = Field(description="分类 key")
|
|
category_label: str = Field(description="分类中文标签")
|
|
card_style: str = Field(description="站内卡片版式")
|
|
push_title: str = Field(description="push 标题(≤11 字固定文案,PRD §5)")
|
|
push_body_sample: str = Field(description="push 正文示例(模板用 PRD 示例值渲染后的效果)")
|
|
push_body_template: str = Field(description="push 正文模板原文,{var} 为变量占位")
|
|
variables: list[str] = Field(description="模板变量名列表(调 /push/test 时可在 vars 里覆盖)")
|
|
sample_vars: dict[str, str] = Field(description="各变量的 PRD 示例值(vars 未覆盖时的缺省)")
|
|
|
|
|
|
class PushTemplatesOut(_CamelModel):
|
|
templates: list[PushTemplateOut] = Field(description="13 种通知类型的 push 模板(PRD 编号顺序)")
|
|
|
|
|
|
class PushTestRequest(_CamelModel):
|
|
"""POST /api/v1/push/test 入参。三种发送内容来源(优先级从高到低):
|
|
|
|
1. 直接指定 title + content;
|
|
2. 指定 type(13 种之一)→ 按 PRD §5 模板渲染,vars 可覆盖模板变量;
|
|
3. 都不传 → 发一条通用测试文案。
|
|
|
|
推送目标:pushToken 直填,或 deviceId 反查该用户已注册设备(/api/v1/device/register 上报过的)。
|
|
"""
|
|
|
|
vendor: str = Field(
|
|
default="",
|
|
description="厂商:honor/huawei/xiaomi/oppo/vivo(中文「华为」「小米」等别名也识别)。"
|
|
"留空时用 deviceId 对应设备上报的 push_vendor",
|
|
)
|
|
push_token: str = Field(default="", description="厂商 push token / regId;留空则走 deviceId 反查")
|
|
device_id: str = Field(default="", description="设备 id(客户端 DeviceId.get());用于反查 token")
|
|
type: str = Field(
|
|
default="",
|
|
description="通知类型 key(13 种,见 GET /push/templates);留空且未直接给 title/content 时发通用测试文案",
|
|
)
|
|
vars: dict[str, str] = Field(
|
|
default_factory=dict,
|
|
description="覆盖 push 模板变量,如 {\"coins\":\"520\",\"cash\":\"6.66\"};缺省用 PRD 示例值",
|
|
)
|
|
title: str = Field(default="", description="直接指定标题(优先于 type 模板)")
|
|
content: str = Field(default="", description="直接指定正文(优先于 type 模板)")
|
|
create_notification: bool = Field(
|
|
default=False,
|
|
description="true=同时往该用户的消息中心 mock 列表插入一条同类型未读通知,push extras 带上它的"
|
|
" notificationId → 可闭环验证「点 push → 落地 → 调 /notifications/read 消红点」联动"
|
|
"(仅 type 为 13 种类型之一时生效)",
|
|
)
|
|
mock: bool = Field(
|
|
default=True,
|
|
description="true(默认)=不真调厂商 API,返回渲染结果(联调安全);false=真发,要求该厂商凭据已配置",
|
|
)
|
|
|
|
|
|
class PushTestOut(_CamelModel):
|
|
ok: bool = Field(description="发送(或 mock 渲染)成功")
|
|
mock: bool = Field(description="本次是否 mock(未真调厂商 API)")
|
|
vendor: str = Field(description="实际使用的厂商 key(已归一化)")
|
|
title: str = Field(description="实际下发的 push 标题")
|
|
body: str = Field(description="实际下发的 push 正文")
|
|
extras: dict[str, str] = Field(
|
|
description="随 push 下发的自定义键值(客户端深链用):type 必有;createNotification=true 时带"
|
|
" notificationId 及该通知的业务参数(feedbackId / permission / …)"
|
|
)
|
|
notification_id: int | None = Field(
|
|
default=None, description="createNotification=true 时新插入的站内 mock 通知 id"
|
|
)
|
|
missing_keys: list[str] = Field(
|
|
default_factory=list,
|
|
description="该厂商仍缺失的配置键(mock 发送时提示「真发前还需配什么」;真发时必为空)",
|
|
)
|
|
vendor_response: dict[str, Any] | None = Field(
|
|
default=None, description="真发时厂商 API 的原始响应(mock 时为 null)"
|
|
)
|