Skip to content

feat(import): 产品级列表导入(L0–L4:批量/dryRun、Upsert 改记录、导入模板、异步 Job、校验审计) #2504

Description

@baozhoutao

产品定位一句话:文件驱动 + 模板可选,web 单次导入上限 5 万条;小文件同步、大文件异步流式;逐行尽力 + 失败行回收,不做 DB 事务;hook 可关。对齐 Salesforce 导入向导 / Airtable,而非传统 ERP 强制模板。

背景 / 问题(均经代码确认)

当前列表(对象视图)的「导入」是只解析、只逐条新增的最小实现,离产品级导入差距很大。

前端(objectui)现状

  • 「导入」走 plugin-gridImportWizard:在浏览器里全量解析 CSV/Excel → 字段映射 → 逐行校验。
  • 提交时在 for 循环里逐条调用单记录新建接口(dataSource.createPOST /api/v1/data/:object),100 行 = 100 次串行请求,非批量、非事务、只新增。
  • 映射模板只存浏览器 localStorage(按用户/浏览器/对象隔离),无法共享。
  • 没有「下载导入模板」。
  • 大文件直接崩:全量进浏览器内存 + 逐条请求,几万行就卡死。

后端(framework)现状

  • 已有 POST /data/:object/import(CSV/JSON、dryRun5000 行上限、逐行结果),但:
    • 前端根本没调用它(SDK @objectstack/client 也还没有 data.import 方法)。
    • 内部逐行 createData(),只 insert,不支持更新/upsert;无异步任务、无历史、无失败行回收。
  • spec 里已定义但未接线:DeduplicationStrategy(skip/update/create_new/fail)+ matchFieldsImportValidationConfigExportImportTemplateFieldMappingEntry(含 lookup/日期 transform)。

核心痛点:① 只能新增、不能更新;② 无下载模板;③ 大文件跑不了;④ 无进度/失败行/历史。


关键决策(本轮敲定,先读这段)

  1. 单次导入上限 = 5 万条。超过引导拆分 / 走批量 API。(对标:Salesforce web 向导封顶 5 万;Airtable 单次仅 2.5 万)
  2. 小文件同步、大文件异步:小文件(≤ 约 2000 行)前端全解析 + 行内改;大文件前端只解析样本做映射预览,原始文件上传服务端流式处理,走异步 job。
  3. 不做 DB 事务。默认"逐行尽力 + 失败行 CSV";"后悔药"用 job 级逻辑撤销(记录本次新增/更新过什么,可回滚),不用 DB 事务。
  4. 逐行走 /import,不用 createManycreateMany 第一条失败即整批 throw 且不报行号,丢掉错误定位/权限/dryRun。
  5. hook 可关:导入向导提供"执行自动化/触发器"开关,大批量默认关(避竞态、换吞吐)。(Airtable 教训:per-row 自动化 + 批量导入 = 火药桶,官方都劝导入前关)
  6. Excel 上限 ≤ CSV;Excel 走基础导入,Upsert/大批量引导转 CSV。
  7. 公式字段无需特殊处理:当前只有一种(虚拟/读时算、不落库),导入时被静默忽略;"存库公式"是待产品确认项,现不存在。
  8. 共享映射模板本轮不做(见「暂不纳入范围」)。

已确认的现状约束(代码位置在此)

实现前把写管道真实行为核清了,避免基于假设设计。

✅ 现成可依赖

  • Hooks/触发器会执行:/import 每行 createDataengine.insert fire beforeInsert(engine.ts:2105)、afterInsert(engine.ts:2155),并发 realtime data.record.created(engine.ts:2162)。与手动新建一致。
  • 逐行校验会走:写库前跑 validateRecord(必填/类型/唯一)+ evaluateValidationRules(对象校验规则)(engine.ts:2150)。
  • 逐行错误定位现成:/import 返回 results:[{row, ok, id?/error?, code?}](rest-server.ts:3187)。"5 万里错哪条"能答——前端消费即可。

⚠️ 现状不具备、需新建

  • 事务/回滚 = 0:单条 createData 无自动事务,仅能靠 context 外部传 transaction(engine.ts:2102),/import 未传;逐行独立落库,第 N 行失败前 N-1 行已提交不回滚。→ 本轮不补 DB 事务,改做 job 级撤销。
  • createMany 不适合导入:createManyData(protocol.ts:2764)无 try/catch,第一条失败整批 throw、不报行号,且 beforeInsert/afterInsert 整批只触发一次(语义变)。
  • hook 在大批量是性能炸弹 & 竞态源:串行逐行 + 每行触发,轻 hook 每行多 1~2 次 DB 往返,重 hook(外部调用/更新关联)每行几十上百 ms。

📌 性能估算(说明为何 5 万必须异步)

/import串行逐行(for + await createData)。5 万行:

场景 每行 5 万行总耗时
无 hook,本地库 ~3–5ms 2.5–4 分钟
轻 hook(每行 +1~2 查询) ~10–15ms 8–12 分钟
重 hook(外部/关联) 50–150ms 40 分钟–2 小时

任何同步 HTTP(网关超时 ~30–120s)都扛不住 → 5 万必须走异步 job(L1),且大批量默认关 hook


分层方案 L0–L4

设计原则:激活 spec 里已设计好的 schema,而非另起炉灶;导入与手动新建走同一条 protocol 管道(尊重必填/唯一/校验/权限,hook 可选)。

L0 — 打通现有能力(基线接线)

  • 前端改为调用服务端 /import,不再逐条 create(仍是服务端逐行,但拿到逐行结果 + dryRun)。
  • SDK 新增 data.import()(现只有 create/createMany,缺 import)。
  • 小文件:向导第二步接 dryRun 全量预检,返回逐行错误,行内修正后再导。

L1 — 异步 Job(5 万的前置门槛,非可选)

  • 落系统对象 import_job:POST .../import 立即返回 jobId(202),状态 queued→validating→running→done/failed
  • 服务端流式解析上传文件(不整文件进内存);进度查询/订阅;可取消、可重试失败行。
  • 天然带导入历史/审计(谁、何时、多少、成功/失败、错误文件)。
  • 失败行回写:失败行 + 原因导出 CSV,供修正后重传。

L2 — 写入模式(Upsert / 改记录)+ 边界处理

  • 激活 DeduplicationStrategy + matchFields,向导可选:仅新增 / 仅更新 / 新增或更新(Upsert)
  • matchFields 默认取对象唯一字段 / 外部 ID(如 external_id)。
  • 提交:/import 逐行(可报行号),不用 createMany
  • 必须 spec 的 Upsert 边界 case(Airtable 踩过的坑):
    • 文件内匹配键重复 → 只用第一行,后续同键忽略(并告警);
    • 匹配键为空 → 当新增;
    • 匹配是否大小写敏感必须明确(默认不敏感或提供归一化,避免 a@x/A@x 建重);
    • 可选"跳过空值"(别拿空覆盖已有);
    • 可选"自动创建缺失的选项"(单/多选遇新值)。

L3 — 模板与智能映射

  • 下载导入模板:按对象字段动态生成带表头(必填标注、枚举可选值、示例行)的 .xlsx/.csv。不持久化、非必须。
  • 字段映射模板:本轮保留现状 localStorage(个人、按浏览器)。服务端共享 import_template 暂不做。
  • 字段类型/映射自动识别(抄 Airtable 强项):映射时智能猜列→字段,减少手工。
  • 关系/lookup 解析:CSV 写「张三」/邮箱,导入自动换记录 id(transform:'lookup')。
  • 类型转换:日期、布尔、数字(transform 已在 spec)。

L4 — 安全、可观测与吞吐控制

  • 尊重平台规则:必填、唯一、校验规则、字段级权限(与手动新建同管道)。
  • hook 开关:向导提供"导入时执行自动化/触发器",大批量默认关(换吞吐、避竞态);开启则逐行触发。
  • job 级撤销(逻辑回滚):import_job 记本次新增 id / 更新前旧值,可一键撤销。不依赖 DB 事务,异步大批量也能撤。
  • 全链路审计(import_job 即载体)。

大文件 & 格式策略

量级 解析位置 预检形态 修错方式
≤ 约 2000 行(小) 前端全解析 全量 dryRun,表格标红 行内直接改
约 2000 ~ 5 万(大) 前端仅样本预览 + 服务端流式 样本预检(把好映射/类型/必填第一关)+ 边导边校验 下载失败行 CSV → Excel 改 → 重传
> 5 万 引导拆分 / 批量 API,web 向导不受理
  • 不做"独立全量预检 phase":对齐 Salesforce/Airtable,大文件边导边校验 + 错误 CSV,不为全量再扫一遍。
  • Excel 上限 ≤ CSV(xlsx 解析更重、同行数占 MB 更多);Excel 走基础导入,要 Upsert/大批量引导转 CSV。

需要改动的仓库

framework(主战场)

  • packages/spec:把 DeduplicationStrategy/ImportValidationConfig/ExportImportTemplate/FieldMappingEntry 正式纳入 import 契约;声明 import/job 端点类型;5 万上限、hook 开关、Upsert 边界语义入契约。
  • packages/rest:/import 增加 upsert+matchFields、dryRun、流式解析、异步 job、失败行回写、hook 开关、上限调整(5000→5 万,超限 413)、模板生成。
  • packages/client(SDK):新增 data.import() 及 job 进度查询。(改完需重建 dist 前端才能消费)
  • 系统对象所在包:新增 import_job(异步任务 + 历史 + 审计 + 撤销载体)。(import_template 共享模板 暂不做)

objectui(消费端)

  • packages/data-objectstack(adapter):dataSource 增加 import(),转调新 SDK(替换逐条 create)。
  • packages/plugin-grid(ImportWizard):样本解析(大文件不再全量进内存)、dryRun 预检、写入模式 + matchFields、下载模板、异步 job 进度条、失败行下载、hook 开关。(映射模板仍 localStorage)
  • packages/app-shell(ObjectView):导入入口切到新的批量/异步通道;权限位。
  • components / react(按需):UI 原子 + grid.import.* 文案。

交付优先级

  • P0:L0(走 /import + 小文件 dryRun) + L3「下载导入模板」 + L2「Upsert 改记录 + 边界处理」。
  • P1:L1(异步 job + 流式 + 进度 + 失败行 CSV + 历史,撑到 5 万) + 样本预检 + hook 开关 + L3 lookup。
  • P2:job 级撤销、字段类型自动识别、xlsx 原生解析优化。

暂不纳入范围

  • 共享映射模板(服务端 import_template / ExportImportTemplate / 组织级共享):先不做,保留现状 localStorage。待核心导入落地验证需求后再评估。
  • DB 事务 / 全有或全无模式:不做,用逐行尽力 + 失败行 CSV + job 撤销替代。
  • > 5 万单次导入:不做,引导拆分 / API。
  • 存库公式:代码里不存在,待产品确认是否规划;若做,导入策略需区分(虚拟=忽略;存库=不收用户值、写管道重算落列)。

界面 UI 设计(向导四步)

沿用现有 ImportWizard 对话框 + 步骤条,扩展为四步。⊕ = 本轮新增。

步骤 1 · 上传

[ 导入 线索 ]                                     (×)
 (1 上传)  2 映射   3 预览与模式   4 导入
┌───────────────────────────────────────────────┐
│   ⬆  拖放 CSV / Excel 文件到这里,或点击浏览      │   ← 现有
│      也可从 Excel / 表格复制后粘贴 (⌘V)          │
└───────────────────────────────────────────────┘
 ⬇ 下载导入模板 [新] 按当前字段生成带表头空表  [下载]   ← ⊕ L3
 ⓘ 单次最多 5 万条;更大量请拆分或用批量 API           ← ⊕ 上限提示
                                     [取消]   [下一步]

步骤 2 · 映射(+ 预检)

 1 上传  (2 映射)  3 预览与模式   4 导入
 映射模板 [选择模板… ▾] [💾 存为模板] 仅本机保存   ← 现有(localStorage)
 ┌ 文件列 ── 目标字段 ── 类型 ──────── 状态 ──┐
 │ 客户名称   name ▾      文本           已映射   │
 │ 邮箱       email ▾     邮箱           已映射   │
 │ 负责人     owner ▾    「按邮箱查找」   已映射   │ ← ⊕ L3 lookup
 │ 金额       amount ▾    数字           类型提示 │  (类型自动识别 ⊕ L3)
 │ 内部备注   — 跳过 —                    跳过     │
 └───────────────────────────────────────────┘
 ⚠ 预检:小文件全量 / 大文件样本 —— 98% 可导入,2% 有错 · 查看  ← ⊕ L0
                                     [上一步] [下一步]

步骤 3 · 预览与写入模式

 1 上传  2 映射  (3 预览与模式)  4 导入
 ○ 仅新增   ● 新增或更新(Upsert)   ○ 仅更新    ← ⊕ L2
   匹配字段 [邮箱 ▾]   ☐ 跳过空值   ☐ 自动建选项   ← ⊕ L2 边界
 ☐ 导入时执行自动化/触发器(大批量默认关)          ← ⊕ L4 hook 开关
 ┌ # ── 客户名称 ── 邮箱 ─────────── 金额 ──┐
 │ 1  华勤科技    li@huaqin.com     120,000 │
 │ 2  明志电子    wang@mz.com        85,000 │
 │ 3  星野贸易   ⚠ 格式错误(小文件可行内改)  —│ ← 现有(行内修正)
 └──────────────────────────────────────┘
                              [上一步] [● 开始导入]

步骤 4 · 导入结果

 1 上传  2 映射  3 预览与模式  (4 导入)
 ⏳ 大文件:导入中 ▓▓▓▓▓░░░ 62%(import_job 进度)   ← ⊕ L1
 ✓ 导入完成
 ┌ 总计 ─┐ ┌ 成功 ────────┐ ┌ 失败 ─┐
 │ 50000 │ │ 48700        │ │ 1300  │
 │       │ │ 新增4.1万·更新7.7千│ │     │      ← 成功区分新增/更新(L2)
 └───────┘ └──────────────┘ └───────┘
 🗎 1300 行失败,可下载修正后重传   [⬇ 下载失败行 CSV]   ← ⊕ L1
 ↩ 撤销本次导入(job 级回滚)                          ← ⊕ L4
                                            [完成]

静态示意,展示信息架构与新增控件位置,非最终视觉。小文件走同步 + 行内改;大文件先「导入中 + 进度条」再切结果态。


补充:模板概念澄清(避免混淆)

「模板」指两个不同东西:

A. 导入模板文件(下载来填数据的空表):按对象字段动态生成、带表头/必填标注/示例行;系统生成→用户下载→填数据→上传数据文件不持久化、非必须(纯辅助,用户也可拿现成 Excel 直接导)。

B. 字段映射模板(记住「列→字段」配置):只存映射规则,不含业务数据;用户映射好一次后「存为模板」下次复用。现状 localStorage(私有、换机即失);共享版(服务端 import_template)本轮不做

一句话:A 是"给你空表填数据",B 是"记住这类文件怎么对字段";两者都非必须,整体文件驱动 + 模板可选


对标小结(为何这么定)

  • Salesforce:web 向导封顶 5 万,更大量走 Data Loader / Bulk API;跑完给 error CSV。→ 我们的 5 万上限 + 失败行 CSV 来源。
  • Airtable:单次仅 2.5 万 / 5MB(CSV 和 Excel 同限),超了拆分 / sync / API;导入会触发 Automations 但公认竞态/洪水/炸额度,官方劝导入前关;Merge(Upsert)有一堆边界坑;"后悔药"是导前 base 快照。→ 我们的 hook 开关、Upsert 边界 spec、job 级撤销 来源。
  • 共识:没有一家在浏览器里硬扛大文件,都是限量 + 服务端异步 + 拆分

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions