from __future__ import annotations from typing import Annotated, Any from fastapi import APIRouter, Header from fastapi.responses import StreamingResponse from sqlalchemy import func, select from bid_doctor.core import events from bid_doctor.core.db import tenant_tx from bid_doctor.identity.deps import Ctx from bid_doctor.notification.models import Notification router = APIRouter(tags=["notifications"]) @router.get("/events") async def stream_events(ctx: Ctx, last_event_id: Annotated[str | None, Header()] = None) -> StreamingResponse: """SSE(API2:前端用 fetch 读,可以带 Bearer 头)。""" return StreamingResponse( events.stream(ctx.tenant_id, last_event_id), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, ) @router.get("/notifications") async def list_notifications(ctx: Ctx, unread: bool | None = None) -> dict[str, Any]: async with tenant_tx(ctx) as s: q = select(Notification).where(Notification.user_id == ctx.user_id) if unread: q = q.where(Notification.read_at.is_(None)) rows = (await s.execute(q.order_by(Notification.created_at.desc()).limit(50))).scalars().all() n = ( await s.execute( select(func.count()) .select_from(Notification) .where(Notification.user_id == ctx.user_id, Notification.read_at.is_(None)) ) ).scalar_one() titles = { "parse_succeeded": "招标文件解析完成", "parse_failed": "招标文件解析失败,额度已退回", "check_succeeded": "投标文件检查完成", "check_failed": "投标文件检查没有完成,不扣额度", "report_succeeded": "检查报告已生成", "report_failed": "检查报告生成失败", } return { "unread_count": n, "next_cursor": None, "items": [ { "id": r.id, "kind": r.kind, "title": titles.get(r.kind, r.kind), "project_id": r.project_id, "link": f"/projects/{r.project_id}/checklist" if r.project_id else None, "read_at": r.read_at, "created_at": r.created_at, } for r in rows ], }