8
n8n 中文网amn8n.com

时间追踪与账单自动化

中级

这是一个Document Extraction领域的自动化工作流,包含 15 个节点。主要使用 Code, Jira, Gmail, Merge, ManualTrigger 等节点。 使用Jira和Gmail自动生成开发者发票和合规提醒

前置要求
  • Google 账号和 Gmail API 凭证
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "id": "VyFk3RnfdN7deOBG",
  "meta": {
    "instanceId": "8443f10082278c46aa5cf3acf8ff0f70061a2c58bce76efac814b16290845177",
    "templateCredsSetupCompleted": true
  },
  "name": "时间追踪与账单自动化:",
  "tags": [],
  "nodes": [
    {
      "id": "e62e8f45-3b5b-4a1d-a120-b0c07ebcc178",
      "name": "当点击\"执行工作流\"时",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -128,
        0
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "fadb2658-c02d-4819-b6eb-12114cc58127",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1120,
        -352
      ],
      "parameters": {
        "width": 256,
        "height": 336,
        "content": "合并提醒与发票数据流"
      },
      "typeVersion": 1
    },
    {
      "id": "4b5a9488-f7cd-436b-afaf-f698c756d933",
      "name": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        736,
        -368
      ],
      "parameters": {
        "width": 288,
        "height": 336,
        "content": "识别缺失时间日志的问题"
      },
      "typeVersion": 1
    },
    {
      "id": "174d0eec-6cce-4d0c-8b38-045b67f12f76",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        368,
        -368
      ],
      "parameters": {
        "width": 288,
        "height": 336,
        "content": "按团队成员汇总工时"
      },
      "typeVersion": 1
    },
    {
      "id": "13457e31-0e46-48e0-88a1-91610a40b646",
      "name": "便签3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        80,
        176
      ],
      "parameters": {
        "width": 304,
        "height": 368,
        "content": "获取所有带时间数据的项目问题"
      },
      "typeVersion": 1
    },
    {
      "id": "edcde860-f8c8-4229-be41-c4c026be9a46",
      "name": "便签4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        720,
        560
      ],
      "parameters": {
        "width": 304,
        "height": 400,
        "content": "生成带文本附件的发票摘要"
      },
      "typeVersion": 1
    },
    {
      "id": "d2450d54-f3e7-4732-bccd-2bc0e29b8238",
      "name": "便签5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1760,
        208
      ],
      "parameters": {
        "width": 272,
        "height": 384,
        "content": "向团队发送发票和提醒"
      },
      "typeVersion": 1
    },
    {
      "id": "559bf371-f7b3-477f-82d9-a022ed54856f",
      "name": "便签 6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1424,
        -384
      ],
      "parameters": {
        "width": 288,
        "height": 368,
        "content": "协调JSON和二进制附件"
      },
      "typeVersion": 1
    },
    {
      "id": "0da5f2fa-32f6-4c7e-85a0-474bf3550f4e",
      "name": "获取所有带时间数据的项目问题",
      "type": "n8n-nodes-base.jira",
      "position": [
        176,
        0
      ],
      "parameters": {
        "options": {},
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "id": "199LdjjU3PhhL8xb",
          "name": "saurabh jira"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "76eca7bd-8b7d-4119-a18f-81ae5c142653",
      "name": "按团队成员汇总工时",
      "type": "n8n-nodes-base.code",
      "position": [
        448,
        0
      ],
      "parameters": {
        "jsCode": "// Get all Jira issues from input\nconst items = $input.all().map(i => i.json);\n\n// Initialize grouped output\nconst grouped = {};\n\n// Iterate over each issue\nfor (const issue of items) {\n  const f = issue.fields || {};\n\n  const assigneeName = f.assignee?.displayName || 'Unassigned';\n  const email = f.assignee?.emailAddress || 'unknown@domain.com';\n  const projectName = f.project?.name || 'Unknown Project';\n  const sprint = f.customfield_10020?.[0]?.name || 'No Sprint';\n  const timeSpentSeconds = f.timespent || 0;\n  const timeSpentHours = (timeSpentSeconds / 3600).toFixed(2);\n\n  if (!grouped[assigneeName]) {\n    grouped[assigneeName] = {\n      assignee: assigneeName,\n      email,\n      project_name: projectName,\n      total_hours: 0,\n      issues: []\n    };\n  }\n\n  grouped[assigneeName].total_hours += parseFloat(timeSpentHours);\n  grouped[assigneeName].issues.push({\n    issue_key: issue.key,\n    summary: f.summary,\n    timespent_hours: timeSpentHours,\n    status: f.status?.name || 'Unknown',\n    priority: f.priority?.name || 'None',\n    sprint,\n  });\n}\n\n// Convert grouped object into array format\nconst output = Object.values(grouped).map(user => ({\n  json: {\n    assignee: user.assignee,\n    email: user.email,\n    project_name: user.project_name,\n    total_hours: user.total_hours.toFixed(2),\n    issues: user.issues,\n  }\n}));\n\nreturn output;\n"
      },
      "typeVersion": 2,
      "alwaysOutputData": false
    },
    {
      "id": "3ef99004-e0bd-4346-8ac0-6e83a88260f0",
      "name": "识别缺失时间日志的问题",
      "type": "n8n-nodes-base.code",
      "position": [
        832,
        0
      ],
      "parameters": {
        "jsCode": "const users = $input.all().map(i => i.json);\n\nconst reminders = [];\n\nfor (const user of users) {\n  const missing = user.issues.filter(i => parseFloat(i.timespent_hours) === 0);\n\n  if (missing.length > 0) {\n    const table = missing.map(i => \n      `<tr>\n        <td>${i.issue_key}</td>\n        <td>${i.summary}</td>\n        <td>${i.status}</td>\n        <td>${i.priority}</td>\n        <td>${i.sprint}</td>\n      </tr>`\n    ).join('');\n\n    reminders.push({\n      json: {\n        assignee: user.assignee,\n        email: user.email,\n        missing_count: missing.length,\n        project_name: user.project_name,\n        html_message: `\n          <p>Hi ${user.assignee},</p>\n          <p>Our system found ${missing.length} issues where no time is logged:</p>\n          <table border=\"1\" cellspacing=\"0\" cellpadding=\"5\">\n            <tr><th>Issue Key</th><th>Summary</th><th>Status</th><th>Priority</th><th>Sprint</th></tr>\n            ${table}\n          </table>\n          <p>Please log your hours for these issues before EOD to ensure accurate billing.</p>\n          <p>Thank you,<br>Automation Bot 🤖</p>\n        `\n      }\n    });\n  }\n}\n\nreturn reminders;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "db74c4fe-3bf1-45f9-a971-64ffc74695df",
      "name": "生成带文本附件的发票摘要",
      "type": "n8n-nodes-base.code",
      "position": [
        832,
        384
      ],
      "parameters": {
        "jsCode": "// Combined Invoice Generator + Text File Export\n\nconst users = $input.all().map(i => i.json);\nconst RATE = 50; // hourly rate\nconst results = [];\n\nfor (const user of users) {\n  const totalHours = parseFloat(user.total_hours || 0);\n  if (totalHours === 0) continue;\n\n  const totalAmount = totalHours * RATE;\n\n  // Build the plain text table\n  const issueLines = user.issues.map(i =>\n    `${i.issue_key.padEnd(10)} | ${i.summary.padEnd(30)} | ${i.timespent_hours} hrs | ${i.status}`\n  ).join('\\n');\n\n  // Final plain-text invoice content\n  const text = `\n===========================================\nINVOICE SUMMARY — ${user.project_name}\n===========================================\n\nAssignee: ${user.assignee}\nEmail: ${user.email}\nRate: $${RATE}/hour\n\n-------------------------------------------\nIssues\n-------------------------------------------\n${issueLines}\n\n-------------------------------------------\nTotal Hours: ${totalHours.toFixed(2)}\nTotal Amount: $${totalAmount.toFixed(2)}\n-------------------------------------------\n\nGenerated automatically by Techdome Billing Automation Bot.\n`;\n\n  // Push JSON + Binary for email attachment\n  results.push({\n    json: {\n      assignee: user.assignee,\n      email: user.email,\n      project_name: user.project_name,\n      total_hours: totalHours.toFixed(2),\n      total_amount: totalAmount.toFixed(2)\n    },\n    binary: {\n      data: {\n        fileName: `Invoice_${user.assignee}.txt`,\n        mimeType: 'text/plain',\n        data: Buffer.from(text, 'utf8').toString('base64')\n      }\n    }\n  });\n}\n\nreturn results;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "064ef92a-f13c-4926-8526-a1eb398fa3db",
      "name": "合并提醒与发票数据流",
      "type": "n8n-nodes-base.merge",
      "position": [
        1216,
        16
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "d3aec18e-05de-4ca9-8b19-263695b105b7",
      "name": "协调JSON和二进制附件",
      "type": "n8n-nodes-base.code",
      "position": [
        1488,
        16
      ],
      "parameters": {
        "jsCode": "// Combine JSON + Binary after Merge node\n// Safe for n8n v1.112.6 and later\n\nconst combined = [];\n\n// Get all incoming items\nconst items = $input.all();\n\n// We might have duplicates or partials, so we merge smartly\nfor (const item of items) {\n  const json = item.json || {};\n  const binary = item.binary || {};\n\n  // Case 1: this item already has both\n  if (json.email && binary.data) {\n    combined.push({ json, binary });\n    continue;\n  }\n\n  // Case 2: only has JSON\n  if (json.email && !binary.data) {\n    // Try to find its binary partner\n    const match = items.find(\n      (i) => i.binary && i.binary.data && i.json?.assignee === json.assignee\n    );\n\n    if (match) {\n      combined.push({ json, binary: match.binary });\n    } else {\n      combined.push({ json }); // fallback\n    }\n    continue;\n  }\n\n  // Case 3: only has Binary\n  if (binary.data && !json.email) {\n    // Try to find its JSON partner\n    const match = items.find(\n      (i) => i.json && i.json.assignee && i.binary === undefined\n    );\n    if (match) {\n      combined.push({ json: match.json, binary });\n    } else {\n      combined.push({ binary });\n    }\n    continue;\n  }\n}\n\nreturn combined;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "94b59a38-4e45-47dc-8c3d-d133ddd07455",
      "name": "向团队发送发票和提醒",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1808,
        16
      ],
      "webhookId": "0c82c299-6938-42ed-bda6-5007d79af34f",
      "parameters": {
        "sendTo": "={{ $json.email }}",
        "message": "=Hi {{$json[\"assignee\"]}},<br><br>\nPlease find attached your invoice summary for <strong>{{$json[\"project_name\"]}}</strong>.<br><br>\n📊 <b>Total Hours:</b> {{ $('Aggregate Hours by Team Member').item.json.total_hours }}<br>\nRegards,<br>\nTechdome Billing Automation Bot\n",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [
              {}
            ]
          }
        },
        "subject": "={{ $json.project_name }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "RchiXdmY8WaQhOSJ",
          "name": "Gmail account"
        }
      },
      "typeVersion": 2.1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "c7c9ac45-c34e-4b9e-a15d-0aea4da1f6d6",
  "connections": {
    "Aggregate Hours by Team Member": {
      "main": [
        [
          {
            "node": "Identify Issues with Missing Time Logs",
            "type": "main",
            "index": 0
          },
          {
            "node": "Generate Invoice Summary with Text Attachment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Invoices & Reminders to Team": {
      "main": [
        []
      ]
    },
    "Reconcile JSON & Binary Attachments": {
      "main": [
        [
          {
            "node": "Send Invoices & Reminders to Team",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking ‘Execute workflow’": {
      "main": [
        [
          {
            "node": " Fetch All Project Issues with Time Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Identify Issues with Missing Time Logs": {
      "main": [
        [
          {
            "node": "Combine Reminder & Invoice Data Streams",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Reminder & Invoice Data Streams": {
      "main": [
        [
          {
            "node": "Reconcile JSON & Binary Attachments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    " Fetch All Project Issues with Time Data": {
      "main": [
        [
          {
            "node": "Aggregate Hours by Team Member",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Invoice Summary with Text Attachment": {
      "main": [
        [
          {
            "node": "Combine Reminder & Invoice Data Streams",
            "type": "main",
            "index": 1
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

中级 - 文档提取

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
中级
节点数量15
分类1
节点类型6
难度说明

适合有一定经验的用户,包含 6-15 个节点的中等复杂度工作流

作者
Rahul Joshi

Rahul Joshi

@rahul08

Rahul Joshi is a seasoned technology leader specializing in the n8n automation tool and AI-driven workflow automation. With deep expertise in building open-source workflow automation and self-hosted automation platforms, he helps organizations eliminate manual processes through intelligent n8n ai agent automation solutions.

外部链接
在 n8n.io 查看

分享此工作流