- 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>
62 lines
2.1 KiB
Python
62 lines
2.1 KiB
Python
from __future__ import annotations
|
||
|
||
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
|
||
|
||
router = APIRouter(tags=["projects"])
|
||
|
||
|
||
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 补齐
|
||
async with tenant_tx(ctx) as s:
|
||
return {"items": await workbench.rows(s, ctx, limit), "next_cursor": None}
|
||
|
||
|
||
@router.post("/projects", status_code=201)
|
||
async def create_project(body: ProjectIn, ctx: Ctx) -> dict[str, Any]:
|
||
async with tenant_tx(ctx) as s:
|
||
p = await service.create(s, ctx, body.name)
|
||
return await workbench.detail(s, ctx, p.id)
|
||
|
||
|
||
@router.get("/projects/{project_id}")
|
||
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)
|