"""消息通知中心: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 ]