From e7fb7979258a3ae16a1ff15a6dd082911a2e7090 Mon Sep 17 00:00:00 2001 From: YANG JIANKUAN Date: Wed, 4 Nov 2026 20:51:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(report):=20=E6=A3=80=E6=9F=A5=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=20PDF=20=E5=AF=BC=E5=87=BA=E5=92=8C=E5=BD=92=E6=A1=A3?= =?UTF-8?q?=EF=BC=9B=E9=A1=B9=E7=9B=AE=E7=8A=B6=E6=80=81=E6=B5=81=E8=BD=AC?= =?UTF-8?q?=EF=BC=88=E9=80=92=E4=BA=A4=20/=20=E5=BD=92=E6=A1=A3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - report 模块:导出 → worker 用 Gotenberg 把 HTML 转成 A4 PDF → 按项目数据密钥加密存储 → report (不可改、存哈希);report_job 记生成过程(迁移 0009),完成推 report.ready、发通知 - 报告首页:项目信息、投标文件版本、检查依据、规则版本、导出人、检查覆盖范围、汇总、免责声明; 问题明细带两侧出处和处理人;附录为需人工核对的勾选结果;模板文字不出现「通过 / 合格 / 保证」 - 前端「报告」标签页:导出选项、内嵌预览、历次报告下载;工作台「导出检查报告」直达 - 项目状态流转 POST /projects/{id}/transitions:致命未处理也能递交(确认框说明), 递交后只读,撤销归档回到归档前的状态,都写审计 - 通知补上检查、报告的标题 Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 3 +- apps/web/src/components/ProjectHead.vue | 61 ++++- apps/web/src/components/ProjectRow.vue | 6 +- apps/web/src/lib/keys.ts | 1 + apps/web/src/lib/queries.ts | 1 + apps/web/src/pages/ReportPage.vue | 239 +++++++++++++++++ apps/web/src/router.ts | 6 + docs/architecture/m3-plan.md | 4 +- docs/architecture/openapi.yaml | 11 +- packages/api-client/src/schema.d.ts | 19 +- server/migrations/versions/0009_report_job.py | 64 +++++ server/src/bid_doctor/app.py | 2 + server/src/bid_doctor/audit/service.py | 13 + server/src/bid_doctor/core/errors.py | 2 + server/src/bid_doctor/notification/api.py | 9 +- server/src/bid_doctor/project/api.py | 26 +- server/src/bid_doctor/project/service.py | 43 ++++ server/src/bid_doctor/report/__init__.py | 0 server/src/bid_doctor/report/api.py | 64 +++++ .../report/assets/logo-horizontal.svg | 1 + server/src/bid_doctor/report/models.py | 42 +++ server/src/bid_doctor/report/runner.py | 113 ++++++++ server/src/bid_doctor/report/service.py | 169 ++++++++++++ server/src/bid_doctor/report/template.py | 243 ++++++++++++++++++ server/src/bid_doctor/worker.py | 7 + server/tests/test_report_flow.py | 140 ++++++++++ 26 files changed, 1276 insertions(+), 13 deletions(-) create mode 100644 apps/web/src/pages/ReportPage.vue create mode 100644 server/migrations/versions/0009_report_job.py create mode 100644 server/src/bid_doctor/report/__init__.py create mode 100644 server/src/bid_doctor/report/api.py create mode 100644 server/src/bid_doctor/report/assets/logo-horizontal.svg create mode 100644 server/src/bid_doctor/report/models.py create mode 100644 server/src/bid_doctor/report/runner.py create mode 100644 server/src/bid_doctor/report/service.py create mode 100644 server/src/bid_doctor/report/template.py create mode 100644 server/tests/test_report_flow.py diff --git a/AGENTS.md b/AGENTS.md index 1eaea5d..4b44dcb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -219,4 +219,5 @@ samples/ - 2026-11-04:查标第六轮:送给模型的「同一行」按单元格分行;修了「以合同签订时间为准」被当成中标后才交、业绩评分项被跳过的缺陷;重复条目按用户决定改为各自判断、只合并显示(C5 修订,`docs/decisions.md`);致命项重问按数据(开发集致命级证据校验失败 0 条)暂不做。评测集裁定 1 条。开发集召回 74%、致命级 25 / 32。见 `docs/architecture/check-eval.md` §9。 - 2026-11-04:查标第 3 步冻结(`a60d16e`),跑留出集一次:S03、S06、S09 各一份风格 B 规矩 + 风格 C 马虎(风格 C 由封存一侧的会话实现),96 个注入问题 Fable 复核通过 79,用户抽查 19 条全部同意。留出集召回 54%(开发集 74%)、致命级 56%(开发集 78%)、负偏离未说明 25%(开发集 92%)、规则问题 13 / 20;S03 的规矩投标文件因响应方式识别过度触发报了 1094 个提示,人工确认响应方式后降到 45 个。开发集分数有相当一部分是调出来的;要修的 5 处见 `docs/architecture/check-eval.md` §11。 - 2026-11-04:按用户决定(E2)先修留出集暴露的 5 处(响应方式识别、偏离栏按「是不是明确满足」判断、另报价 / 附加条件按结构判断、空白栏、异体字),规则集 2026.11-r3。开发集不退化(召回 75%、规矩投标文件问题数不变);留出集参考数字召回 54% → 69%、规矩投标文件问题 1134 → 82(已看过,正式成绩仍以冻结代码那次为准)。见 `docs/architecture/check-eval.md` §12。 -- 下一步:跑国动真实投标文件(等用户提供,用修好的代码)。 +- 2026-11-04:查标第 5 步检查报告:导出 → 后台用 Gotenberg 生成 A4 PDF(首页覆盖范围和免责声明、问题明细、需人工核对附录,页眉 logo、页脚免责一句和页码)→ 加密归档,可重复下载;前端加「报告」标签页(选项、导出、内嵌预览、历次下载)。补了项目状态流转(递交 / 撤销递交 / 归档 / 撤销归档,致命未处理也能递交,递交后只读,都留痕)。产品可以从上传招标文件一路走到导出报告、标记递交。见 `docs/architecture/m3-plan.md` 进度。 +- 下一步:国动真实投标文件(用户晚些提供,到时先读标、请用户看清单,再查标一次);其余可做:第 4 步资质库和资质匹配、第 6 步前端补齐(键盘操作、批量处理、判断有误反馈)。 diff --git a/apps/web/src/components/ProjectHead.vue b/apps/web/src/components/ProjectHead.vue index 4a8cb21..38ddf10 100644 --- a/apps/web/src/components/ProjectHead.vue +++ b/apps/web/src/components/ProjectHead.vue @@ -2,19 +2,61 @@ /** * 项目头(原型的 projHead):项目名、采购人等、投标截止、状态和就绪;下面是「读标 / 查标」标签页。 * 就绪只在「已检查」时显示(G7),只表示致命问题都已处理,不代表投标文件没有问题(READY_TIP)。 - * 概览、报告页做到时在这里加标签页。 + * 状态操作(FR-M6-02):递交 / 撤销递交 / 归档 / 撤销归档。致命问题没处理完也可以递交(红线 4),确认框里说明; + * 递交后项目只读,可以撤销,都留痕。 */ -import { computed } from "vue"; +import { unwrap } from "@bid-doctor/api-client"; +import { useQueryClient } from "@tanstack/vue-query"; +import { computed, ref } from "vue"; import { RouterLink } from "vue-router"; +import ConfirmDialog from "@/components/ConfirmDialog.vue"; +import { api, errorTitle } from "@/lib/api"; +import { qk } from "@/lib/keys"; import { deadline, legalRegime, projectStatus, READY_TIP } from "@/lib/labels"; import type { ProjectDetail } from "@/lib/queries"; +import { toast } from "@/lib/toast"; -const props = defineProps<{ project?: ProjectDetail; tab: "checklist" | "check" }>(); +const props = defineProps<{ project?: ProjectDetail; tab: "checklist" | "check" | "report" }>(); +const qc = useQueryClient(); const p = computed(() => props.project); const meta = computed(() => p.value ? [p.value.purchaser, p.value.legal_regime !== "unknown" ? legalRegime(p.value.legal_regime) : "", p.value.procurement_method, p.value.code].filter(Boolean).join(" · ") : "", ); const due = computed(() => deadline(p.value?.deadline_at)); +type Action = "submit" | "unsubmit" | "archive" | "unarchive"; +const ACTIONS: Record = { + tracking: [{ action: "submit", label: "标记为已递交" }], + drafting: [{ action: "submit", label: "标记为已递交" }], + checked: [{ action: "submit", label: "标记为已递交" }], + submitted: [ + { action: "unsubmit", label: "撤销递交" }, + { action: "archive", label: "归档" }, + ], + archived: [{ action: "unarchive", label: "撤销归档" }], +}; +const actions = computed(() => (p.value ? ACTIONS[p.value.status] ?? [] : [])); +const confirmSubmit = ref(false); +const busy = ref(false); +async function run(action: Action) { + if (!p.value) return; + if (action === "submit" && !confirmSubmit.value) { + confirmSubmit.value = true; + return; + } + busy.value = true; + try { + await unwrap(api.POST("/projects/{project_id}/transitions", { params: { path: { project_id: p.value.id } }, body: { action } })); + confirmSubmit.value = false; + void qc.invalidateQueries({ queryKey: qk.project(p.value.id) }); + void qc.invalidateQueries({ queryKey: qk.projects }); + toast(action === "submit" ? "已标记为已递交,项目改为只读" : action === "archive" ? "已归档" : "已撤销"); + } catch (e) { + toast(errorTitle(e), { ok: false }); + } finally { + busy.value = false; + } +} + const checkTo = computed(() => p.value?.latest_check_run_id ? { name: "results", params: { projectId: p.value.id, runId: p.value.latest_check_run_id } } @@ -37,7 +79,13 @@ const checkTo = computed(() => — -
状态
{{ projectStatus(p.status) }}
+
+
状态
+
+ {{ projectStatus(p.status) }} + +
+
就绪
@@ -50,6 +98,11 @@ const checkTo = computed(() => + +

标记后项目改为只读,可以随时撤销。

+

还有 {{ p.readiness.open_fatal_count }} 项致命问题没有处理。是否递交由你决定,系统不会阻止。

+
diff --git a/apps/web/src/components/ProjectRow.vue b/apps/web/src/components/ProjectRow.vue index 1e23ca4..2e86957 100644 --- a/apps/web/src/components/ProjectRow.vue +++ b/apps/web/src/components/ProjectRow.vue @@ -26,8 +26,10 @@ const home = computed(() => { return { name: "results", params: { projectId: id, runId: p.value.latest_check_run_id } }; return { name: "checklist", params: { projectId: id } }; }); -/** 「下一步」:骨架阶段只有上传、解析进度、清单三个去处,其余动作先落到清单页 */ -const next = computed(() => home.value); +/** 「下一步」:导出报告去报告页,其余同默认入口 */ +const next = computed(() => + p.value.next_action === "export_report" ? { name: "report", params: { projectId: p.value.id } } : home.value, +);