编排入口

order-parse-service.ts 负责:加锁 → 建批次 → Gmail → 解析 → 事务写入 → 写日志

一、Parse 库是什么?

Parse 库不是单独的 npm 包,而是项目里负责「派送表自动解析」的一组模块,核心职责:

职责 说明
搜邮件 按柜号在 Gmail 中检索
下附件 下载 Excel/CSV 二进制
解析表 动态识别表头、映射字段、标注 warning
入库 delivery_itemswarehouse_summariesattachments
留痕 每步写入 parse_logs,失败可追查
控并发 同一订单/柜号禁止重复 parsing

入口有两条链路(编排层不同,底层能力复用):

1
2
3
4
5
链路 A(订单检索)   orders 页点击「检索」
→ order-parse-service.ts :: parseOrderFromGmail()

链路 B(柜号解析) google-sheet 页 / 弹框 / 本地上传
→ container-parse-service.ts :: parseContainerEmail() 等

二、模块地图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
                   ┌─────────────────────────────────────┐
│ API 入口(Route) │
│ orders/[id]/search │
│ orders/batch-search │
│ containers/[id]/reparse │
│ containers/by-no/.../parse-* │
└──────────────┬──────────────────────┘

┌────────────────────────┼────────────────────────┐
▼ ▼ ▼
order-parse-service container-parse-service delivery-excel-parser
(订单编排) (柜号编排) (Excel/CSV 解析)
│ │ │
└────────────┬───────────┴────────────┬───────────┘
▼ ▼
gmail.ts batch-no.ts
(搜邮件/下附件) (批次号 = containers.id)

┌────────────┼────────────┐
▼ ▼ ▼
parse-log parse-db-error parse-lock
(写日志) (失败处理) (幂等锁)

文件清单

文件 角色 一句话
order-parse-service.ts 编排 A 从订单出发,创建 containers 记录,走 Gmail 全流程
container-parse-service.ts 编排 B 从柜号出发,更新 container_parse_meta,支持 Gmail/手动/上传
delivery-excel-parser.ts 解析引擎 表头检测、字段映射、warning、仓库汇总计算
gmail.ts 外部 IO Gmail OAuth、搜邮件、选最优邮件、下载附件
batch-no.ts 批次号 batch_no = String(containers.id)
parse-log.ts 日志层 safeWriteParseLog 事务外写 parse_logs
parse-db-error.ts 错误层 事务回滚后的独立日志 + 状态更新 + 分类 + 告警
parse-lock.ts 并发层 禁止同一订单/柜号并发 parsing
parse-result-columns.ts 展示层 解析结果页列定义(与解析流程无直接耦合)
delivery-items-export.ts 导出层 按原模板格式导出 CSV/XLSX

三、一次完整解析的生命周期

订单检索(链路 A)为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
sequenceDiagram
participant U as 用户
participant API as orders/search
participant Lock as parse-lock
participant Orch as order-parse-service
participant Gmail as gmail.ts
participant Parser as delivery-excel-parser
participant TX as Prisma 事务
participant Log as parse-log
participant Err as parse-db-error

U->>API: POST 检索
API->>Orch: parseOrderFromGmail(orderId)
Orch->>Lock: assertOrderNotParsing(orderId)
Orch->>Orch: containers.create(parse_status=parsing)
Orch->>Gmail: searchEmailsByContainer(柜号)
alt 找不到邮件
Orch->>Log: safeWriteParseLog(failed)
Orch->>Orch: containers.update(failed)
else 找到邮件
Orch->>Gmail: pickBestEmailForParse + getEmailDetail
Orch->>Gmail: downloadAttachmentBuffer
Orch->>Parser: parseDeliveryFileBuffer
Orch->>TX: 仅写 attachments / delivery_items / warehouse_summaries
alt 事务成功
Orch->>Log: safeWriteParseLog(success)
else 事务失败
TX-->>Orch: rollback
Orch->>Err: handleParseDbWriteFailure
Err->>Log: safeWriteParseLog(failed, 含 ctx+stack)
Err->>Orch: containers.update(failed)
end
end
Orch-->>API: OrderParseResult

关键设计原则

  1. 重 IO 在事务外:Gmail 搜索、附件下载、Excel 解析都不进 $transaction
  2. 日志在事务外parse_logssafeWriteParseLog,业务回滚不影响失败日志
  3. 失败必留痕handleParseDbWriteFailure 默认把 containers 标为 failed

四、核心模块详解

4.1 delivery-excel-parser.ts — 解析引擎

输入:附件 Buffer + 可选文件名
输出DeliveryParseResult

1
2
3
4
5
6
type DeliveryParseResult = {
headerRow: number; // 检测到的表头行号(1-based)
items: DeliveryItemParsed[]; // 明细行
summaries: WarehouseSummaryComputed[]; // 内存汇总
warnings: string[]; // 全局警告
};

表头检测detectHeaderRow):

  • 扫描前 30 行
  • 每行统计命中 HEADER_KEYWORDS 的列数
  • ≥ 2 个命中 → 认定为表头行

字段映射FIELD_ALIASES):

Excel 表头变体 数据库字段
柜号 container_no
SO/客户代码/唛头 customer_code
FBA ID fba_id
Reference ID / PO reference_id
仓库代码 warehouse_code
箱数 carton_count

Warning 规则

  • 柜号为空 → 仍入库,is_warning=truewarning="柜号为空"
  • 仓库代码为空 → 仍入库,追加 warning
  • 合计行(含「合计」「总计」「total」)→ 自动跳过

对外 API

1
2
3
parseDeliveryExcelBuffer(buffer)       // .xlsx / .xls
parseDeliveryFileBuffer(buffer, name) // 自动识别 CSV
deliveryItemToCreateInput(item, meta) // 转为 Prisma create 数据

4.2 gmail.ts — 邮件与附件

函数 作用
searchEmailsByContainer from:发件人 柜号 → fallback 仅 柜号
pickBestEmailForParse 多封邮件时选最优(有附件优先、snippet 更长、附件更大)
getEmailDetail 读正文 + 附件列表
downloadAttachmentBuffer Gmail API 下载 → Buffer

4.3 batch-no.ts — 批次号

1
containerBatchNo(containerId) => String(containerId)

同一轮解析中,containersattachmentsdelivery_itemsparse_logs 共用同一 batch_no,便于在「解析日志」页按批次追溯。


4.4 parse-log.ts — 日志层

两种写入方式:

函数 使用场景 是否在事务内
writeParseLog(tx, ...) 短事务内与状态更新绑定(遗留,逐步减少)
safeWriteParseLog(meta) 推荐:所有解析步骤日志 ❌ 独立连接

标准 step 值

step 含义
search_email Gmail 搜索
check_attachment 检查是否有可解析附件
download_attachment 下载附件
parse_excel Excel/CSV 解析
save_database 业务数据入库
upload_attachment 本地上传
parse_pipeline 管线级失败(container 链路)

status 枚举log_status):success | failed | warning

查看入口:页面 /parse-logs,API GET /api/v1/parse-logs


4.5 parse-db-error.ts — 错误层

当 Prisma $transaction 抛出异常、业务数据已回滚时,由本模块接手:

1
2
3
4
5
6
7
8
handleParseDbWriteFailure(ctx, err)

├─ 1. classifyDbError(err) → retryable | business | fatal
├─ 2. formatParseDbError → 用户提示 + 原始错误 + [分类] + ctx + stack
├─ 3. safeWriteParseLog(step=save_database, status=failed)
├─ 4. containers.update(parse_status=failed) ← 默认开启
├─ 5. attachments.update(failed) ← 可选
└─ 6. alertParseFailure → console + webhook

错误分类与用户提示

分类 典型 Prisma 码 用户看到
retryable P2034 死锁、P1001 连接失败、timeout 数据库暂时不可用,请稍后重试
business P2002 唯一约束 数据冲突,请勿重复操作
fatal 其他 数据库写入失败,事务已回滚

上下文 ctx 字段

1
2
3
4
{
container_no, container_id, batch_no,
attachment_id, email_message_id, attachment_name, user_id
}

4.6 parse-lock.ts — 并发控制

问题:用户连点「检索」,会创建多条 parsing 记录,重复写历史数据。

方案

1
2
assertOrderNotParsing(orderId)      // 订单链路入口
assertContainerNotParsing(containerNo) // 柜号链路入口

逻辑:

  1. 先释放超时(默认 10 分钟)的 stale parsing → 自动标 failed
  2. 若仍有活跃 parsingthrow Error("正在解析中,请稍后再试")

环境变量:PARSE_LOCK_STALE_MS=600000


4.7 两条编排链路对比

链路 A:order-parse-service 链路 B:container-parse-service
触发 /orders 检索 / 批量检索 /cargo-sheet 解析 / 上传 / 选手动附件
前置 orders 表有记录 cargo_sheet 有柜号
containers 每次检索 新建 一条 复用已有或从 sheet 同步
批次号 containers.id containers.id
附件存储 写入 attachments.file_content 不存附件(仅解析)
状态表 直接写 containers.parse_status 同时写 container_parse_meta
CSV 支持 parseDeliveryFileBuffer 仅 Excel(parseDeliveryExcelBuffer

五、事务边界(最重要)

✅ 正确模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 事务外:IO + 解析
const buffer = await downloadAttachmentBuffer(...);
const parsed = await parseDeliveryFileBuffer(buffer, filename);

// 2. 事务内:仅业务表
await prisma.$transaction(async (tx) => {
await tx.attachments.create({ file_content: buffer, ... });
await tx.delivery_items.updateMany({ is_history: true });
await tx.delivery_items.createMany({ ... });
await rebuildWarehouseSummaries(tx, ...);
});

// 3. 事务外:写日志
await safeWriteParseLog({ step: "save_database", status: "success", ... });

❌ 错误模式(已修复)

1
2
3
4
await prisma.$transaction(async (tx) => {
await tx.delivery_items.createMany({ ... });
await tx.parse_logs.create({ ... }); // ← 业务失败时日志一起回滚,无法排查
});

六、历史版本机制(is_history)

重复解析同一柜号时 不物理删除 旧数据:

1
2
3
4
5
6
7
8
// 旧数据标记历史
await tx.delivery_items.updateMany({
where: { container_no, is_history: false },
data: { is_history: true },
});

// 插入新数据 is_history = false
await tx.delivery_items.createMany({ data: [...] });

列表、汇总、导出均过滤 is_history = false


七、如何调试 Parse 问题

7.1 用户报告「检索失败」

  1. 打开 /containers 找对应柜号的 parse_statuserror_message
  2. 点击「解析日志」→ /parse-logs?containerNo=XXX
  3. step 在哪一步 failed
    • search_email → Gmail 搜不到 / 授权问题
    • parse_excel → 附件格式/表头问题
    • save_database → 数据库事务失败(看 message 里的 [retryable] 等分类)

7.2 用户报告「一直解析中」

  • 检查 containers.parse_status = parsing
  • 可能原因:进程 crash 未更新状态
  • 解决:等 10 分钟 stale 自动释放,或手动改 parse_status = failed

7.3 本地复现

1
2
3
4
5
# 1. 确保 Gmail 已授权(订单页会提示连接)
# 2. 确保数据库连通
# 3. 对单个订单 POST
curl -X POST http://localhost:3000/api/v1/orders/1/search \
-H "Cookie: app_access_token=..."

7.4 告警

配置 webhook 后,以下情况会 POST 到 PARSE_ALERT_WEBHOOK_URL

  • parse_log_write_failed — 连失败日志都写不进去
  • container_status_update_failed — 失败后状态更新失败
  • attachment_status_update_failed — 附件状态更新失败

八、扩展指南

新增一个解析步骤

  1. 在编排层(order-parse-servicecontainer-parse-service)添加逻辑
  2. 成功/失败均调用 safeWriteParseLog({ step: "your_step", ... })
  3. 若涉及 DB 写入,放在 $transaction 内,不要在事务内写 log

新增 Excel 列映射

编辑 delivery-excel-parser.ts

1
2
3
4
5
// FIELD_ALIASES 增加别名
"新列名": "target_field",

// HEADER_KEYWORDS 增加关键词(帮助表头检测)
"新关键词",

新增解析入口(如新 API)

1
2
3
4
5
6
7
8
import { parseOrderFromGmail } from "@/lib/order-parse-service";
// 或
import { parseContainerEmail } from "@/lib/container-parse-service";

// 记得:
// 1. 调用 assertOrderNotParsing / assertContainerNotParsing
// 2. 失败时用 handleParseDbWriteFailure
// 3. 步骤日志用 safeWriteParseLog

九、与环境变量的关系

变量 影响模块
DATABASE_URL 全部
GOOGLE_CLIENT_ID/SECRET gmail.ts
GMAIL_DEFAULT_SENDER gmail.ts 默认发件人
PARSE_LOCK_STALE_MS parse-lock.ts 锁超时
PARSE_ALERT_WEBHOOK_URL parse-db-error.ts 告警

十、一图总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
柜号 + Gmail Token


┌─────────────┐ ┌──────────────────┐
│ parse-lock │────▶│ 禁止并发 parsing │
└─────────────┘ └──────────────────┘


┌─────────────┐ ┌──────────────────┐
│ gmail.ts │────▶│ 邮件 + 附件 Buffer │
└─────────────┘ └──────────────────┘


┌─────────────────────┐
│ delivery-excel-parser│──▶ items + warnings + summaries
└─────────────────────┘


┌─────────────────────┐
│ $transaction │──▶ delivery_items, warehouse_summaries, attachments
│ (仅业务表) │
└─────────────────────┘

成功 │ 失败
│ └─▶ parse-db-error ──▶ safeWriteParseLog(failed) + update status + alert

safeWriteParseLog(success)


/containers 页面展示结果
/parse-logs 页面查看日志

相关文档:交付文档 · Gmail 检索与解析说明

系列导航

主题
上一篇 04 Excel 解析
本篇 解析 ETL 流水线
下一篇 06 可编辑大表
索引 专栏首页

← 返回专栏首页