【Next.js实战①】Gmail API 按柜号检索邮件:OAuth 双 Cookie 与搜索 Fallback

1. 业务场景:为什么要在 Web 里接 Gmail?

在物流/货代业务里,运营同事每天会收到大量带 柜号 的邮件,附件往往是 Excel 或 CSV 派送明细。传统做法是人工打开邮箱、搜索柜号、下载附件、再粘贴到内部系统------慢、易漏、难追溯。

Next.js + Gmail API 可以把流程改成:

  1. 用户在订单页点「检索」
  2. 后端按柜号搜索已授权邮箱
  3. 自动下载 xlsx/csv 附件
  4. 交给解析模块入库(FBA、仓库、箱数等)

本文聚焦 第 2~3 步:OAuth 授权、搜索策略、附件下载。解析与入库在专栏下一篇展开。


2. 架构要点:两套 Cookie,千万别混

很多初学者把「用户登录系统」和「读取 Gmail」做成同一套 Token,结果要么权限过大,要么刷新逻辑互相污染。

Cookie 用途 签发方
app_access_token / app_refresh_token 登录本系统 /api/v1/auth/login
gmail_access_token / gmail_refresh_token 读 Gmail Gmail OAuth 回调

能登录 ≠ 能搜邮件。 检索前必须单独走一遍 Gmail OAuth。

统一入口:

typescript 复制代码
// gmail-tokens.ts
export async function resolveGmailTokens(request: Request) {
  const access = getCookie(request, "gmail_access_token");
  if (access) return { accessToken: access };

  const refresh = getCookie(request, "gmail_refresh_token");
  if (refresh) {
    const refreshed = await refreshAccessToken(refresh);
    return { ...refreshed, refreshed: true };
  }
  return null;
}

API 层拿到 null 时返回 401,并在 meta.needReconnect: true 里告诉前端:请跳转 /api/v1/gmail/auth 重新授权。


3. OAuth 流程(App Router)

3.1 环境变量

env 复制代码
GOOGLE_CLIENT_ID=xxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=xxx
GOOGLE_REDIRECT_URI=http://localhost:3000/api/v1/gmail/callback
GMAIL_DEFAULT_SENDER=ops@example.com

Google Cloud Console 里:

  • 启用 Gmail API
  • OAuth 同意屏幕配置测试用户
  • 重定向 URI 与 GOOGLE_REDIRECT_URI 完全一致
  • Scope 建议:https://www.googleapis.com/auth/gmail.readonly(只读即可)

3.2 路由分工

步骤 路径 职责
发起授权 GET /api/v1/gmail/auth 302 到 Google 授权页
回调换 Token GET /api/v1/gmail/callback code → access + refresh
写 Cookie gmail-tokens.ts HttpOnly、Secure(生产)
前端提示 GmailAuthNotifier.tsx ?gmail_connected=true

回调核心逻辑:

typescript 复制代码
// callback/route.ts(示意)
const { tokens } = await oauth2Client.getToken(code);
const response = NextResponse.redirect(new URL("/orders", request.url));
setGmailTokenCookies(response, tokens);
return response;

3.3 Token 刷新

Access Token 约 1 小时过期。resolveGmailTokens 在只有 refresh 时自动刷新,并在响应里 setGmailTokenCookies 写回。前端无需感知,除非 refresh 也失效------此时引导用户重新 OAuth。


4. 按柜号搜索:Fallback 策略

函数签名:

typescript 复制代码
searchEmailsByContainer(
  containerNo: string,
  sender?: string,
  accessToken: string,
  refreshToken?: string
): Promise<EmailSearchHit[]>

两次查询 fallback:

复制代码
查询 1:from:{GMAIL_DEFAULT_SENDER} {柜号}
   ↓ 无结果
查询 2:{柜号}   (不限发件人)

为什么需要 fallback? 业务上常配置「默认发件人」,但实际邮件可能来自客户个人邮箱。只查 from: 会漏信;第二次放宽发件人,仅用柜号关键词命中。

Gmail 搜索语法示例:

复制代码
from:ops@example.com ABCD1234567
ABCD1234567

每条命中邮件拉 metadata(Subject / From / Date),并标记 hasExcelAttachment(含 xlsx csv)。


5. 多封命中:如何选「最佳邮件」

同一柜号可能有多封往来邮件。全自动流程需要 pickBestEmailForParse()

  1. 优先isParseable 附件的邮件(xlsx/xls/csv)
  2. 再比较 snippet 长度、附件数量、附件大小等打分
  3. 线 A(订单检索)全自动用得分最高的一封
  4. 线 B(弹框检索)可让用户手动点某一封看详情
typescript 复制代码
function scoreEmail(email: EmailSearchHit): number {
  let score = 0;
  if (email.hasParseableAttachment) score += 100;
  score += Math.min(email.snippet.length, 200) / 10;
  score += email.attachmentCount;
  return score;
}

6. 附件下载:Buffer 不进数据库事务

6.1 可解析附件判定

typescript 复制代码
export function isParseableAttachment(mime: string, filename: string) {
  return isExcelMimeType(mime) || isCsvFile(mime, filename);
}

function isExcelMimeType(mime: string) {
  return /spreadsheet|excel|xlsx|xls/i.test(mime);
}

function isCsvFile(mime: string, filename: string) {
  return mime.includes("csv") || filename.toLowerCase().endsWith(".csv");
}

6.2 下载接口

typescript 复制代码
async function downloadAttachmentBuffer(
  messageId: string,
  attachmentId: string,
  accessToken: string
): Promise<Buffer> {
  const res = await gmail.users.messages.attachments.get({
    userId: "me",
    messageId,
    id: attachmentId,
  });
  return Buffer.from(res.data.data!, "base64");
}

6.3 与 Prisma 事务的边界

反模式: 在 Prisma $transaction 里调 Gmail 下载 + Excel 解析。网络/解析一旦失败,PostgreSQL 可能进入 25P02(事务 aborted),后续 SQL 全部挂掉。

推荐模式:

复制代码
① 事务外:search → pickBest → download → parse
② 短事务:写 delivery_items / parse_logs / attachments

Vercel 上检索 API 建议 export const maxDuration = 120(长任务)。


7. 完整链路(订单页「检索」)

复制代码
orders/page.tsx 点「检索」
  → POST /api/v1/orders/[id]/search
  → parseOrderFromGmail()        // order-parse-service.ts
      → generateBatchNo()
      → insert containers (parsing)
      → searchEmailsByContainer()
      → pickBestEmailForParse()
      → getEmailDetail()
      → downloadAttachmentBuffer()  // 事务外
      → parseDeliveryFileBuffer()   // 下一篇详解
      → $transaction 写库
      → parse_logs save_database

batch_no 通常取 String(containers.id),同柜多次检索产生多批次,便于审计与回滚。


8. 常见踩坑清单

现象 原因 处理
401 needReconnect Gmail Token 过期 重新走 /gmail/auth
搜不到邮件 只查了默认发件人 确认 fallback 第二次查询
附件列表为空 邮件只有链接无附件 前端提示「无 xlsx/csv」
生产 OAuth 失败 redirect_uri 不一致 Console 与 env 逐字对比
事务里下载超时 IO 在 transaction 内 IO 移出事务

关于作者

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

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

9. 小结

Gmail 集成的关键是三件事:

  1. 登录与 Gmail 授权分离 (两套 Cookie + resolveGmailTokens
  2. 搜索要有 fallback(默认发件人 + 纯柜号)
  3. 下载解析不进长事务(先 IO,再短事务写库)

下一篇我们将讲 Excel/CSV 派送表动态解析:表头不在第 1 行、列名 alias 映射、仓库汇总。


相关推荐
weixin_307779131 小时前
Python写入Shell文件使用Linux系统的换行符
linux·开发语言·python·自动化
云水一下1 小时前
Vue.js从零到精通系列(五):全局状态管理——Pinia 核心与实践
前端·javascript·vue.js
zmzb01032 小时前
Python课后习题训练记录Day130
开发语言·python
阿里嘎多学长2 小时前
2026-06-13 GitHub 热点项目精选
开发语言·程序员·github·代码托管
xiaoshuaishuai82 小时前
C# 委托与事件
开发语言·c#
kmblack12 小时前
javascript计算年龄
开发语言·javascript·ecmascript
Dick5072 小时前
ROS2 多机器人通用 Driver 层复盘:BaseRobotDriver 到多平台 Mock 切换实现
前端·javascript·机器人
肖爱Kun3 小时前
STL标准模块库操作
开发语言·音视频
Song_da_da_3 小时前
C# 接口(Interface)深度解析:规范、解耦与灵活扩展
开发语言·c#