
天天
对于经常报销发票的财务人员来说,录入发票信息很麻烦。
这个工作流能自动识别发票图片、提取关键信息并存入表格,最终完成邮件通知。
打开 n8n,新建一个工作流

第一步,我希望通过聊天触发,所以新建一个 on chat message 节点

第二步,搜索 OpenAI,

选择这个分析图片的节点:

第三步,添加一个 Code,对数据进行清洗

第四步,存入 Google 表格

选择增加一行新内容

第 5 步,提取聊天框中的邮箱,加一个 Code

第 6 步,增加个 if,判断邮箱是否为空

第 7 步,发送邮件,搜索 Gmail

选择 send a message

以上就是所有的工作流节点

接下来对每个节点,进行详细的配置说明。
双击节点,添加文件上传的属性

将开关打开

这时候就可以测试一下,比如我电脑上有这个发票图片:

在聊天框中,输入“发送给 你的邮箱@gmail.com”, 点击回形针 选择你的发票图片

点击发送,可以看到他的输出结果,名称是data0,我们后续会用到

进入官网 openai.com,选择 API 平台

在右上角 dashboard 里,进入 apikeys,新建一个 key

起个名字,比如叫 n8n

注意,你的账户里要先充钱,不然调不通。或者换其他的 API 平台
比如我发现我 OpenAI 没钱了,临时换了用 Gemini,步骤都是一样的

选择模型,提示词参考下面的
你是一个专业的“发票信息抽取器”。输入是一段发票文字(可能来自 OCR、邮件正文或聊天粘贴),输出是严格有效的 JSON,适合直接写入数据库/Google Sheets。
## 任务
从给定的发票文本中识别并抽取字段,返回单个 JSON 对象,放在一个标记代码块里(json ... )。不要输出任何解释、注释或多余字符。
## 输出要求
仅输出一个 JSON 代码块,不得包含额外文字。
不存在或不确定的值一律填 null。
日期统一为 YYYY-MM-DD(ISO 8601);
金额/数量等数值字段用 数字类型(而非字符串),保留精度但不要添加货币符号或千分位。
税率使用小数(例如 1% → 0.01),并同时提供显示字段(例如 "1%")。
items 数组必须存在;若无行项目,输出空数组 []。
## 字段定义(Schema)
{
"invoice": {
"title": "string|null",
"number": "string|null",
"issue_date": "YYYY-MM-DD|null",
"currency": "string|null",
"buyer": {
"name": "string|null",
"tax_id": "string|null",
"note": "string|null"
},
"seller": {
"name": "string|null",
"tax_id": "string|null"
},
"items": [
{
"name": "string|null",
"spec": "string|null",
"unit": "string|null",
"quantity": "number|null",
"unit_price": "number|null",
"amount_without_tax": "number|null",
"tax_rate": "number|null",
"tax_rate_display": "string|null",
"tax_amount": "number|null"
}
],
"totals": {
"amount_without_tax": "number|null",
"tax_total": "number|null",
"amount_with_tax": "number|null"
},
"issuer": "string|null"
}
}
输入类型选 字节类型,名称是上面的 data0

执行一下,发现右侧输出结果了

这一步是要把上一步的结果,更加格式化好看一些
输入代码参考:
// n8n Code 节点 - 发票数据解析
// 获取输入数据(处理可能的字符串格式)
const inputData = $input.first().json.choices[0].message.content;
// 如果输入是字符串,先解析为 JSON
let invoiceData;
if (typeof inputData === 'string') {
// 移除可能的 markdown 代码块标记
const cleanData = inputData.replace(/^```json\s*/, '').replace(/\s*```$/, '').trim();
invoiceData = JSON.parse(cleanData);
} else {
invoiceData = inputData;
}
// 提取发票数据
const invoice = invoiceData.invoice;
// 返回扁平化的 JSON 数据
return [{
json: {
title: invoice.title,
invoiceNumber: invoice.number,
issueDate: invoice.issue_date,
currency: invoice.currency,
issuer: invoice.issuer,
buyerName: invoice.buyer.name,
buyerTaxId: invoice.buyer.tax_id,
buyerNote: invoice.buyer.note,
sellerName: invoice.seller.name,
sellerTaxId: invoice.seller.tax_id,
itemName: invoice.items[0].name,
itemSpec: invoice.items[0].spec,
itemUnit: invoice.items[0].unit,
quantity: invoice.items[0].quantity,
unitPrice: invoice.items[0].unit_price,
amountWithoutTax: invoice.items[0].amount_without_tax,
taxRate: invoice.items[0].tax_rate,
taxRateDisplay: invoice.items[0].tax_rate_display,
taxAmount: invoice.items[0].tax_amount,
subtotal: invoice.totals.amount_without_tax,
totalTax: invoice.totals.tax_total,
grandTotal: invoice.totals.amount_with_tax
}
}];
执行一下,发现报错了

不要慌,直接用 ai 帮我们解决,我用 Claude,可以这样提问:
这是我在 n8n 中的输入:复制左侧的代码
这是我希望清洗的代码:复制上面的代码
但是遇到了这样的问题:复制报错信息
Claude 直接给我了修正后的代码:

// n8n Code 节点 - 发票数据解析
// 获取输入数据
const inputItem = $input.first().json;
// 根据实际的数据结构提取文本内容
let textContent;
if (inputItem.content && inputItem.content.parts && inputItem.content.parts[0]) {
textContent = inputItem.content.parts[0].text;
} else if (inputItem.choices && inputItem.choices[0]) {
// 兼容其他可能的数据结构
textContent = inputItem.choices[0].content?.parts?.[0]?.text ||
inputItem.choices[0].message?.content;
} else {
// 如果都不匹配,尝试直接使用
textContent = inputItem;
}
// 解析JSON数据
let invoiceData;
if (typeof textContent === 'string') {
// 移除可能的 markdown 代码块标记
const cleanData = textContent.replace(/^```json\s*/, '').replace(/\s*```$/, '').trim();
invoiceData = JSON.parse(cleanData);
} else {
invoiceData = textContent;
}
// 提取发票数据
const invoice = invoiceData.invoice;
// 处理多个商品项目 - 返回每个商品作为独立的记录
const items = [];
for (let i = 0; i < invoice.items.length; i++) {
const item = invoice.items[i];
items.push({
json: {
// 发票基本信息
title: invoice.title,
invoiceNumber: invoice.number,
issueDate: invoice.issue_date,
currency: invoice.currency,
issuer: invoice.issuer,
// 买方信息
buyerName: invoice.buyer.name,
buyerTaxId: invoice.buyer.tax_id,
buyerNote: invoice.buyer.note,
// 卖方信息
sellerName: invoice.seller.name,
sellerTaxId: invoice.seller.tax_id,
// 商品明细
itemIndex: i + 1, // 商品序号
itemName: item.name,
itemSpec: item.spec,
itemUnit: item.unit,
quantity: item.quantity,
unitPrice: item.unit_price,
amountWithoutTax: item.amount_without_tax,
taxRate: item.tax_rate,
taxRateDisplay: item.tax_rate_display,
taxAmount: item.tax_amount,
// 发票总计
subtotal: invoice.totals.amount_without_tax,
totalTax: invoice.totals.tax_total,
grandTotal: invoice.totals.amount_with_tax
}
});
}
// 如果没有商品项,至少返回发票基本信息
if (items.length === 0) {
return [{
json: {
title: invoice.title,
invoiceNumber: invoice.number,
issueDate: invoice.issue_date,
currency: invoice.currency,
issuer: invoice.issuer,
buyerName: invoice.buyer.name,
buyerTaxId: invoice.buyer.tax_id,
buyerNote: invoice.buyer.note,
sellerName: invoice.seller.name,
sellerTaxId: invoice.seller.tax_id,
subtotal: invoice.totals.amount_without_tax,
totalTax: invoice.totals.tax_total,
grandTotal: invoice.totals.amount_with_tax
}
}];
}
return items;
我重新复制上面的代码,执行后,发现结果果然正常了
ps: 记住了吗?以后就这样调试

googel 表格的配置,可以参考我前前面的攻略
我在 google 上新建了一个表格:n8n 发票

表格的列名参考:
发票标题 发票号码 开票日期 购方名称 购方税号 购方备注 销方名称 销方税号 商品名称 规格型号 单位 数量 单价 不含税金额 税率 税率显示 税额 合计不含税金额 合计税额 合计价税合计金额 开票人

把左侧对应的字段,都拖动列名对应的位置

点击执行,可以在右侧看到效果了

在 Google sheets 里,可以看到数据已经进来了

下面一步,是要把上面的关键信息,发到我们的邮箱里,这里涉及到一些配置信息,可以参考下篇攻略。
评论
0暂无评论