125 lines
5.1 KiB
Python
125 lines
5.1 KiB
Python
"""消息通知中心 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)
|