# Excel 日期 → JS Date → DB 在非 UTC 时区会偏 1 天

**日期**：2026-05-19
**触发场景**：从 `Master_Metadata_v5.xlsx` seed 172 台机器人到 dev DB（slot-3，CST/UTC+8）。Excel "Delivery Date" 列存的字符串 `"2026-03-08 00:00:00"`，DB 落地后变 `2026-03-07`。

## 表面症状
人工抽查 spotcheck.mjs 输出：
```
Excel: 2026-03-08 00:00:00 → DB: 2026-03-07
Excel: 2026-02-27 00:00:00 → DB: 2026-02-26
```
所有 delivery date 整体倒退 1 天。但 verify 脚本 28 个检查 PASS 0 FAIL —— 验证器和 seed 走的是同一个 `toIsoDate` 函数，bug 在两侧抵消了。

## 直接原因
```js
// 原写法（错）：
function toIsoDate(v) {
  const d = new Date(String(v).trim());
  return d.toISOString().slice(0, 10);
}
```
`new Date("2026-03-08 00:00:00")`（无 `T` 分隔、无 `Z` / 时区后缀）按 **JS Date 规范当本地时间解析**，在 UTC+8 等于 `2026-03-07T16:00:00 UTC`。`.toISOString().slice(0,10)` 取 UTC 部分 → `"2026-03-07"`。

## 元根因
**两条数据通道用了不一致的时区语义**：
- **Excel cell** 是「用户眼里的日期」，没有时区概念
- **JS Date / ISO string** 必须有时区，bare YYYY-MM-DD 字符串被规范当 UTC，bare YYYY-MM-DD HH:mm:ss（带空格）被规范当本地

跨这条边界时，必须显式声明："Excel 的 3/8 是用户讲的 3/8，跟 UTC / 本地无关，存什么时区都得保留 3/8 这个数字"。

## 修复（可复用）

```js
// 新写法：取本地 Y/M/D，绕开 ISO/UTC 反复转换
function ymd(d) {
  if (isNaN(d.getTime())) return null;
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
function toIsoDate(v) {
  if (v == null || v === '') return null;
  if (v instanceof Date) return ymd(v);
  if (typeof v === 'number') return ymd(new Date(Math.round((v - 25569) * 86400 * 1000)));  // Excel serial
  const s = String(v).trim();
  if (!s) return null;
  // 字符串里已经有 YYYY-MM-DD 就直接抽，不走 Date 解析
  const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
  if (m) return `${m[1]}-${m[2]}-${m[3]}`;
  return ymd(new Date(s));
}
```

Seed 侧用 `new Date(s)` 把字符串 `"2026-03-08"` 转回 Date 时，无 `T` 无 `Z` 的 `YYYY-MM-DD` 被规范当 **UTC midnight**（跟 `"YYYY-MM-DD HH:mm:ss"` 不一样）。所以存到 PG Timestamptz 是 `2026-03-08T00:00:00.000Z`，读出来 `.toISOString().slice(0,10)` 还是 `"2026-03-08"` ✓。

## 验证方法
不能信单一脚本（验证器和 seed 共用一个工具函数时，bug 会两侧抵消）。最少要：
1. 直接看 DB 原始值（`docker exec ... psql -c "SELECT delivered_at::date ..."`）
2. 人工 spotcheck 几条 Excel 行 vs DB 行（不通过工具函数）

## 适用范围
任何 Excel/CSV → JS → DB 的 seed / import 路径，只要源数据是 "用户视角的日期" 而开发/部署机不是 UTC，都有这个坑。FFOA 项目里 `prisma/seeds/` 下其他从 Excel 派生的 seed（如未来 SO importer、客户主档 importer）都应复用上面的 `ymd()` + `toIsoDate()` 模式。

## 相关文件
- `backend/prisma/seeds/fixtures/build-robot-master-fixture.mjs` (fixture 派生)
- `backend/prisma/seeds/fixtures/verify-robot-master.mjs` (验证脚本 —— 自己也曾踩过)
- `backend/prisma/seeds/robot-manager-master-seed.ts` (seed 落库)
