Files
bid-doctor/server/migrations/versions/0009_report_job.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

65 lines
2.8 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.

"""检查报告(查标第 5 步,FR-M3-09):报告后台生成,生成过程记在 report_job;报告本身生成后不再改(report)。
- report_job:每次点「导出报告」一行,排队 → 生成 → 成功(指向 report)/ 失败;前端轮询兜底,事件 report.ready 推送;
- report 加 open_fatal_at_export(导出时还没处理的致命问题数,报告首页和报告列表都显示)和 options(带不带出处、附录);
- 通知加 report_succeeded / report_failed。
Revision ID: 0009
"""
from __future__ import annotations
from alembic import op
revision = "0009"
down_revision = "0008"
def upgrade() -> None:
op.get_bind().exec_driver_sql("""
ALTER TABLE report ADD COLUMN open_fatal_at_export int NOT NULL DEFAULT 0;
ALTER TABLE report ADD COLUMN options jsonb NOT NULL DEFAULT '{}';
CREATE TABLE report_job (
id uuid PRIMARY KEY,
tenant_id uuid NOT NULL,
project_id uuid NOT NULL,
check_run_id uuid NOT NULL,
status text NOT NULL DEFAULT 'queued' CHECK (status IN ('queued', 'running', 'succeeded', 'failed')),
options jsonb NOT NULL DEFAULT '{}',
report_id uuid,
error_code text,
requested_by uuid NOT NULL REFERENCES user_account (id),
created_at timestamptz NOT NULL DEFAULT now(),
finished_at timestamptz,
UNIQUE (tenant_id, id),
CHECK (status <> 'succeeded' OR report_id IS NOT NULL),
FOREIGN KEY (tenant_id, project_id) REFERENCES project (tenant_id, id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id, check_run_id) REFERENCES check_run (tenant_id, id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id, report_id) REFERENCES report (tenant_id, id) ON DELETE SET NULL (report_id)
);
CREATE INDEX ON report_job (tenant_id, project_id, created_at);
ALTER TABLE report_job ENABLE ROW LEVEL SECURITY;
ALTER TABLE report_job FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON report_job
USING (tenant_id = current_tenant()) WITH CHECK (tenant_id = current_tenant());
ALTER TABLE notification DROP CONSTRAINT notification_kind_check;
ALTER TABLE notification ADD CONSTRAINT notification_kind_check
CHECK (kind IN ('parse_succeeded', 'parse_failed', 'check_succeeded', 'check_failed', 'report_succeeded',
'report_failed', 'quota_80', 'quota_100', 'subscription_activated', 'subscription_expiring'));
""")
def downgrade() -> None:
op.get_bind().exec_driver_sql("""
DROP TABLE report_job;
ALTER TABLE report DROP COLUMN options;
ALTER TABLE report DROP COLUMN open_fatal_at_export;
ALTER TABLE notification DROP CONSTRAINT notification_kind_check;
ALTER TABLE notification ADD CONSTRAINT notification_kind_check
CHECK (kind IN ('parse_succeeded', 'parse_failed', 'check_succeeded', 'check_failed',
'quota_80', 'quota_100', 'subscription_activated', 'subscription_expiring'));
""")