"""厂商推送(测试/联调)接口契约。与消息中心同族,出参统一 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)" )