Files
bid-doctor/server/src/bid_doctor/notification/api.py
YANG JIANKUAN e7fb797925
Some checks failed
ci / server (push) Has been cancelled
ci / web (push) Has been cancelled
ci / contract (push) Has been cancelled
feat(report): 检查报告 PDF 导出和归档;项目状态流转(递交 / 归档)
- report 模块:导出 → worker 用 Gotenberg 把 HTML 转成 A4 PDF → 按项目数据密钥加密存储 → report
  (不可改、存哈希);report_job 记生成过程(迁移 0009),完成推 report.ready、发通知
- 报告首页:项目信息、投标文件版本、检查依据、规则版本、导出人、检查覆盖范围、汇总、免责声明;
  问题明细带两侧出处和处理人;附录为需人工核对的勾选结果;模板文字不出现「通过 / 合格 / 保证」
- 前端「报告」标签页:导出选项、内嵌预览、历次报告下载;工作台「导出检查报告」直达
- 项目状态流转 POST /projects/{id}/transitions:致命未处理也能递交(确认框说明),
  递交后只读,撤销归档回到归档前的状态,都写审计
- 通知补上检查、报告的标题

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 20:51:43 +08:00

65 lines
2.3 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.

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
],
}