本篇定位

Gmail 自动处理系统 上线运维:Vercel + Neon、环境变量、Gmail OAuth 生产配置、派送明细导出。

A. 环境变量

| 变量 | 说明 |
|

C. 启动命令

1
2
3
4
npm install
npm run db:generate
npm run dev # http://localhost:3000
npm run build # 生产构建

D. 项目目录结构(核心)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
src/
├── app/
│ ├── api/v1/ # REST API(38 个 route.ts)
│ ├── containers/ # 解析结果页
│ ├── orders/ # 订单 + 检索
│ ├── google-sheet/ # Sheet 复刻
│ ├── warehouse-summaries/
│ └── parse-logs/
├── components/
│ ├── ParseResultDialog.tsx
│ └── layout/
├── lib/
│ ├── gmail.ts # Gmail 搜索 / 下载
│ ├── delivery-excel-parser.ts # Excel 解析
│ ├── order-parse-service.ts # 解析编排 + 入库
│ ├── parse-log.ts # 日志
│ ├── parse-db-error.ts # 事务失败
│ └── delivery-items-export.ts # 导出
└── middleware.ts # 路由守卫
prisma/
├── schema.prisma
└── seed.ts

1.3 API 设计

所有 API 前缀:/api/v1/,需登录(JWT Cookie),由 src/middleware.ts 拦截未认证请求。

认证

| 方法 | 路径 | 文件 | 说明 |
|

1.5 异常处理

找不到邮件
当调用没有找到对应的邮件时提示没有相关邮件

找到多封邮件
当存在多附件时,读取附件大小优先使用文件最大的附件

Excel 表头位置不固定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const HEADER_KEYWORDS = [
"fba", "reference", "warehouse", "仓库", "箱数", "carton", "柜号",
"重量", "weight", "cbm", "体积", "派送", "delivery", "客户", "唛头",
"so", "po", "备注", "note", "打板", "pallet", "实际", "actual",
];

function isSummaryRow(values: string[]) {
const joined = values.join(" ").toLowerCase();
return joined.includes("合计") || joined.includes("总计") || joined.includes("total");
}

function detectHeaderRow(rows: string[][]) {
for (let i = 0; i < Math.min(rows.length, 30); i++) {
const normalized = rows[i].map((v) => normalizeHeader(v));
const hits = normalized.filter((cell) =>
HEADER_KEYWORDS.some((keyword) => cell.includes(normalizeHeader(keyword))),
).length;
if (hits >= 2) return i;
}
return 0;
}

仓库代码为空
读取文件时当单元格为空时在数据库表delivery_items.warnings字段进行赋值

1
2
3
4
5
6
7
8
if (!row.warehouse_code) {
row.warnings.push("仓库代码为空");
warnings.push(`第 ${i + 1} 行:仓库代码为空`);
}
if (row.carton_count === null) {
row.warnings.push("箱数为空");
warnings.push(`第 ${i + 1} 行:箱数为空`);
}

同⼀个柜号重复解析
重复解析没哟删除之前的内容。是数据库内新增了一个is_history的字段,默认值是false,当前有相同的柜号进来时。将历史数据修改为true,同时引入批次号的概念。批次号规则:containers、attachments、delivery_items、parse_logs 四表共用同一 batch_no,值为 String(containers.id)。用于检索同一次解析的数据。

Gmail API 失败
失败后可手动进行重新解析:

Gmail API 失败后手动重新解析

数据库写入失败
失败后可手动进行重新解析:

1
2
3
4
5
6
7
export const PARSE_DB_WRITE_FAILURE_MESSAGE = "数据库写入失败,事务已回滚";

export async function handleParseDbWriteFailure(ctx, err, options?) {
// 1. 写入 parse_logs (step=save_database, status=failed)
// 2. 可选更新 containers / attachments 为 failed
return message;
}

支持批量柜号解析

订单页批量勾选柜号并检索

支持重复解析时保留历史版本

重复解析后历史版本保留示意

支持导出和原表格式类似的 Excel

派送明细导出 Excel 与模板格式一致

支持权限控制:管理员可重新解析,普通用户只能查看

对操作按钮进行了权限控制。只有 admin 账号有权限可以操作。

管理员

管理员可见重新解析等操作按钮

普通用户

普通用户仅可查看解析结果

支持日志追踪:每一步解析过程可查看

parse_logs 逐步骤解析日志

** 其他项的内容没有进行实现**

派送表导出

1
2
3
4
5
6
7
8
// GET /api/v1/containers/[id]/export
// lib/delivery-items-export.ts
export async function exportDeliveryItems(containerId: bigint, format: "csv" | "xlsx") {
const rows = await prisma.delivery_items.findMany({
where: { container_id: containerId, is_history: false },
});
return buildTemplateWorkbook(rows);
}

导出列与运营模板一致(含客人备注、打板数量、仓库备注等 13 列),详见 DELIVERY_EXPORT_COLUMNS

前端调用ParseResultDialog.tsx):

1
2
3
const exportRes = await fetch(`/api/v1/containers/${containerId}/export?format=xlsx`);
const blob = await exportRes.blob();
// 触发浏览器下载

部署架构

1
2
3
4
5
6
flowchart TD
DEV[本地 npm run build] --> VERCEL[Vercel 部署]
VERCEL --> NEON[(Neon PostgreSQL 连接池)]
VERCEL --> ENV[环境变量]
USER[运营人员] --> EXPORT[解析结果导出]
EXPORT --> FILE[CSV / XLSX 模板]

Vercel 配置

1
2
3
4
5
{
"buildCommand": "npm run build",
"installCommand": "npm install",
"framework": "nextjs"
}

package.jsonpostinstall: prisma generate 确保 Serverless 构建含 Prisma Client。

长任务 API(如 Gmail 检索)需设置 export const maxDuration = 120

数据库迁移

采用 Prisma schema + 手工 SQLprisma/sql/*.sql),上线前在 Neon 控制台或 psql 执行建表脚本,再 npx prisma db seed 初始化演示管理员(立即修改默认密码)。

上线检查清单

  • 生产 JWT_SECRET 已更换(openssl rand -hex 32
  • Gmail OAuth 重定向 URI 与 Google Console 一致
  • 默认 seed 账号已禁用或改密
  • PARSE_LOCK_STALE_MS 按需调整
  • 可选:PARSE_ALERT_WEBHOOK_URL 配置解析失败告警
  • Nginx 仅响应正式域名(拒绝 IP 直访)
  • robots.txt / sitemap 已包含专栏 URL

种子账号(脱敏)

角色 邮箱 密码
管理员 ops@demo.local DemoPass123!
只读 viewer@demo.local Viewer123456

生产环境勿使用上述账号。

异常处理与运维

异常 处理
Gmail Token 失效 引导 /api/v1/gmail/auth 重新授权
解析一直 parsing 等 stale 锁释放或手动改 parse_status
数据库写入失败 /parse-logs step=save_database
批量检索部分失败 订单页 toast + /containers 查看详情

系列导航

主题
上一篇 07 CRUD 模板
本篇 Vercel 部署与派送表导出
索引 专栏首页

← 返回专栏首页