运营 CSV 导入:Schema 合同、行级去重与落库对账
CSV 能被表格软件打开,不代表金额、日期、空值和客户身份会被系统一致解释。本文设计隔离上传、版本化列合同、规范行键、冲突分桶和可重建的导入收据。
本文要点
- 主题聚焦:TypeScript、DevOps、独立开发
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
同一份 CSV 在 Excel 中显示“正常”,进入服务后却可能把 00123 变成 123、把 1,20 当两个字段、把空字符串当成删除指令,或把 03/04/26 解释成两个不同日期。更危险的是任务处理到 70% 崩溃,操作者重新上传改过一行的文件,系统无法说明哪些客户已经创建、哪些只是重复、哪些被后一次覆盖。
安全文件上传管道处理大小、类型、恶意内容和隔离存储;本篇从文件通过安全扫描后开始,只讨论表格语义到业务记录的可核验映射。训练数据 Schema 演进面向训练语料兼容性,这里处理运营人员提交的有限业务行,不共享“遇到未知列就尽量猜”的策略。
导入合同要固定方言和业务类型
RFC 4180 给出逗号、引号和 CRLF 的通用格式,现实文件还需要明确 encoding、BOM、delimiter、header、换行、引号、空值 token、最大逻辑记录和多行字段。发布 csvImportContract.v1,为每列声明稳定 field ID、显示别名、业务类型、是否必填、默认值、时区、decimal scale、枚举版本和规范化规则。日期只接受合同列出的 ISO 格式;金额使用十进制最小单位;标识符永远按字符串读取。
schema: csvImportContract.v1
contract: customer-catalog@6
encoding: UTF-8
delimiter: ","
unknownColumns: REJECT
duplicatePolicy: QUARANTINE_CONFLICT
fields:
customer_external_id: {type: string, required: true, trim: ascii-edge}
plan_code: {type: enum, vocabulary: plans@12}
effective_at: {type: datetime, format: RFC3339}
Unicode NFKC 可能把视觉相近字符折叠,适合某些搜索字段,却可能改变外部 ID;因此每个字段单独声明 NFC、NFKC 或不规范化,并同时保留原值。升级合同不得改变已封存 import 的解释。旧文件需要新语义时创建 migration generation,输出旧值、新值和原因,不能重跑后覆盖旧 staging。
每一逻辑行都获得稳定去向
接收端先计算原文件 SHA256,保存 object generation、租户、contract digest、上传者、字节数和 logical row count。parser 正确处理引号内换行,错误位置同时报告逻辑行号与原始 byte range。解析结果写隔离 staging,以 (importId, logicalRow) 唯一;worker 重启重复写相同行时,规范 payload hash 相同即返回原 receipt,不同则进入 ROW_MUTATION_CONFLICT。
规范化后按合同中的业务键生成 rowKey。同一文件内相同 key 且 payload 相同记 DUPLICATE_IDENTICAL,只保留一个候选;同 key 不同 payload 记 DUPLICATE_CONFLICT,整组隔离,不能靠“最后一行获胜”。跨文件去重使用 source namespace、业务 key 和 import mode,不使用整行 hash 代替身份。INSERT_ONLY、UPSERT_IF_VERSION 与 REPLACE_SNAPSHOT 是不同合同,必须由操作者显式选择。
预览报告给出 VALID、INVALID_SCHEMA、UNKNOWN_REFERENCE、DUPLICATE_IDENTICAL、DUPLICATE_CONFLICT、STALE_TARGET 各桶数量及样例,并对有效规范行计算 candidateRoot。批准只绑定 import ID、contract、mode、candidate root 和数量;原文件或引用词表变化即重新预览。这里的批准只允许把已验证行晋级到业务表,不触发邮件、退款等外部动作;这与批量客户操作的效果控制面保持分离。
晋级和对账必须守恒
INSERT_ONLY 与 UPSERT_IF_VERSION 可由晋级器分小批读取 staging,以自然键和 expected version 条件写入;业务记录、row receipt 和 outbox 同事务。REPLACE_SNAPSHOT 不能逐行改 active 集合:先构建完整不可变 dataset generation,验证 candidate root 和全量成员,再以 CAS 原子切换 active pointer;新文件未包含的旧行仅在切换后对新 reader 消失。读请求固定 generation,旧代排空后才回收。连接中断按 receipt 或 active generation 确认。PostgreSQL 事务隔离只提供实现基础。
最终 importReconciliation.v1 必须满足:逻辑输入行总数等于 blank、invalid、duplicate、skipped、inserted、updated 与 quarantined 的互斥总和;每个候选 row 恰有一个终态 receipt;按规范业务键重算的目标摘要与 committed root 或 active dataset root 相符。错误 CSV 导出时以纯文本转义以 = + - @ 开头的单元格,避免表格公式注入;原始 PII、错误样例和 staging 按 AI 数据隐私合规的用途与保留期删除,并生成 deletion receipt。
故障测试覆盖 UTF-8 BOM、非法字节、嵌入换行、重复 header、Unicode 合成/分解、同 key 冲突、词表升级、小批崩溃、两个晋级器并发、snapshot 构建中断及 pointer CAS 前后崩溃。目标是跨租户写入为 0、同一 row 重复晋级效果为 0、reader 观察到半套 snapshot 为 0、所有计数守恒、错误定位 100% 可回到 byte range。
发布门要求一万份生成式方言夹具解析结果与独立 oracle 完全一致;十万行导入的 reconciliation 差额为 0;重复执行五次目标摘要不变;未知列、非法日期和冲突 key 的误接纳为 0;p95 十万行预览低于 90 秒。上线先只生成预览,再对内部目录启用 INSERT_ONLY,最后开放带版本的 upsert。若守恒失败或错误率突增,停止晋级并把入口 alias 切回旧 contract;已经提交的行按 receipt 生成显式反向 migration,绝不删除 staging 后假装整批未发生。
Related