feat(export): 导出与交付(M6)—— 前置检查、目录树预览、分卷打包与 7 天过期清理
客户最关心的那条链:precheck(不通过就没有 force 分支,铁律 10)→ 目录树预览 (与真正打包共用同一份构建代码,预览不会说谎)→ 异步分卷(每卷都是完整可解的 ZIP) → 产物落表按行判可下载(过期回「已被清理、请重新导出」)。 契约没有但界面必需的 GET /exports/precheck 一并补上(否则按钮要点一次才知道能不能导)。 顺带修 F-11:severity 口径统一为 INFO=1/WARN=2/ERROR=3(三处注释曾互相矛盾, 按直觉写会把「提示」当阻断,正常数据也导不出来)。 验证:scripts/e2e-m6-export.py 60/60、e2e-m6-export-split.py 33/33; ZIP 的 UTF-8 标志位(general purpose bit 11)与 sha256 都有断言。
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
-- ============================================================================
|
||||
-- V11:导出产物明细表(支持分卷)
|
||||
--
|
||||
-- 为什么不能继续用 collect_export_task.result_files 这个 JSON 字段
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 契约 §12.4 的 `splitSizeMb`(默认 50MB)意味着一次导出可能产出**多个** zip;
|
||||
-- 而每个分卷都要能单独下载、单独校验(sha256)、单独记大小。
|
||||
-- 这些需求落到 JSON 字段上有两个具体问题:
|
||||
--
|
||||
-- ① **下载要按主键取**:`GET /exports/{id}/file?idx=2` 现在靠"解析 JSON
|
||||
-- 再取第 n 个"。一旦结果文件很多,每次下载都要把整个 JSON 读出来解析;
|
||||
-- 更要紧的是**没法用主键做授权**(越权判断只能靠"这个 id 属于这个任务吗"
|
||||
-- 这种间接推理)。
|
||||
-- ② **过期清理要能按文件做**:清理时要把磁盘文件删掉、把行标成不可下载。
|
||||
-- JSON 字段没法部分更新,只能整段重写 —— 而重写期间别人正在下载,
|
||||
-- 就会读到"一半旧一半新"的内容。
|
||||
--
|
||||
-- 所以:**一次导出 = 一行 task + N 行产物**。result_files 保留(老数据兼容),
|
||||
-- 新代码一律读写这张表。
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS collect_export_file (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
export_id BIGINT NOT NULL,
|
||||
task_id BIGINT,
|
||||
-- 第几卷(单卷时是 0)—— 文件名里带它,客户按顺序解压
|
||||
part_no INTEGER NOT NULL DEFAULT 0,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_path VARCHAR(512) NOT NULL,
|
||||
file_size BIGINT NOT NULL DEFAULT 0,
|
||||
checksum VARCHAR(80),
|
||||
-- 1 可下载 / 0 已清理(文件已从磁盘删掉,但这一行**留着**:
|
||||
-- "当时确实产出过这个包"是审计要保留的事实,删行会让历史对不上)
|
||||
downloadable SMALLINT NOT NULL DEFAULT 1,
|
||||
-- ⚠️ 审计列:实体继承 TenantEntity → FillMetaObjectHandler 会填 create_by/update_by。
|
||||
-- 少了这两列的表现是"插入直接报 column does not exist",而且**编译期不报**。
|
||||
-- 这个坑在本项目已经踩过三次(collect_task_org / collect_export_task / collect_message),
|
||||
-- 所以加了 scripts/check-entity-columns.py 做机械校验 —— 见 F-24。
|
||||
create_by BIGINT,
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_by BIGINT,
|
||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted SMALLINT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
COMMENT ON TABLE collect_export_file IS '导出产物(一个分卷一行);分卷大小见 collect_export_task.split_size_mb';
|
||||
COMMENT ON COLUMN collect_export_file.part_no IS '分卷序号,从 0 开始;单卷时恒为 0';
|
||||
COMMENT ON COLUMN collect_export_file.downloadable IS '1 可下载 / 0 已清理(文件已删,但这一行保留作审计)';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_export_file_export
|
||||
ON collect_export_file (export_id, part_no);
|
||||
|
||||
-- 过期清理要扫"已过期的导出任务",按 (status, expire_time) 走
|
||||
CREATE INDEX IF NOT EXISTS idx_export_task_expire
|
||||
ON collect_export_task (status, expire_time);
|
||||
Reference in New Issue
Block a user