- 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>
65 lines
2.3 KiB
Python
65 lines
2.3 KiB
Python
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
|
||
],
|
||
}
|