Ruled: 5904855243 · letter Q1 A · Q2 C — ISecurityService.getWritableFields is added and the template narrows by it (round 2 on PR #20683, Part of #18386: acceptance 6 is a hand check in four spreadsheet apps, so the card stays open for it after the PR lands; ruling 5906743185); the column rule keeps system and drops hidden; acceptance 1 is corrected accordingly · 2026-09-30T05:41Z
问题
导入模板目前没有统一实现,两个仓库各做了一套,且都把用户填了也写不进去的列放进了模板。
objectui 的「下载模板」按钮前端自行拼 CSV,列取自 importTargetFields 的全量结果;framework 这边 GET /data/:object/export 的默认列是 buildFieldMetaMap(schema) 的全部 key,只按 FLS 收窄,不看 system / hidden / readonly。注释里 rest-server.ts 明确写了「an empty export doubles as an import template」——但空导出只保证「没有数据行」,没回答「表头该有哪些列」。
实测一个业务对象(项目),模板前 7 列是 registry 注入的系统字段:
| 列 |
字段 |
readonly |
hidden |
system |
| 组织标识 |
organization_id |
✅ |
✅ |
✅ |
| 创建时间 |
created_at |
✅ |
— |
✅ |
| 创建人 |
created_by |
✅ |
— |
✅ |
| 更新时间 |
updated_at |
✅ |
— |
✅ |
| 更新人 |
updated_by |
✅ |
— |
✅ |
| 所有者 |
owner_id |
❌ |
— |
✅ |
| 所属业务单元 |
owning_business_unit_id |
✅ |
✅ |
✅ |
因为是 registry 注入,每个对象都这样,不是个别对象配错。
比「列太多」更严重的是:这些 readonly 列会被 stripReadonlyFields 在写入时静默剥掉(engine.ts)。用户照模板填了「创建人」,导入成功、无报错、数据没进去。模板在教用户填注定被丢弃的列。
为什么不能直接改导出的默认列
导出和模板的诉求相反:
|
所有者 / 修改时间 |
| 导出(我要看数据) |
要 —— 列表上显示了,导出就该有 |
| 模板(我要填数据) |
不要 —— 填了会被平台盖掉或 strip |
同一套列规则无法同时满足。所以导出的列规则保持不变,模板作为新模式引入 —— 这样对现网调用方零影响。
方案:一个接口,两个模式
GET /data/:object/export → 现状,不改
GET /data/:object/export?template=true → 新增
目标列规则
|
导出模式(不改) |
模板模式(新增) |
| 起点 |
schema 全字段 |
schema 全字段 |
system |
保留 |
排除 |
hidden |
保留 |
保留(可写即进模板,裁定 5904855243 Q2 C) |
readonly |
保留 |
排除 |
formula / summary |
保留 |
排除 |
autonumber |
保留 |
排除 |
| FLS |
∩ 可读 |
∩ 可写 |
?fields= 显式指定 |
照办,不收窄 |
照办,不收窄 |
| 数据行 |
有 |
0 行 |
| 列顺序 |
作者声明顺序 |
作者声明顺序(不重排,必填靠 * 标记) |
判据一句话:「这列我填了,导入后真的会落库吗?」答"会"才进模板。 每条排除都对应引擎里一个真实的丢弃或拒绝行为,不是审美判断。
输出格式:xlsx(不是 CSV)
模板必须表达值域,CSV 做不到:
|
CSV |
xlsx |
| 封闭值域下拉 |
做不到 |
Excel 数据验证,点选不会错 |
| multiselect 用逗号分隔 |
与 CSV 分隔符打架 |
无冲突 |
| 值域说明 |
只能污染表头 |
独立 sheet |
前导零(00123) |
被 Excel 吃成 123 |
可声明文本单元格 |
具体结构:
- Sheet1「模板」:表头(必填带
*)+ 一行示例值;select / radio / boolean 列挂数据验证下拉
- Sheet2「填写说明」:每字段一行 —— 合法值、格式、分隔符;
lookup 写「填 <目标对象> 的名称」。同时兼作下拉数据源(Excel 内联下拉有 255 字符上限,选项多时必须引用区域)
值域怎么写
导入端(import-coerce.ts)实际接受的范围比模板告诉用户的宽得多,模板应如实传达:
| 类型 |
导入端接受 |
模板该给 |
| select / radio |
① value 精确 ② label 大小写不敏感 ③ 翻译后 label |
label(用户看得懂,导入端认) |
| multiselect |
按 , ; 、 换行 拆分 |
示例用 、 连两个真实选项;说明列出四种分隔符 |
| boolean |
true/t/yes/y/1/on/是/对/✓/√ + 反面 |
示例给「是」,不是 true |
| lookup / reference |
显示名 → resolveRef 解析 |
说明写「填名称」,提示重名会 reference_ambiguous |
| number |
容忍 1,234 / $¥€£¥ / 25% / (100) |
说明里写明 |
实现要点
已定的边界
- upsert 匹配列:模板不预置
autonumber。要 upsert 的人自己加列,映射步骤里照样选得到 —— 功能不丢。理由:为少数场景在模板里塞一列「填了新建时无效」的东西,又回到"教人填没用的列"的老问题。
- 示例值行:保留。它传达格式,Sheet2 说明里提示用户删除。
验收
- 任一业务对象
?template=true,下载的 xlsx 中不含任何 system / readonly / formula / summary / autonumber 列;作者声明 hidden: true 且可写入的字段保留(裁定 5904855243 Q2 C)
- 保留列的顺序与作者
fields 声明顺序一致;必填且没有默认值的列,表头带 *(裁定 5896083518 Q3 A)
select / boolean 列在 Excel 中可下拉选择
- 不带
?template=true 的导出,列与本 issue 之前逐字节一致(现网零行为变更)
- 下载的模板填入数据后原样导回,不产生
invalid_option / invalid_boolean
- 跨表格软件实测:Excel / WPS / Numbers / Google Sheets 下拉均正常 —— 单测证明不了,需真下载真打开
关联
objectui 侧改动(「下载模板」改调本接口、删除前端 CSV 生成)见 objectstack-ai/objectui 的对应 issue。本 issue 落地后 objectui 那侧才能动。
Ruled: 5904855243 · letter Q1 A · Q2 C —
ISecurityService.getWritableFieldsis added and the template narrows by it (round 2 on PR #20683,Part of #18386: acceptance 6 is a hand check in four spreadsheet apps, so the card stays open for it after the PR lands; ruling5906743185); the column rule keepssystemand dropshidden; acceptance 1 is corrected accordingly · 2026-09-30T05:41Z问题
导入模板目前没有统一实现,两个仓库各做了一套,且都把用户填了也写不进去的列放进了模板。
objectui 的「下载模板」按钮前端自行拼 CSV,列取自
importTargetFields的全量结果;framework 这边GET /data/:object/export的默认列是buildFieldMetaMap(schema)的全部 key,只按 FLS 收窄,不看system/hidden/readonly。注释里rest-server.ts明确写了「an empty export doubles as an import template」——但空导出只保证「没有数据行」,没回答「表头该有哪些列」。实测一个业务对象(项目),模板前 7 列是 registry 注入的系统字段:
organization_idcreated_atcreated_byupdated_atupdated_byowner_idowning_business_unit_id因为是 registry 注入,每个对象都这样,不是个别对象配错。
比「列太多」更严重的是:这些
readonly列会被stripReadonlyFields在写入时静默剥掉(engine.ts)。用户照模板填了「创建人」,导入成功、无报错、数据没进去。模板在教用户填注定被丢弃的列。为什么不能直接改导出的默认列
导出和模板的诉求相反:
同一套列规则无法同时满足。所以导出的列规则保持不变,模板作为新模式引入 —— 这样对现网调用方零影响。
方案:一个接口,两个模式
目标列规则
systemhidden5904855243Q2 C)readonlyformula/summaryautonumber?fields=显式指定*标记)判据一句话:「这列我填了,导入后真的会落库吗?」答"会"才进模板。 每条排除都对应引擎里一个真实的丢弃或拒绝行为,不是审美判断。
输出格式:xlsx(不是 CSV)
模板必须表达值域,CSV 做不到:
00123)123具体结构:
*)+ 一行示例值;select/radio/boolean列挂数据验证下拉lookup写「填 <目标对象> 的名称」。同时兼作下拉数据源(Excel 内联下拉有 255 字符上限,选项多时必须引用区域)值域怎么写
导入端(
import-coerce.ts)实际接受的范围比模板告诉用户的宽得多,模板应如实传达:value精确 ②label大小写不敏感 ③ 翻译后 label,;、换行 拆分、连两个真实选项;说明列出四种分隔符true/t/yes/y/1/on/是/对/✓/√+ 反面trueresolveRef解析reference_ambiguous1,234/$¥€£¥/25%/(100)实现要点
createXlsxStream。0 行数据不需要流式,而 streaming writer 对dataValidation的支持未经验证(exceljs 类型里WorksheetModel.dataValidations是注释掉的)。用普通new ExcelJS.Workbook()+workbook.xlsx.write(res)。exceljs@^4.4.0已是packages/rest的既有依赖,loadExcelJs()懒加载封装已存在。?template=true需要进DATA_EXPORT_PARAMS允许集,否则被refuseUnknownQueryParams挡掉。权限沿用现有两道闸(已被 The import template (enforceApiAccess('export')+enforceExportPermission)。GET /data/:object/export?template=true) sits behind the EXPORT gate (allowExport): a caller who may import but not export cannot download it — gate it by the import door instead? (from #18386 acceptance-6 verification) #20896 的裁定 A(5921162178)取代:模板分支template=true改由导入闸判定(enforceApiAccess(..., 'import')加该对象的新建权限),导出的两道闸不再挡在它前面;非模板导出不变。已定的边界
autonumber。要 upsert 的人自己加列,映射步骤里照样选得到 —— 功能不丢。理由:为少数场景在模板里塞一列「填了新建时无效」的东西,又回到"教人填没用的列"的老问题。验收
?template=true,下载的 xlsx 中不含任何system/readonly/formula/summary/autonumber列;作者声明hidden: true且可写入的字段保留(裁定5904855243Q2 C)fields声明顺序一致;必填且没有默认值的列,表头带*(裁定5896083518Q3 A)select/boolean列在 Excel 中可下拉选择?template=true的导出,列与本 issue 之前逐字节一致(现网零行为变更)invalid_option/invalid_boolean关联
objectui 侧改动(「下载模板」改调本接口、删除前端 CSV 生成)见 objectstack-ai/objectui 的对应 issue。本 issue 落地后 objectui 那侧才能动。