【Next.js实战②】Excel 派送表动态解析:表头识别与 FIELD_ALIASES 映射

1. 痛点:列名不统一,硬编码列号必崩

货代/物流场景里,Gmail 附件中的派送表 没有统一模板

  • 表头可能在第 3 行,不在第 1 行
  • 同一含义多种写法:FBA ID / fba / Reference PO
  • 既有 .xlsx 也有 .csv,分隔符可能是 Tab

如果写 row[2] 取 FBA、row[5] 取箱数------换一家客户模板就全线崩溃。

正确做法是:动态识别表头行 + 别名映射到标准字段 + 行级 warning


2. 解析在链路中的位置

复制代码
Gmail 下载 Buffer
  → parseDeliveryFileBuffer(buffer, filename, containerNo)
  → parseDeliveryMatrix(rows)
  → DeliveryParseResult { items, summaries, warnings }
  → Prisma 写 delivery_items + warehouse_summaries

统一入口(推荐):

typescript 复制代码
parseDeliveryFileBuffer(
  buffer: Buffer,
  filename?: string,
  defaultContainerNo?: string
): DeliveryParseResult

分支逻辑:

复制代码
filename 以 .csv 结尾 → parseCsvBufferToMatrix → parseDeliveryMatrix
否则                  → ExcelJS 读 xlsx      → parseDeliveryMatrix

注意:项目里还有「订单大表导入」走 order-sheet-import.ts,入库 cargo_sheet,与本文 派送明细表 不是同一条链路,不要混用。


3. 五步流水线

复制代码
① 读成二维矩阵 string[][]
② detectHeaderRow     --- 前 30 行找表头
③ buildColumnMap      --- 表头 → 标准字段名
④ parseDeliveryMatrix --- 逐行解析 + warning
⑤ aggregateWarehouseSummaries --- 按仓库汇总箱数

返回值结构:

typescript 复制代码
interface DeliveryParseResult {
  headerRow: number;           // 1-based,给人看
  items: DeliveryItemParsed[];
  summaries: WarehouseSummaryComputed[];
  warnings: string[];          // 如「第 5 行:箱数为空」
}

4. 读矩阵:xlsx 与 csv

4.1 xlsx(ExcelJS)

typescript 复制代码
const workbook = await loadWorkbook(buffer);
const sheet = workbook.worksheets[0];  // 只读第一个工作表
const rows = getWorksheetRows(sheet, 30); // 每行最多 30 列,去尾空

4.2 csv

typescript 复制代码
// 优先 UTF-8,乱码 fallback latin1
// 去 BOM \uFEFF
// 分隔符:Tab 数量 > 逗号 → 用 Tab,否则逗号
// 支持引号包裹、" "" 转义

同一套 parseDeliveryMatrix 处理两种来源,避免维护两套业务规则。


5. 表头识别 detectHeaderRow

在前 30 行 扫描,某行 normalized 后与关键词表命中 ≥2 个 即视为表头:

typescript 复制代码
const HEADER_KEYWORDS = [
  "fba", "reference", "warehouse", "仓库", "箱数", "carton", "柜号",
  "重量", "weight", "cbm", "体积", "派送", "delivery", "备注", "note",
];

function normalizeHeader(cell: string) {
  return cell.replace(/\s+/g, "").toLowerCase();
}

function detectHeaderRow(rows: string[][]): number {
  for (let i = 0; i < Math.min(rows.length, 30); i++) {
    const hits = rows[i]
      .map(normalizeHeader)
      .filter((cell) =>
        HEADER_KEYWORDS.some((kw) => cell.includes(normalizeHeader(kw)))
      ).length;
    if (hits >= 2) return i;
  }
  return 0; // 兜底:第 0 行
}

跳过汇总行:

typescript 复制代码
function isSummaryRow(values: string[]) {
  const joined = values.join(" ").toLowerCase();
  return /合计|总计|total/.test(joined);
}

6. 列映射 FIELD_ALIASES

表头 normalized 后查别名表,映射到标准 key:

表头别名(部分) 标准字段 入库列
柜号 / containerno container_no container_no
FBA / FBA ID / fbaid fba_id fba_id
Reference / PO reference_id reference_id
箱数 / cartons carton_count carton_count
仓库 / 仓库代码 warehouse_code warehouse_code
CBM / 体积 cbm cbm
Weight / 重量 weight weight
客人备注 / 客户备注 customer_note customer_note
打板数量 pallet_count pallet_count
typescript 复制代码
const FIELD_ALIASES: Record<string, keyof DeliveryItemParsed> = {
  fbaid: "fba_id",
  fba: "fba_id",
  "fbaid": "fba_id",
  warehouse: "warehouse_code",
  仓库: "warehouse_code",
  cartons: "carton_count",
  箱数: "carton_count",
  // ... 完整列表见 delivery-excel-parser.ts
};

function buildColumnMap(headerRow: string[]) {
  const map: Record<number, keyof DeliveryItemParsed> = {};
  headerRow.forEach((cell, colIndex) => {
    const key = FIELD_ALIASES[normalizeHeader(cell)];
    if (key) map[colIndex] = key;
  });
  return map;
}

未出现在映射表的列直接忽略------比强行解析未知列更安全。


7. 数据行规则

表头下一行 到矩阵末尾:

规则 行为
整行空 跳过
汇总行 跳过
无柜号 defaultContainerNo;仍无则跳过
FBA、Reference、仓库、箱数全空 跳过(无效行)
仓库为空 仍入库 + warning「仓库代码为空」
箱数为空 仍入库 + warning「箱数为空」

数值列走 cellToNumber(去千分位逗号);柜号列 toUpperCase()

typescript 复制代码
if (!row.warehouse_code) {
  row.warnings.push("仓库代码为空");
  warnings.push(`第 ${i + 1} 行:仓库代码为空`);
}

设计哲学: 尽量入库可挽救的数据,用 warnings 标记可疑行,而不是整表 reject。


8. 仓库汇总 aggregateWarehouseSummaries

warehouse_code(大写)分组:

typescript 复制代码
{
  warehouse_code: "ONT8",
  total_cartons: 120,      // sum(carton_count),null 当 0
  item_count: 15,
}

写入 warehouse_summaries,与 delivery_items 共用同一 batch_no,便于按「一次检索」对账。


9. 与 Gmail 链路的衔接

调用方 文件 说明
订单「检索」 order-parse-service.ts 线 A,支持 csv + xlsx
按柜号解析 container-parse-service.ts 线 B,建议统一走 File 版入口
typescript 复制代码
// order-parse-service.ts(示意)
const buffer = await downloadAttachmentBuffer(messageId, attachmentId, token);
const parsed = parseDeliveryFileBuffer(buffer, filename, containerNo);
await persistDeliveryItems(parsed.items, batchNo);
await persistWarehouseSummaries(parsed.summaries, batchNo);
await writeParseLogs(parsed.warnings);

10. 测试建议

  1. 表头在第 1 / 第 5 行 各准备一份样例
  2. 列名 alias 覆盖:FBA ID vs fba
  3. csv Tab 分隔 vs 逗号分隔
  4. 含合计行 的表,确认不被当数据行
  5. 缺仓库/缺箱数 行,确认有 warning 且仍入库

单元测试可对 detectHeaderRowbuildColumnMap 纯函数直接断言,无需 mock Gmail。


11. 小结

派送表解析的三板斧:

  1. 动态表头(关键词命中 ≥2,前 30 行扫描)
  2. FIELD_ALIASES(列名归一化,忽略未知列)
  3. warning 不阻断(可疑行标记,汇总仍可用)

配合上一篇 Gmail 检索,你就有了「搜邮件 → 下附件 → 解析入库」的核心闭环。后续可继续看 ETL 事务边界、RBAC、可编辑大表等篇章。


关于作者

devcfg 以已知,溯本源,探未知 · Next.js · Excel 解析 · 工程实战

专注全栈业务自动化,同系列文章收录于本人 CSDN 专栏。

本文为「Next.js Gmail 自动处理系统」第 2/8 篇 。正文与代码可在 CSDN 独立阅读

本系列目录(专栏内按序阅读)

  1. Gmail API 按柜号检索邮件:OAuth 双 Cookie 与搜索 Fallback
  2. Excel 派送表动态解析(本篇)
  3. JWT 双 Token 认证与 RBAC 权限控制
  4. 文档解析 ETL 流水线与 Prisma 事务边界
  5. Next.js 可编辑大表与历史快照
  6. CRUD 列表模板(Zod + Mapper)
  7. 系统架构与 Prisma 数据模型
  8. Vercel 部署与派送表导出

欢迎在评论区交流 ExcelJS、CSV 分隔符或表头识别算法。觉得有帮助请 点赞 / 收藏 / 关注

下一篇预告:《JWT 双 Token 认证与 RBAC 权限控制》(专栏续更)

相关推荐
2401_840759761 小时前
2026年前端框架生态与AI开发新趋势
前端·人工智能·科技
研☆香2 小时前
jQuery特殊属性操作方法
前端·javascript·jquery
ShiJiuD6668889992 小时前
外卖项目总结下 (前端板块)
前端
daols882 小时前
vue vxe-table 复制数据到 Excel:支持带表头复制
vue.js·excel·vxe-table
liming4952 小时前
Maven中央库迁移
服务器·前端·maven
努力的lpp2 小时前
渗透主流工具完整参数手册(sqlmap、Nmap、Hydra、Dirsearch、Xray)
javascript·网络协议·测试工具·安全·http·工具
problc2 小时前
用 JavaScript 打开中国的版式文档:@sharp9/ofdjs 诞生记
开发语言·javascript·ecmascript
超哥--9 小时前
B站视频内容智能分析系统(九):React 前端与管理面板
前端·react.js·前端框架
Cutecat_12 小时前
视频字幕处理工具横向:提取模式 vs 编辑模式,该如何选择
android·前端·ios·语音识别