产品定位一句话:文件驱动 + 模板可选,web 单次导入上限 5 万条;小文件同步、大文件异步流式;逐行尽力 + 失败行回收,不做 DB 事务;hook 可关。对齐 Salesforce 导入向导 / Airtable,而非传统 ERP 强制模板。
背景 / 问题(均经代码确认)
当前列表(对象视图)的「导入」是只解析、只逐条新增的最小实现,离产品级导入差距很大。
前端(objectui)现状
- 「导入」走
plugin-grid 的 ImportWizard:在浏览器里全量解析 CSV/Excel → 字段映射 → 逐行校验。
- 提交时在
for 循环里逐条调用单记录新建接口(dataSource.create → POST /api/v1/data/:object),100 行 = 100 次串行请求,非批量、非事务、只新增。
- 映射模板只存浏览器
localStorage(按用户/浏览器/对象隔离),无法共享。
- 没有「下载导入模板」。
- 大文件直接崩:全量进浏览器内存 + 逐条请求,几万行就卡死。
后端(framework)现状
- 已有
POST /data/:object/import(CSV/JSON、dryRun、5000 行上限、逐行结果),但:
- 前端根本没调用它(SDK
@objectstack/client 也还没有 data.import 方法)。
- 内部逐行
createData(),只 insert,不支持更新/upsert;无异步任务、无历史、无失败行回收。
- spec 里已定义但未接线:
DeduplicationStrategy(skip/update/create_new/fail)+ matchFields、ImportValidationConfig、ExportImportTemplate、FieldMappingEntry(含 lookup/日期 transform)。
核心痛点:① 只能新增、不能更新;② 无下载模板;③ 大文件跑不了;④ 无进度/失败行/历史。
关键决策(本轮敲定,先读这段)
- 单次导入上限 = 5 万条。超过引导拆分 / 走批量 API。(对标:Salesforce web 向导封顶 5 万;Airtable 单次仅 2.5 万)
- 小文件同步、大文件异步:小文件(≤ 约 2000 行)前端全解析 + 行内改;大文件前端只解析样本做映射预览,原始文件上传服务端流式处理,走异步 job。
- 不做 DB 事务。默认"逐行尽力 + 失败行 CSV";"后悔药"用 job 级逻辑撤销(记录本次新增/更新过什么,可回滚),不用 DB 事务。
- 逐行走
/import,不用 createMany。createMany 第一条失败即整批 throw 且不报行号,丢掉错误定位/权限/dryRun。
- hook 可关:导入向导提供"执行自动化/触发器"开关,大批量默认关(避竞态、换吞吐)。(Airtable 教训:per-row 自动化 + 批量导入 = 火药桶,官方都劝导入前关)
- Excel 上限 ≤ CSV;Excel 走基础导入,Upsert/大批量引导转 CSV。
- 公式字段无需特殊处理:当前只有一种(虚拟/读时算、不落库),导入时被静默忽略;"存库公式"是待产品确认项,现不存在。
- 共享映射模板本轮不做(见「暂不纳入范围」)。
已确认的现状约束(代码位置在此)
实现前把写管道真实行为核清了,避免基于假设设计。
✅ 现成可依赖
- Hooks/触发器会执行:
/import 每行 createData → engine.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 级撤销 来源。
- 共识:没有一家在浏览器里硬扛大文件,都是限量 + 服务端异步 + 拆分。
背景 / 问题(均经代码确认)
当前列表(对象视图)的「导入」是只解析、只逐条新增的最小实现,离产品级导入差距很大。
前端(objectui)现状
plugin-grid的ImportWizard:在浏览器里全量解析 CSV/Excel → 字段映射 → 逐行校验。for循环里逐条调用单记录新建接口(dataSource.create→POST /api/v1/data/:object),100 行 = 100 次串行请求,非批量、非事务、只新增。localStorage(按用户/浏览器/对象隔离),无法共享。后端(framework)现状
POST /data/:object/import(CSV/JSON、dryRun、5000 行上限、逐行结果),但:@objectstack/client也还没有data.import方法)。createData(),只 insert,不支持更新/upsert;无异步任务、无历史、无失败行回收。DeduplicationStrategy(skip/update/create_new/fail)+matchFields、ImportValidationConfig、ExportImportTemplate、FieldMappingEntry(含 lookup/日期 transform)。核心痛点:① 只能新增、不能更新;② 无下载模板;③ 大文件跑不了;④ 无进度/失败行/历史。
关键决策(本轮敲定,先读这段)
/import,不用createMany。createMany第一条失败即整批 throw 且不报行号,丢掉错误定位/权限/dryRun。已确认的现状约束(代码位置在此)
实现前把写管道真实行为核清了,避免基于假设设计。
✅ 现成可依赖
/import每行createData→engine.insertfirebeforeInsert(engine.ts:2105)、afterInsert(engine.ts:2155),并发 realtimedata.record.created(engine.ts:2162)。与手动新建一致。validateRecord(必填/类型/唯一)+evaluateValidationRules(对象校验规则)(engine.ts:2150)。/import返回results:[{row, ok, id?/error?, code?}](rest-server.ts:3187)。"5 万里错哪条"能答——前端消费即可。createData无自动事务,仅能靠 context 外部传transaction(engine.ts:2102),/import未传;逐行独立落库,第 N 行失败前 N-1 行已提交不回滚。→ 本轮不补 DB 事务,改做 job 级撤销。createMany不适合导入:createManyData(protocol.ts:2764)无 try/catch,第一条失败整批 throw、不报行号,且beforeInsert/afterInsert整批只触发一次(语义变)。📌 性能估算(说明为何 5 万必须异步)
/import是串行逐行(for+await createData)。5 万行:任何同步 HTTP(网关超时 ~30–120s)都扛不住 → 5 万必须走异步 job(L1),且大批量默认关 hook。
分层方案 L0–L4
设计原则:激活 spec 里已设计好的 schema,而非另起炉灶;导入与手动新建走同一条 protocol 管道(尊重必填/唯一/校验/权限,hook 可选)。
L0 — 打通现有能力(基线接线)
/import,不再逐条create(仍是服务端逐行,但拿到逐行结果 + dryRun)。data.import()(现只有create/createMany,缺import)。L1 — 异步 Job(5 万的前置门槛,非可选)
import_job:POST .../import立即返回jobId(202),状态queued→validating→running→done/failed。L2 — 写入模式(Upsert / 改记录)+ 边界处理
DeduplicationStrategy+matchFields,向导可选:仅新增 / 仅更新 / 新增或更新(Upsert)。external_id)。/import逐行(可报行号),不用createMany。a@x/A@x建重);L3 — 模板与智能映射
.xlsx/.csv。不持久化、非必须。服务端共享暂不做。import_templatetransform:'lookup')。transform已在 spec)。L4 — 安全、可观测与吞吐控制
import_job记本次新增 id / 更新前旧值,可一键撤销。不依赖 DB 事务,异步大批量也能撤。大文件 & 格式策略
需要改动的仓库
framework(主战场)
DeduplicationStrategy/ImportValidationConfig/ExportImportTemplate/FieldMappingEntry正式纳入 import 契约;声明 import/job 端点类型;5 万上限、hook 开关、Upsert 边界语义入契约。/import增加 upsert+matchFields、dryRun、流式解析、异步 job、失败行回写、hook 开关、上限调整(5000→5 万,超限 413)、模板生成。data.import()及 job 进度查询。(改完需重建 dist 前端才能消费)import_job(异步任务 + 历史 + 审计 + 撤销载体)。(暂不做)import_template共享模板objectui(消费端)
import(),转调新 SDK(替换逐条 create)。grid.import.*文案。交付优先级
/import+ 小文件 dryRun) + L3「下载导入模板」 + L2「Upsert 改记录 + 边界处理」。暂不纳入范围
import_template/ExportImportTemplate/ 组织级共享):先不做,保留现状 localStorage。待核心导入落地验证需求后再评估。界面 UI 设计(向导四步)
沿用现有
ImportWizard对话框 + 步骤条,扩展为四步。⊕ = 本轮新增。步骤 1 · 上传
步骤 2 · 映射(+ 预检)
步骤 3 · 预览与写入模式
步骤 4 · 导入结果
补充:模板概念澄清(避免混淆)
「模板」指两个不同东西:
A. 导入模板文件(下载来填数据的空表):按对象字段动态生成、带表头/必填标注/示例行;系统生成→用户下载→填数据→上传数据文件。不持久化、非必须(纯辅助,用户也可拿现成 Excel 直接导)。
B. 字段映射模板(记住「列→字段」配置):只存映射规则,不含业务数据;用户映射好一次后「存为模板」下次复用。现状
localStorage(私有、换机即失);共享版(服务端import_template)本轮不做。对标小结(为何这么定)