Files
shaguabijia-app-server/app/api/v1/notifications.py
T

125 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""消息通知中心 endpoint(PRD《消息通知中心》)。
路由前缀 `/api/v1/notifications`,需 Bearer 鉴权(消息按用户隔离)。
GET / 消息列表(分页;全列表时间倒序,不分组——PRD 原文的分组已取消)
GET /unread-count 未读总数(首页铃铛角标)
POST /read 标记已读({ids:[...]} 单条/多条 或 {all:true} 全量清零)
数据落库 `notification` 表(repositories/notification.py,按用户隔离)。业务事件(奖励过期、
提现回执、反馈回复……)调 `create_notification` 下发;未接入业务前列表为空,可用
`/api/v1/push/test` 的 createNotification 造联调数据。
⚠️ 字段命名:本组接口对外为 **camelCase**(sentAt / isRead / pageSize…,PRD 前端契约),
详见 schemas/notification.py 顶部说明。
"""
from __future__ import annotations
import logging
from fastapi import APIRouter, HTTPException, Query
from app.api.deps import CurrentUser, DbSession
from app.core import notification_catalog as catalog
from app.models.notification import Notification
from app.repositories import notification as notif_repo
from app.schemas.notification import (
InfoRow,
MarkReadOut,
MarkReadRequest,
NotificationItem,
NotificationListOut,
UnreadCountOut,
)
logger = logging.getLogger("shagua.notifications")
router = APIRouter(prefix="/api/v1/notifications", tags=["notifications"])
def _to_item(n: Notification) -> NotificationItem:
"""通知行 + 类型静态目录 → 接口出参。"""
ntype = catalog.get_type(n.type)
return NotificationItem(
id=n.id,
category=ntype.category,
category_label=catalog.category_label(ntype.category),
type=ntype.key,
card_style=ntype.card_style,
title=ntype.card_title,
coins=n.coins,
cash_cents=n.cash_cents,
cash_yuan=notif_repo.cash_yuan(n.cash_cents),
info_rows=[InfoRow(**row) for row in n.info_rows],
action_text=ntype.action_text,
extra=n.extra,
sent_at=notif_repo.as_cst(n.sent_at),
is_read=n.is_read,
)
@router.get("", response_model=NotificationListOut, summary="消息列表(分页)")
def list_notifications(
user: CurrentUser,
db: DbSession,
page: int = Query(default=1, ge=1, description="页码,1 起"),
page_size: int = Query(
default=20, ge=1, le=100, alias="pageSize", description="每页条数,默认 20,最大 100"
),
) -> NotificationListOut:
"""通知中心消息列表。
- 排序服务端已做好:**全列表按时间倒序**(最新在前,不做分类分组;PRD §1 的
"按分类分组"为笔误,已与需求方确认取消),前端按返回顺序渲染即可。
- 每条的字段构成与各版式说明见 NotificationItem schema。
- 响应同时带 unreadCount,进页面时可顺手刷新角标。
- 无消息时返回空列表(total=0);数据由业务事件下发,联调可用 /push/test 造。
"""
items, total, unread = notif_repo.list_notifications(
db, user.id, page=page, page_size=page_size
)
return NotificationListOut(
items=[_to_item(n) for n in items],
page=page,
page_size=page_size,
total=total,
has_more=page * page_size < total,
unread_count=unread,
)
@router.get("/unread-count", response_model=UnreadCountOut, summary="未读总数(铃铛角标)")
def get_unread_count(user: CurrentUser, db: DbSession) -> UnreadCountOut:
"""首页铃铛角标数据源。刷新时机(PRD §4):进入首页时、从通知中心/其他页面返回首页时。
- count:精确未读条数;
- badgeText:直接可展示的角标文案——超过 99 返回 "99+",等于 0 返回 null(隐藏整个角标)。
"""
count = notif_repo.unread_count(db, user.id)
badge = None if count == 0 else ("99+" if count > 99 else str(count))
return UnreadCountOut(count=count, badge_text=badge)
@router.post("/read", response_model=MarkReadOut, summary="标记已读(单条/多条/全量)")
def mark_read(req: MarkReadRequest, user: CurrentUser, db: DbSession) -> MarkReadOut:
"""红点消除(PRD §4),两种调用模式:
1. `{"ids": [90001]}` —— 点击某张消息卡片(无论点击后是跳转/弹窗/无动作都算已读);
用户点击 push 直达落地页时,客户端也用它把对应站内消息同步置读(push extras 里带
notificationId);
2. `{"all": true}` —— 进入通知中心自动清零(只是浏览列表就消红点,无需逐条点击)。
幂等:不存在或已读的 id 忽略;重复调用 markedCount 为 0、不报错。
响应带 unreadCount(处理后剩余未读),可直接刷新铃铛角标。
"""
if not req.all and not req.ids:
raise HTTPException(status_code=400, detail="ids 与 all 至少传一个:{ids:[...]} 或 {all:true}")
marked, unread = notif_repo.mark_read(db, user.id, ids=req.ids, mark_all=req.all)
logger.info(
"notifications read user_id=%d mode=%s marked=%d unread_left=%d",
user.id,
"all" if req.all else f"ids×{len(req.ids or [])}",
marked,
unread,
)
return MarkReadOut(ok=True, marked_count=marked, unread_count=unread)