Files
bid-doctor/server/src/bid_doctor/project/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

62 lines
2.1 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 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)