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>
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
|
||||
from bid_doctor.core.db import tenant_tx
|
||||
from bid_doctor.document.models import SourceDocument
|
||||
from bid_doctor.identity.deps import Ctx
|
||||
from bid_doctor.project import service, workbench
|
||||
|
||||
@@ -17,6 +19,10 @@ class ProjectIn(BaseModel):
|
||||
name: str | None = Field(default=None, max_length=200)
|
||||
|
||||
|
||||
class TransitionIn(BaseModel):
|
||||
action: Literal["submit", "unsubmit", "archive", "unarchive"]
|
||||
|
||||
|
||||
@router.get("/projects")
|
||||
async def list_projects(ctx: Ctx, limit: int = Query(default=20, ge=1, le=100)) -> dict[str, Any]:
|
||||
# TODO(工作台筛选):status / deadline / q / 游标分页,按 openapi.yaml 补齐
|
||||
@@ -35,3 +41,21 @@ async def create_project(body: ProjectIn, ctx: Ctx) -> dict[str, Any]:
|
||||
async def get_project(project_id: UUID, ctx: Ctx) -> dict[str, Any]:
|
||||
async with tenant_tx(ctx) as s:
|
||||
return await workbench.detail(s, ctx, project_id)
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/transitions")
|
||||
async def transition_project(project_id: UUID, body: TransitionIn, ctx: Ctx) -> dict[str, Any]:
|
||||
async with tenant_tx(ctx) as s:
|
||||
parsed = (
|
||||
await s.execute(
|
||||
select(SourceDocument.id)
|
||||
.where(
|
||||
SourceDocument.project_id == project_id,
|
||||
SourceDocument.role == "tender",
|
||||
SourceDocument.text_layer_key.is_not(None),
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
).scalar_one_or_none() is not None
|
||||
await service.transition(s, ctx, project_id, body.action, parsed)
|
||||
return await workbench.detail(s, ctx, project_id)
|
||||
|
||||
Reference in New Issue
Block a user