Skip to content

【缺陷】数据导出(CSV/XLSX)日期时间列硬编码按 UTC 渲染,与界面时区不一致(@objectstack/rest export-format.ts formatDate) #8373

Description

@baozhoutao

版本与环境

  • @objectstack/* 17.0.0-rc.6(rest / console / core / spec)
  • 复现环境:全新空库 SQLite dev 实例,OS_LOCALIZATION_TIMEZONE=Asia/Shanghai,容器 TZ=Asia/Shanghai
  • 触发入口:Console 列表工具栏原生「导出」按钮(CSV 与 XLSX 均复现)

现象

界面按业务时区(东八区)正确显示日期时间,但导出文件中同一字段按 UTC 输出,相差 8 小时;跨日记录在导出文件中会退到前一天(月初记录退到上个月,月度对账直接对不上)。

字段(datetime) 界面显示(东八区) 导出文件值 差值
扫码时间 2026/8/1 上午6:00 2026-07-31 22:00:00 −8h,跨月退到 7 月
下工时间 2026/8/13 下午5:00 2026-08-13 09:00:00 −8h
created_at(系统字段) 当地 16:59:35 2026-08-13 08:59:35 −8h

CSV 与 XLSX 输出完全相同的 UTC 字符串(XLSX 中为文本单元格,非带时区的日期型)。已在 3 个不同对象上复现;formatCellValue() 按字段类型分派,与对象无关,波及全部含 date/datetime 列的导出

复现步骤

  1. 全新实例,OS_LOCALIZATION_TIMEZONE=Asia/Shanghai;
  2. 任一对象造一条 datetime 值为当地 2026-08-01 06:00(落库 2026-07-31T22:00:00.000Z)的记录;
  3. 列表页确认界面显示 2026/8/1 上午6:00;
  4. 点工具栏「导出」下载 CSV 或 XLSX;
  5. 文件中该列为 2026-07-31 22:00:00(期望:2026-08-01 06:00:00)。

根因(已定位到函数)

调用链:Console exportDownload()GET /api/v1/data/{object}/export?format=…&fields=…(请求无时区参数)→ @objectstack/rest src/rest-server.ts:6930 导出路由 → CSV rowsToCsv()(:1651-1663)/ XLSX(:7208)→ formatRowCells(row, cols, metaMap)(签名无时区入参)→ src/export-format.ts:174-181:

/** `YYYY-MM-DD` (date) or `YYYY-MM-DD HH:mm:ss` (datetime), in UTC. */
function formatDate(value: unknown, withTime: boolean): unknown {
  const d = toDate(value);
  if (!d) return value;
  const ymd = `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
  if (!withTime) return ymd;
  return `${ymd} ${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}:${pad2(d.getUTCSeconds())}`;
}

getUTC* 写死,由同文件 formatCellValue()(:251-252)对 type: 'date' / type: 'datetime' 无条件调用。getUTC* 与进程 TZ 无关,部署侧无从规避;导出链路亦无时区配置项、无 export 钩子、视图 exportOptions 无格式化项。

三条佐证,倾向这是疏漏而非有意设计:

  1. 时区在这条路径上拿得到、只是没接:导出路由第一步 resolveExecCtx(src/rest-server.ts:6935)取到的 ExecutionContext 已带 timezone(resolveLocalizationContext() 级联 平台默认→全局→租户),格式化层未使用;
  2. 同平台别处认时区:自动编号日期令牌按 ADR-0053「business timezone」渲染,fallback 才是 UTC(@objectstack/spec RenderAutonumberInput.timezone);导出格式化与该语义不一致;
  3. 导出自身两套时钟:同文件 exportContentDisposition()(src/export-format.ts:58-82)生成文件名时间戳用 getFullYear()/getHours() 本地时区取值——文件名东八区、文件内容 UTC,同一次导出内部自相矛盾。

建议修法

ExecutionContext.timezone 透传进 formatRowCells / formatCellValue,formatDateIntl.DateTimeFormat(…, { timeZone }) 按业务时区取日历分量;timezone 缺省时维持现状(UTC),保证向后兼容。

来源

下游实施项目验收中发现并完成定位:steedos-labs/os-project-titanwind-ehr#1269(含完整取证:3 对象实测截图、导出原件、逐介入点排查记录)。修复前下游导出文件无法用于月度对账,盼排期。

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions