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>
242 lines
11 KiB
Python
242 lines
11 KiB
Python
"""消息通知中心:13 种通知类型的静态目录 + Push 文案模板。
|
|
|
|
对应 PRD《消息通知中心》:§1 类型清单 / §3 字段元素 / §5 Push 文案。
|
|
这里只放**静态定义**(分类、版式、标题、操作行、Push 模板),供两处消费:
|
|
- repositories/notification_mock.py 生成消息中心 mock 列表(虚拟数据阶段)
|
|
- api/v1/push.py 渲染 13 类 push 标题/文案(厂商推送 + 测试端点)
|
|
|
|
PRD 文案规范(§5):push 标题 ≤11 字、固定文案不带变量;变量只出现在正文里且尽量前置。
|
|
模板变量用 `{name}` 占位,渲染时缺省回退 sample_vars(PRD 示例值),保证 mock 阶段随时可发。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 分类(仅作卡片头部的分类标签展示;列表不按分类分组——PRD §1 的分组已确认取消,全表时间倒序)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
CATEGORY_WITHDRAW = "withdraw_assistant"
|
|
CATEGORY_SYSTEM = "system"
|
|
CATEGORY_FEEDBACK = "feedback"
|
|
CATEGORY_REPORT = "report"
|
|
CATEGORY_INVITE = "invite"
|
|
|
|
# key → 中文标签
|
|
CATEGORIES: dict[str, str] = {
|
|
CATEGORY_WITHDRAW: "提现助手",
|
|
CATEGORY_SYSTEM: "系统通知",
|
|
CATEGORY_FEEDBACK: "我的反馈",
|
|
CATEGORY_REPORT: "我的爆料",
|
|
CATEGORY_INVITE: "好友邀请",
|
|
}
|
|
|
|
|
|
def category_label(key: str) -> str:
|
|
return CATEGORIES[key]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 卡片版式(PRD §3「版式」列;前端按此渲染五种卡)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
CARD_DUAL_AMOUNT = "dual_amount" # 双金额卡(金币数 + 现金数)
|
|
CARD_WITHDRAW = "withdraw" # 提现卡(¥金额)
|
|
CARD_PLAIN_TEXT = "plain_text" # 纯文本卡(无数值)
|
|
CARD_COIN_REWARD = "coin_reward" # 金币奖励卡(金币数 + 单位「金币」)
|
|
CARD_FRIEND_CASH = "friend_cash" # 好友现金卡(¥金额)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class NotificationType:
|
|
"""一种通知类型的静态定义(卡片元数据 + Push 模板)。"""
|
|
|
|
key: str # 类型 key(接口 type 字段;前端按它决定点击跳转,见 PRD §2)
|
|
category: str # 分类 key(CATEGORIES 之一)
|
|
card_style: str # 卡片版式(CARD_* 之一)
|
|
card_title: str # 卡片标题(PRD §3「标题」列)
|
|
action_text: str | None # 操作行文案;None = 无操作行(如「提现成功」)
|
|
push_title: str # push 标题(≤11 字固定文案,PRD §5)
|
|
push_body_template: str # push 正文模板,`{var}` 为变量
|
|
sample_vars: dict[str, str] = field(default_factory=dict) # PRD 示例值,渲染缺省回退
|
|
|
|
|
|
# 13 种类型,编号/文案与 PRD §1/§3/§5 一一对应(插入顺序 = PRD 编号顺序)。
|
|
TYPES: dict[str, NotificationType] = {
|
|
t.key: t
|
|
for t in [
|
|
# -- 提现助手 -------------------------------------------------------
|
|
NotificationType(
|
|
key="reward_expiring",
|
|
category=CATEGORY_WITHDRAW,
|
|
card_style=CARD_DUAL_AMOUNT,
|
|
card_title="金币现金奖励即将失效",
|
|
action_text="立即激活您的收益",
|
|
push_title="您的奖励即将失效",
|
|
push_body_template="{coins}金币和{cash}元现金{days}天后失效,完成快来激活收益",
|
|
sample_vars={"coins": "86", "cash": "12.80", "days": "3"},
|
|
),
|
|
NotificationType(
|
|
key="reward_expired",
|
|
category=CATEGORY_WITHDRAW,
|
|
card_style=CARD_DUAL_AMOUNT,
|
|
card_title="金币现金奖励已失效",
|
|
action_text="立即赚取新收益",
|
|
push_title="您的奖励已失效",
|
|
push_body_template="{coins}金币和{cash}元现金已过期,完成一次一键领券或一键比价可赚取新收益",
|
|
sample_vars={"coins": "35", "cash": "0.60"},
|
|
),
|
|
NotificationType(
|
|
key="withdraw_success",
|
|
category=CATEGORY_WITHDRAW,
|
|
card_style=CARD_WITHDRAW,
|
|
card_title="提现成功",
|
|
action_text=None, # PRD §3:提现成功卡无操作行,点击也无跳转、仅消红点
|
|
push_title="提现到账提醒",
|
|
push_body_template="¥{amount}已存入您的微信钱包,点击查看到账详情",
|
|
sample_vars={"amount": "0.50"},
|
|
),
|
|
NotificationType(
|
|
key="withdraw_failed",
|
|
category=CATEGORY_WITHDRAW,
|
|
card_style=CARD_WITHDRAW,
|
|
card_title="提现失败,款项已退回",
|
|
action_text="重新提现",
|
|
push_title="提现失败,款项已退回",
|
|
push_body_template="¥{amount}因{reason}退回现金余额,点击重新提现",
|
|
sample_vars={"amount": "3.50", "reason": "微信零钱未实名"},
|
|
),
|
|
# -- 系统通知(权限异常 ×4;标题里的功能名按类型写死,见 PRD §1/§3)----
|
|
NotificationType(
|
|
key="perm_accessibility",
|
|
category=CATEGORY_SYSTEM,
|
|
card_style=CARD_PLAIN_TEXT,
|
|
card_title="检测到您的比价功能已失效",
|
|
action_text="去开启",
|
|
push_title="检测到您的比价功能已失效",
|
|
push_body_template="未开启将导致核心功能不可用,请尽快来傻瓜比价开启",
|
|
),
|
|
NotificationType(
|
|
key="perm_battery",
|
|
category=CATEGORY_SYSTEM,
|
|
card_style=CARD_PLAIN_TEXT,
|
|
card_title="检测到您的比价续航保护已失效",
|
|
action_text="去开启",
|
|
push_title="检测到您的比价续航保护已失效",
|
|
push_body_template="未开启将导致核心功能不可用,请尽快来傻瓜比价开启",
|
|
),
|
|
NotificationType(
|
|
key="perm_autostart",
|
|
category=CATEGORY_SYSTEM,
|
|
card_style=CARD_PLAIN_TEXT,
|
|
card_title="检测到您的比价启动保护已失效",
|
|
action_text="去开启",
|
|
push_title="检测到您的比价启动保护已失效",
|
|
push_body_template="未开启将导致核心功能不可用,请尽快来傻瓜比价开启",
|
|
),
|
|
NotificationType(
|
|
key="perm_overlay",
|
|
category=CATEGORY_SYSTEM,
|
|
card_style=CARD_PLAIN_TEXT,
|
|
card_title="检测到您的比价按钮已失效",
|
|
action_text="去开启",
|
|
push_title="检测到您的比价按钮已失效",
|
|
push_body_template="未开启将导致核心功能不可用,请尽快来傻瓜比价开启",
|
|
),
|
|
# -- 我的反馈 -------------------------------------------------------
|
|
NotificationType(
|
|
key="feedback_reply",
|
|
category=CATEGORY_FEEDBACK,
|
|
card_style=CARD_PLAIN_TEXT,
|
|
card_title="傻瓜比价官方回复了您的反馈",
|
|
action_text="查看详情",
|
|
push_title="您的反馈有回复啦",
|
|
push_body_template="您提的建议我们认真看过了,来看看我们的回复吧~",
|
|
),
|
|
NotificationType(
|
|
key="feedback_reward",
|
|
category=CATEGORY_FEEDBACK,
|
|
card_style=CARD_COIN_REWARD,
|
|
card_title="反馈奖励",
|
|
action_text="查看反馈详情",
|
|
push_title="反馈奖励已到账",
|
|
push_body_template="谢谢您帮傻瓜比价变得更好,{coins}金币已到账,还有一条给您的留言~",
|
|
sample_vars={"coins": "300"},
|
|
),
|
|
# -- 我的爆料 -------------------------------------------------------
|
|
NotificationType(
|
|
key="report_approved",
|
|
category=CATEGORY_REPORT,
|
|
card_style=CARD_COIN_REWARD,
|
|
card_title="爆料审核通过",
|
|
action_text="查看爆料详情",
|
|
push_title="爆料审核通过",
|
|
push_body_template="您爆料的「{store}」更低价审核通过,{coins}金币已到账,感谢您的分享",
|
|
sample_vars={"store": "蜀大侠火锅", "coins": "1000"},
|
|
),
|
|
# -- 好友邀请 -------------------------------------------------------
|
|
NotificationType(
|
|
key="invite_order_reward",
|
|
category=CATEGORY_INVITE,
|
|
card_style=CARD_FRIEND_CASH,
|
|
card_title="好友比价成功,现金已到账",
|
|
action_text="邀请更多好友赚现金",
|
|
push_title="您的邀请奖励已到账",
|
|
push_body_template="您的好友「{nickname}」完成首次下单,{amount}元现金已到账",
|
|
sample_vars={"nickname": "柚子", "amount": "2"},
|
|
),
|
|
NotificationType(
|
|
key="invite_remind",
|
|
category=CATEGORY_INVITE,
|
|
card_style=CARD_PLAIN_TEXT,
|
|
card_title="你邀请的好友还差一步",
|
|
action_text="去提醒 TA",
|
|
push_title="提醒好友完成比价的奖励",
|
|
push_body_template="您的好友「{nickname}」还没完成比价下单,提醒TA完成,您可得{amount}元现金",
|
|
sample_vars={"nickname": "阿泽", "amount": "2"},
|
|
),
|
|
]
|
|
}
|
|
|
|
|
|
class UnknownNotificationType(ValueError):
|
|
"""type key 不在 13 种类型之内。"""
|
|
|
|
|
|
def get_type(type_key: str) -> NotificationType:
|
|
ntype = TYPES.get(type_key)
|
|
if ntype is None:
|
|
raise UnknownNotificationType(
|
|
f"unknown notification type: {type_key!r} (可选: {', '.join(TYPES)})"
|
|
)
|
|
return ntype
|
|
|
|
|
|
def render_push(type_key: str, variables: dict[str, str] | None = None) -> tuple[str, str]:
|
|
"""渲染某类型的 push (标题, 正文)。
|
|
|
|
variables 覆盖模板变量;缺的变量回退 sample_vars(PRD 示例值)——保证虚拟数据
|
|
阶段不传变量也能发出完整文案。多余的变量忽略。
|
|
"""
|
|
ntype = get_type(type_key)
|
|
merged = {**ntype.sample_vars, **(variables or {})}
|
|
|
|
class _Fallback(dict):
|
|
def __missing__(self, key: str) -> str: # 模板变量既没传也没示例值 → 保留 {key} 原样
|
|
return "{" + key + "}"
|
|
|
|
body = ntype.push_body_template.format_map(_Fallback(merged))
|
|
return ntype.push_title, body
|
|
|
|
|
|
def push_variable_names(type_key: str) -> list[str]:
|
|
"""列出模板里出现的变量名(给 /push/templates 预览用)。"""
|
|
import string
|
|
|
|
ntype = get_type(type_key)
|
|
return [
|
|
fname
|
|
for _, fname, _, _ in string.Formatter().parse(ntype.push_body_template)
|
|
if fname
|
|
]
|