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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions