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:
64
server/migrations/versions/0009_report_job.py
Normal file
64
server/migrations/versions/0009_report_job.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""检查报告(查标第 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'));
|
||||
""")
|
||||
Reference in New Issue
Block a user