具有Telegram和Sheets实时更新的智能POS系统
高级
这是一个CRM, AI Summarization领域的自动化工作流,包含 16 个节点。主要使用 Code, Wait, Webhook, Telegram, GoogleSheets 等节点。 使用Web POS界面、AI报告、Telegram提醒和Sheets创建销售订单
前置要求
- •HTTP Webhook 端点(n8n 会自动生成)
- •Telegram Bot Token
- •Google Sheets API 凭证
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
"id": "XRTJrZHlkDjGCLDq",
"meta": {
"instanceId": "0d045f8fe3802ff2be0bb9a9ea445ee6c9ed61973377effe00767e483681e2f4"
},
"name": "具有 Telegram 和 Sheets 实时更新的智能 POS 系统",
"tags": [],
"nodes": [
{
"id": "c6129fa5-cd4f-4903-8522-dcbe4fcd50af",
"name": "发送文本消息",
"type": "n8n-nodes-base.telegram",
"position": [
480,
420
],
"webhookId": "2ddba555-b1a7-4dca-b5d3-6ccb7866fad7",
"parameters": {
"text": "={{ $json.output }}",
"chatId": "YOUR_TELEGRAM_CHAT_ID",
"additionalFields": {
"appendAttribution": false
}
},
"credentials": {
"telegramApi": {
"id": "AZVFe6SQjkKyufRE",
"name": "Laporan Keuangan"
}
},
"typeVersion": 1.2
},
{
"id": "75d42049-e27a-4208-82ef-4a121812563e",
"name": "在表格中追加或更新行",
"type": "n8n-nodes-base.googleSheets",
"position": [
380,
40
],
"parameters": {
"columns": {
"value": {
"SALES ID": "={{ $json[\"SALES ID\"] }}",
"SALES QTY": "={{ $json[\"SALES QTY\"] }}",
"SALES DATE": "={{ $json[\"SALES DATE\"] }}",
"SALES DISCOUNT": "={{ $json[\"SALES DISCOUNT\"] }}",
"SALES PRICE (USD)": "={{ $json[\"SALES PRICE (USD)\"] }}",
"SALES PRODUCT NAME": "={{ $json[\"SALES PRODUCT NAME\"] }}",
"SALES CUSTOMER NAME": "={{ $json[\"SALES CUSTOMER NAME\"] }}"
},
"schema": [
{
"id": "SALES ID",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "SALES ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "SALES DATE",
"type": "string",
"display": true,
"required": false,
"displayName": "SALES DATE",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "SALES CUSTOMER NAME",
"type": "string",
"display": true,
"required": false,
"displayName": "SALES CUSTOMER NAME",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "SALES PRODUCT NAME",
"type": "string",
"display": true,
"required": false,
"displayName": "SALES PRODUCT NAME",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "SALES CATEGORY NAME",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "SALES CATEGORY NAME",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "SALES PRICE (USD)",
"type": "string",
"display": true,
"required": false,
"displayName": "SALES PRICE (USD)",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "SALES QTY",
"type": "string",
"display": true,
"required": false,
"displayName": "SALES QTY",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "SALES DISCOUNT",
"type": "string",
"display": true,
"required": false,
"displayName": "SALES DISCOUNT",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "SALES TOTAL",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "SALES TOTAL",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"SALES ID"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1157363351
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEETS_ID"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "wPmZzacn7hIP4akd",
"name": "Google Sheets account"
}
},
"executeOnce": false,
"typeVersion": 4.6
},
{
"id": "e8b420ed-e5cb-4835-9f9b-979bb9e44e05",
"name": "启动 webhook",
"type": "n8n-nodes-base.webhook",
"position": [
-1180,
220
],
"webhookId": "47332c29-1dd6-4aa2-b59e-80b4b265e3f4",
"parameters": {
"path": "smartpostsystem",
"options": {},
"responseMode": "responseNode"
},
"typeVersion": 1
},
{
"id": "84791b6d-a9b2-42b5-8c26-616c66adadd6",
"name": "获取产品数据",
"type": "n8n-nodes-base.googleSheets",
"position": [
-960,
220
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEETS_ID"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "wPmZzacn7hIP4akd",
"name": "Google Sheets account"
}
},
"executeOnce": true,
"typeVersion": 4.6
},
{
"id": "dff306ac-b6ad-4814-9ed2-2ffed96b420a",
"name": "响应 Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-460,
220
],
"parameters": {
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/html; charset=UTF-8"
}
]
}
},
"respondWith": "text",
"responseBody": "=<!DOCTYPE html>\n<html>\n\n<head>\n <title>Food Ordering App</title>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/css/bootstrap.min.css\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css\">\n <style>\n body {\n background-color: #f8f9fa;\n font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n }\n\n .main-container {\n display: flex;\n height: calc(100vh - 56px);\n max-width: 1400px;\n margin: 0 auto;\n background: white;\n box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);\n }\n\n .left-panel {\n flex: 2;\n padding: 20px;\n border-right: 1px solid #e9ecef;\n overflow-y: auto;\n }\n\n .right-panel {\n flex: 1;\n padding: 20px;\n background: #f8f9fa;\n overflow-y: auto;\n }\n\n .header-section {\n margin-bottom: 20px;\n }\n\n .category-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 15px;\n }\n\n .category-title {\n font-size: 24px;\n font-weight: 600;\n color: #333;\n margin: 0;\n }\n\n .search-filter {\n display: flex;\n gap: 10px;\n margin-bottom: 20px;\n }\n\n .search-box {\n flex: 1;\n position: relative;\n }\n\n .search-box input {\n width: 100%;\n padding: 10px 40px 10px 15px;\n border: 1px solid #ddd;\n border-radius: 8px;\n font-size: 14px;\n }\n\n .search-box i {\n position: absolute;\n right: 15px;\n top: 50%;\n transform: translateY(-50%);\n color: #666;\n }\n\n .filter-btn {\n padding: 10px 15px;\n border: 1px solid #ddd;\n border-radius: 8px;\n background: white;\n color: #666;\n }\n\n .category-tabs {\n display: flex;\n gap: 10px;\n margin-bottom: 20px;\n overflow-x: auto;\n padding-bottom: 5px;\n }\n\n .category-tab {\n padding: 8px 16px;\n border-radius: 20px;\n border: none;\n background: #e9ecef;\n color: #666;\n white-space: nowrap;\n cursor: pointer;\n transition: all 0.3s;\n }\n\n .category-tab.active {\n background: #6c757d;\n color: white;\n }\n\n .products-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));\n gap: 20px;\n }\n\n .product-card {\n background: white;\n border-radius: 12px;\n padding: 15px;\n text-align: center;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n transition: transform 0.2s;\n }\n\n .product-card:hover {\n transform: translateY(-2px);\n }\n\n .product-image {\n width: 100%;\n height: 120px;\n object-fit: cover;\n border-radius: 8px;\n margin-bottom: 10px;\n }\n\n .product-name {\n font-weight: 600;\n margin-bottom: 5px;\n color: #333;\n }\n\n .product-price {\n color: #666;\n font-weight: 500;\n margin-bottom: 10px;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n }\n\n .discount-badge {\n background: #dc3545;\n color: white;\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 12px;\n font-weight: 600;\n }\n\n .add-btn {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n border: none;\n background: #333;\n color: white;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s;\n }\n\n .add-btn:hover {\n background: #555;\n }\n\n .navbar {\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n }\n\n .order-header {\n font-size: 20px;\n font-weight: 600;\n margin-bottom: 20px;\n color: #333;\n }\n\n .user-profile {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-bottom: 20px;\n padding: 10px;\n background: white;\n border-radius: 8px;\n }\n\n .user-avatar {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n background: #6c757d;\n display: flex;\n align-items: center;\n justify-content: center;\n color: white;\n font-weight: 600;\n }\n\n .customer-form {\n background: white;\n border-radius: 8px;\n padding: 15px;\n margin-bottom: 20px;\n }\n\n .form-title {\n font-size: 16px;\n font-weight: 600;\n margin-bottom: 15px;\n color: #333;\n }\n\n .form-group {\n margin-bottom: 15px;\n }\n\n .form-group label {\n display: block;\n margin-bottom: 5px;\n font-weight: 500;\n color: #333;\n font-size: 14px;\n }\n\n .form-control {\n width: 100%;\n padding: 10px 12px;\n border: 1px solid #ddd;\n border-radius: 6px;\n font-size: 14px;\n transition: border-color 0.3s;\n }\n\n .form-control:focus {\n outline: none;\n border-color: #6f42c1;\n box-shadow: 0 0 0 2px rgba(111, 66, 193, 0.1);\n }\n\n .order-items {\n margin-bottom: 20px;\n }\n\n .order-item {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px;\n background: white;\n border-radius: 8px;\n margin-bottom: 10px;\n }\n\n .item-thumbnail {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n object-fit: cover;\n }\n\n .item-details {\n flex: 1;\n }\n\n .item-name {\n font-weight: 500;\n color: #333;\n margin-bottom: 2px;\n }\n\n .item-price {\n color: #666;\n font-size: 14px;\n }\n\n .item-controls {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n\n .qty-btn {\n width: 25px;\n height: 25px;\n border-radius: 50%;\n border: 1px solid #ddd;\n background: white;\n color: #666;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .qty-display {\n min-width: 20px;\n text-align: center;\n font-weight: 500;\n }\n\n .order-summary {\n background: white;\n border-radius: 8px;\n padding: 15px;\n margin-bottom: 20px;\n }\n\n .summary-row {\n display: flex;\n justify-content: space-between;\n margin-bottom: 8px;\n }\n\n .summary-row.total {\n font-weight: 600;\n font-size: 18px;\n border-top: 1px solid #e9ecef;\n padding-top: 10px;\n margin-top: 10px;\n }\n\n .continue-btn {\n width: 100%;\n padding: 15px;\n background: #6f42c1;\n color: white;\n border: none;\n border-radius: 8px;\n font-weight: 600;\n font-size: 16px;\n cursor: pointer;\n transition: background 0.3s;\n }\n\n .continue-btn:hover {\n background: #5a32a3;\n }\n\n @media (max-width: 768px) {\n .main-container {\n flex-direction: column;\n height: auto;\n }\n\n .left-panel,\n .right-panel {\n flex: none;\n }\n\n .products-grid {\n grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));\n }\n }\n </style>\n</head>\n\n<body>\n <!-- Navigation Menu -->\n <nav class=\"navbar navbar-expand-lg navbar-dark bg-dark\">\n <div class=\"container-fluid\">\n <a class=\"navbar-brand\" href=\"#\">POS SYSTEM</a>\n </div>\n </nav>\n\n <div class=\"main-container\">\n <!-- Left Panel - Items Selection -->\n <div class=\"left-panel\">\n <div class=\"header-section\">\n <div class=\"category-header\">\n <h1 class=\"category-title\">POS SYSTEM</h1>\n </div>\n\n <div class=\"search-filter\">\n <div class=\"search-box\">\n <input type=\"text\" id=\"searchInput\" placeholder=\"Search products...\" onkeyup=\"filterProducts()\">\n <i class=\"fas fa-search\"></i>\n </div>\n </div>\n\n <div class=\"category-tabs\" id=\"categoryTabs\">\n <!-- Categories will be dynamically generated from JSON data -->\n </div>\n </div>\n\n <div class=\"products-grid\" id=\"productsGrid\">\n <!-- Products will be dynamically generated from JSON data -->\n </div>\n </div>\n\n <!-- Right Panel - Current Order -->\n <div class=\"right-panel\">\n <h2 class=\"order-header\">Current Order</h2>\n\n <div class=\"customer-form\">\n <h4 class=\"form-title\">Customer Information</h4>\n <div class=\"form-group\">\n <label for=\"customerName\">Name</label>\n <input type=\"text\" id=\"customerName\" class=\"form-control\" placeholder=\"Enter customer name\"\n required>\n </div>\n </div>\n\n <div class=\"order-items\" id=\"orderItems\">\n <!-- Order items will be dynamically added here -->\n </div>\n\n <div class=\"order-summary\">\n <div class=\"summary-row\">\n <span>Subtotal</span>\n <span id=\"subtotal\">$0.00</span>\n </div>\n <div class=\"summary-row\">\n <span>Discount</span>\n <span id=\"discount\">$0.00</span>\n </div>\n <div class=\"summary-row total\">\n <span>Total</span>\n <span id=\"total\">$0.00</span>\n </div>\n </div>\n\n <form action=\"{{ $resumeWebhookUrl }}\" method=\"post\" id=\"orderForm\">\n <!-- Hidden inputs for order data -->\n <input type=\"hidden\" name=\"customerName\" id=\"hiddenCustomerName\">\n <input type=\"hidden\" name=\"orderItems\" id=\"hiddenOrderItems\">\n <input type=\"hidden\" name=\"orderTotals\" id=\"hiddenOrderTotals\">\n\n <button type=\"submit\" class=\"continue-btn\" onclick=\"prepareFormData(event)\">Continue</button>\n </form>\n </div>\n </div>\n\n <script>\n // Products data from n8n workflow - separate arrays\n const productIds = `{{ $json.productId }}`;\n const productNames = `{{ $json.productName }}`;\n const productImages = `{{ $json.productImage }}`;\n const categoryNames = `{{ $json.productCategoryName }}`;\n const prices = `{{ $json.productPriceUsd }}`;\n const discounts = `{{ $json.productDiscount }}`;\n \n let orderItems = [];\n let orderTotal = 0;\n let currentCategory = 'All';\n let allProducts = [];\n\n // Parse products data from n8n\n function initializeProducts() {\n try {\n // Parse the string arrays into actual arrays\n const parsedProductIds = JSON.parse(productIds);\n const parsedProductNames = JSON.parse(productNames);\n const parsedProductImages = JSON.parse(productImages);\n const parsedCategoryNames = JSON.parse(categoryNames);\n const parsedPrices = JSON.parse(prices);\n const parsedDiscounts = JSON.parse(discounts);\n\n // Combine arrays into product objects\n allProducts = parsedProductIds.map((id, index) => ({\n 'PRODUCT ID': id,\n 'PRODUCT NAME': parsedProductNames[index],\n 'PRODUCT IMAGE': parsedProductImages[index],\n 'CATEGORY NAME': parsedCategoryNames[index],\n 'PRICE (USD)': parsedPrices[index],\n 'DISCOUNT': parsedDiscounts[index]\n }));\n\n generateCategoryTabs();\n generateProductsGrid();\n } catch (error) {\n console.error('Error parsing products data:', error);\n // Fallback to empty array if parsing fails\n allProducts = [];\n }\n }\n\n function addToOrder(name, price, image, originalPrice, discountRate) {\n const existingItem = orderItems.find(item => item.name === name);\n\n if (existingItem) {\n existingItem.quantity += 1;\n existingItem.total = existingItem.quantity * existingItem.price;\n } else {\n orderItems.push({\n name: name,\n price: price,\n originalPrice: originalPrice,\n discountRate: discountRate,\n image: image,\n quantity: 1,\n total: price\n });\n }\n\n updateOrderDisplay();\n calculateTotals();\n }\n\n function updateQuantity(name, change) {\n const item = orderItems.find(item => item.name === name);\n\n if (item) {\n item.quantity += change;\n\n if (item.quantity <= 0) {\n orderItems = orderItems.filter(item => item.name !== name);\n } else {\n item.total = item.quantity * item.price;\n }\n\n updateOrderDisplay();\n calculateTotals();\n }\n }\n\n function updateOrderDisplay() {\n const orderItemsContainer = document.getElementById('orderItems');\n orderItemsContainer.innerHTML = '';\n\n orderItems.forEach(item => {\n const itemElement = document.createElement('div');\n itemElement.className = 'order-item';\n\n // Calculate item discount\n const itemDiscount = (item.originalPrice * item.quantity * item.discountRate);\n const discountText = item.discountRate > 0 ? `<br><small style=\"color: #dc3545;\">-$${itemDiscount.toFixed(2)} discount</small>` : '';\n\n itemElement.innerHTML = `\n <img src=\"${item.image}\" class=\"item-thumbnail\" alt=\"${item.name}\">\n <div class=\"item-details\">\n <div class=\"item-name\">${item.name}</div>\n <div class=\"item-price\">$${item.price.toFixed(2)}${discountText}</div>\n </div>\n <div class=\"item-controls\">\n <button class=\"qty-btn\" onclick=\"updateQuantity('${item.name}', -1)\">-</button>\n <span class=\"qty-display\">${item.quantity}</span>\n <button class=\"qty-btn\" onclick=\"updateQuantity('${item.name}', 1)\">+</button>\n </div>\n `;\n orderItemsContainer.appendChild(itemElement);\n });\n }\n\n function calculateTotals() {\n const subtotal = orderItems.reduce((sum, item) => sum + item.total, 0);\n\n // Calculate total discount based on individual item discounts\n const totalDiscount = orderItems.reduce((sum, item) => {\n const itemDiscount = (item.originalPrice * item.quantity * item.discountRate);\n return sum + itemDiscount;\n }, 0);\n\n const total = subtotal;\n\n document.getElementById('subtotal').textContent = `$${subtotal.toFixed(2)}`;\n document.getElementById('discount').textContent = `$${totalDiscount.toFixed(2)}`;\n document.getElementById('total').textContent = `$${total.toFixed(2)}`;\n }\n\n function prepareFormData(event) {\n event.preventDefault();\n\n // Validate customer information\n const customerName = document.getElementById('customerName').value.trim();\n\n if (!customerName) {\n alert('Please enter customer name.');\n document.getElementById('customerName').focus();\n return;\n }\n\n if (orderItems.length === 0) {\n alert('Please add items to your order first.');\n return;\n }\n\n // Prepare order data\n const orderData = {\n customer: {\n name: customerName\n },\n items: orderItems,\n totals: {\n subtotal: orderItems.reduce((sum, item) => sum + item.total, 0),\n total: orderItems.reduce((sum, item) => sum + item.total, 0)\n }\n };\n\n // Set hidden form values\n document.getElementById('hiddenCustomerName').value = customerName;\n document.getElementById('hiddenOrderItems').value = JSON.stringify(orderItems);\n document.getElementById('hiddenOrderTotals').value = JSON.stringify(orderData.totals);\n\n // Show loading state\n const submitBtn = document.querySelector('.continue-btn');\n const originalText = submitBtn.textContent;\n submitBtn.textContent = 'Processing...';\n submitBtn.disabled = true;\n\n // Submit the form\n document.getElementById('orderForm').submit();\n }\n\n // Function to get unique categories from products data\n function getCategories() {\n const categories = [...new Set(allProducts.map(product => product['CATEGORY NAME']))];\n return ['All', ...categories];\n }\n\n // Function to generate category tabs\n function generateCategoryTabs() {\n const categories = getCategories();\n const categoryTabsContainer = document.getElementById('categoryTabs');\n categoryTabsContainer.innerHTML = '';\n\n categories.forEach(category => {\n const tab = document.createElement('button');\n tab.className = 'category-tab';\n if (category === 'All') {\n tab.classList.add('active');\n }\n tab.textContent = category;\n tab.onclick = () => filterByCategory(category);\n categoryTabsContainer.appendChild(tab);\n });\n }\n\n // Function to filter products by category\n function filterByCategory(category) {\n currentCategory = category;\n\n // Update active tab\n document.querySelectorAll('.category-tab').forEach(tab => {\n tab.classList.remove('active');\n if (tab.textContent === category) {\n tab.classList.add('active');\n }\n });\n\n // Filter products\n if (category === 'All') {\n // Re-initialize all products\n initializeProducts();\n } else {\n // Filter by category\n const parsedProductIds = JSON.parse(productIds);\n const parsedProductNames = JSON.parse(productNames);\n const parsedProductImages = JSON.parse(productImages);\n const parsedCategoryNames = JSON.parse(categoryNames);\n const parsedPrices = JSON.parse(prices);\n const parsedDiscounts = JSON.parse(discounts);\n\n // Combine arrays into product objects and filter by category\n allProducts = parsedProductIds.map((id, index) => ({\n 'PRODUCT ID': id,\n 'PRODUCT NAME': parsedProductNames[index],\n 'PRODUCT IMAGE': parsedProductImages[index],\n 'CATEGORY NAME': parsedCategoryNames[index],\n 'PRICE (USD)': parsedPrices[index],\n 'DISCOUNT': parsedDiscounts[index]\n })).filter(product => product['CATEGORY NAME'] === category);\n\n generateProductsGrid();\n }\n }\n\n // Function to generate product cards\n function generateProductsGrid() {\n const productsGrid = document.getElementById('productsGrid');\n productsGrid.innerHTML = '';\n\n allProducts.forEach(product => {\n const productCard = document.createElement('div');\n productCard.className = 'product-card';\n\n // Calculate final price with discount\n const originalPrice = product['PRICE (USD)'];\n const discount = product['DISCOUNT'];\n const finalPrice = originalPrice * (1 - discount);\n\n // Use image from JSON data\n const imageUrl = product['PRODUCT IMAGE'];\n\n productCard.innerHTML = `\n <img src=\"${imageUrl}\" class=\"product-image\" alt=\"${product['PRODUCT NAME']}\">\n <div class=\"product-name\">${product['PRODUCT NAME']}</div>\n <div class=\"product-price\">\n $${finalPrice.toFixed(2)}\n ${discount > 0 ? `<span class=\"discount-badge\">-${(discount * 100).toFixed(0)}%</span>` : ''}\n </div>\n <button class=\"add-btn\" onclick=\"addToOrder('${product['PRODUCT NAME']}', ${finalPrice}, '${imageUrl}', ${originalPrice}, ${discount})\">+</button>\n `;\n\n productsGrid.appendChild(productCard);\n });\n }\n\n // Function to filter products by search\n function filterProducts() {\n const searchTerm = document.getElementById('searchInput').value.toLowerCase();\n const productsGrid = document.getElementById('productsGrid');\n productsGrid.innerHTML = '';\n\n const filteredProducts = allProducts.filter(product =>\n product['PRODUCT NAME'].toLowerCase().includes(searchTerm) ||\n product['CATEGORY NAME'].toLowerCase().includes(searchTerm)\n );\n\n filteredProducts.forEach(product => {\n const productCard = document.createElement('div');\n productCard.className = 'product-card';\n\n // Calculate final price with discount\n const originalPrice = product['PRICE (USD)'];\n const discount = product['DISCOUNT'];\n const finalPrice = originalPrice * (1 - discount);\n\n // Use image from JSON data\n const imageUrl = product['PRODUCT IMAGE'];\n\n productCard.innerHTML = `\n <img src=\"${imageUrl}\" class=\"product-image\" alt=\"${product['PRODUCT NAME']}\">\n <div class=\"product-name\">${product['PRODUCT NAME']}</div>\n <div class=\"product-price\">\n $${finalPrice.toFixed(2)}\n ${discount > 0 ? `<span class=\"discount-badge\">-${(discount * 100).toFixed(0)}%</span>` : ''}\n </div>\n <button class=\"add-btn\" onclick=\"addToOrder('${product['PRODUCT NAME']}', ${finalPrice}, '${imageUrl}', ${originalPrice}, ${discount})\">+</button>\n `;\n\n productsGrid.appendChild(productCard);\n });\n }\n\n // Initialize the display\n initializeProducts();\n updateOrderDisplay();\n calculateTotals();\n </script>\n</body>\n\n</html>"
},
"typeVersion": 1.4
},
{
"id": "da9ca9ef-1767-4fe9-aff2-9e15a7194fd8",
"name": "等待点击",
"type": "n8n-nodes-base.wait",
"position": [
-240,
220
],
"webhookId": "2b00ae48-f8f7-49f5-8237-d50f83ff5aa2",
"parameters": {
"resume": "webhook",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 1
},
{
"id": "7e69a7e5-5a31-41b8-9ac2-bae76e807dd1",
"name": "响应点击",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-40,
220
],
"parameters": {
"options": {},
"respondWith": "text",
"responseBody": "=<!DOCTYPE html>\n<html lang=\"id\">\n<head>\n <meta charset=\"UTF-8\" />\n <title>Processing Order</title>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <style>\n body {\n font-family: Arial, sans-serif;\n text-align: center;\n padding-top: 100px;\n background-color: #f0f9f4;\n }\n .icon {\n font-size: 64px;\n color: #4CAF50;\n }\n .message {\n font-size: 24px;\n color: #333;\n margin-top: 20px;\n }\n .redirect {\n font-size: 16px;\n color: #666;\n margin-top: 10px;\n }\n </style>\n <script>\n setTimeout(function() {\n window.location.href = \"{{ $resumeWebhookUrl }}\";\n }, 3000); // Wait 3 seconds\n </script>\n</head>\n<body>\n <div class=\"icon\">✅</div>\n <div class=\"message\">Order is successfully processed</div>\n <div class=\"redirect\">You will be redirected shortly...</div>\n</body>\n</html>\n"
},
"typeVersion": 1
},
{
"id": "2f766abf-2d22-4721-9942-247d5534b482",
"name": "格式化表格数据",
"type": "n8n-nodes-base.code",
"position": [
180,
40
],
"parameters": {
"jsCode": "// Get data from body\nconst data = $input.first().json.body;\n\n// Parse orderItems and orderTotals\nconst items = JSON.parse(data.orderItems);\n\n// Function to generate unique Sales ID\nfunction generateSalesId() {\n const timestamp = Date.now();\n const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');\n return `S-${timestamp}-${random}`;\n}\n\n// Get today's date\nconst today = new Date().toISOString().split('T')[0];\n\n// Generate output\nconst salesId = generateSalesId();\nconst output = items.map(item => {\n const discount = Number((item.originalPrice - item.price).toFixed(2));\n return {\n 'SALES ID': salesId,\n 'SALES DATE': today, \n 'SALES CUSTOMER NAME': data.customerName,\n 'SALES PRODUCT NAME': item.name,\n 'SALES CATEGORY NAME': '', // Category data not available yet\n 'SALES PRICE (USD)': item.price,\n 'SALES QTY': item.quantity,\n 'SALES DISCOUNT': discount,\n 'SALES TOTAL': item.total\n };\n});\n\nreturn output.map(item => ({ json: item }));\n"
},
"typeVersion": 2
},
{
"id": "89ac1e1d-3a2d-47b7-b0c4-e0e06a90471e",
"name": "AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
180,
420
],
"parameters": {
"text": "=customer name : {{ $json.body.customerName }}\norder items : {{ $json.body.orderItems }}\norder total : {{ $json.body.orderTotals }}\n\nSales report format : \nNew sales! (opening or greetings to the owner )\ncustomer name : \norder details :\nHave a good day (closing)",
"options": {
"systemMessage": "=You are a virtual assistant whose primary task is to create sales reports for business owners.\nWrite in a simple and friendly format. Use emojis to make it more interactive.\nSome item prices are separated by commas.\nFormat all numbers such as prices, subtotal, and total to 2 decimal places only (e.g., 12.97, not 12.969999999999999).\nAvoid using long floating-point numbers.\n\nAvoid using special characters that may break Markdown formatting, such as:\n*, _, [, ], (, ), ~, >, #, +, -, =, {, }, ., !, $.\nUse plain text without special symbols unless necessary.\nDo not use Markdown or HTML formatting."
},
"promptType": "define"
},
"typeVersion": 2
},
{
"id": "f4b29ac5-8dee-48bc-83a3-136795a447fa",
"name": "OpenRouter聊天模型",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
"position": [
180,
600
],
"parameters": {
"model": "google/gemini-2.0-flash-exp:free",
"options": {}
},
"credentials": {
"openRouterApi": {
"id": "5gucapot70b4Qz8b",
"name": "OpenRouter ASNM"
}
},
"typeVersion": 1
},
{
"id": "72a622bc-b3b7-4840-9b08-686c65b482f9",
"name": "便签",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1760,
-440
],
"parameters": {
"width": 500,
"height": 1320,
"content": "# 具有 Telegram 和 Sheets 实时更新的智能 POS 系统"
},
"typeVersion": 1
},
{
"id": "ae33dc7d-b8c1-483d-a685-370c5b08e61f",
"name": "为 webhook 格式化数据",
"type": "n8n-nodes-base.code",
"position": [
-740,
220
],
"parameters": {
"jsCode": "// Get all input data\nconst input = $input.all();\n\n// Extract product data columns\nconst productId = input.map(item => item.json[\"PRODUCT ID\"]);\nconst productName = input.map(item => item.json[\"PRODUCT NAME\"]);\nconst productImage = input.map(item => item.json[\"PRODUCT IMAGE\"]);\nconst productCategoryName = input.map(item => item.json[\"PRODUCT CATEGORY NAME\"]);\nconst productPriceUsd = input.map(item => item.json[\"PRODUCT PRICE (USD)\"]);\nconst productDiscount = input.map(item => item.json[\"PRODUCT DISCOUNT\"]);\n\n// Return in JSON string format\nreturn [{\n json: {\n // Product data\n productId: JSON.stringify(productId),\n productName: JSON.stringify(productName),\n productImage: JSON.stringify(productImage),\n productCategoryName: JSON.stringify(productCategoryName),\n productPriceUsd: JSON.stringify(productPriceUsd),\n productDiscount: JSON.stringify(productDiscount),\n\n // Webhook URL (if needed for redirect or resubmit)\n webhookUrl: '{{ $json.webhookUrl }}',\n resumeWebhookUrl: '{{ $resumeWebhookUrl }}'\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "7b344857-b997-4882-94c9-770c19df9394",
"name": "便签1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-540,
40
],
"parameters": {
"color": 2,
"width": 640,
"height": 340,
"content": "- 以 HTML 格式创建 POS 界面"
},
"typeVersion": 1
},
{
"id": "570f22fa-08aa-4b1f-8e84-de57f893f90f",
"name": "便签2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1220,
40
],
"parameters": {
"width": 640,
"height": 340,
"content": "- 提供可从浏览器访问的 webhook 端点"
},
"typeVersion": 1
},
{
"id": "a873b6fd-a22c-46c6-a6aa-0a88188df4e0",
"name": "便签3",
"type": "n8n-nodes-base.stickyNote",
"position": [
140,
-120
],
"parameters": {
"color": 4,
"width": 500,
"height": 340,
"content": "- 创建适合 Google Sheets 格式的销售数据。"
},
"typeVersion": 1
},
{
"id": "28e4314f-7883-4517-95f6-3653674085a2",
"name": "便签4",
"type": "n8n-nodes-base.stickyNote",
"position": [
140,
300
],
"parameters": {
"color": 5,
"width": 500,
"height": 420,
"content": "- 接收订单数据,将其转换为业务所有者友好的销售报告(使用 LLM),然后将报告发送到 Telegram。"
},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "5be9223c-a7a3-4295-a853-a7776c722d17",
"connections": {
"AI Agent": {
"main": [
[
{
"node": "Send a text message",
"type": "main",
"index": 0
}
]
]
},
"Wait for click": {
"main": [
[
{
"node": "Respond to click",
"type": "main",
"index": 0
}
]
]
},
"Respond to click": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
},
{
"node": "Format data for sheet",
"type": "main",
"index": 0
}
]
]
},
"Get products data": {
"main": [
[
{
"node": "Format data for webhook",
"type": "main",
"index": 0
}
]
]
},
"Start the webhook": {
"main": [
[
{
"node": "Get products data",
"type": "main",
"index": 0
}
]
]
},
"Respond to Webhook": {
"main": [
[
{
"node": "Wait for click",
"type": "main",
"index": 0
}
]
]
},
"Format data for sheet": {
"main": [
[
{
"node": "Append or update row in sheet",
"type": "main",
"index": 0
}
]
]
},
"OpenRouter Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Format data for webhook": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
}
}
}常见问题
如何使用这个工作流?
复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。
这个工作流适合什么场景?
高级 - 客户关系管理, AI 摘要总结
需要付费吗?
本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。
相关工作流推荐
通过 Google RSS、Openrouter 和 Telegram 的自动化股票新闻提醒
基于Google RSS、Gemini和Telegram通知的自动化股票新闻提醒
If
Code
Merge
+9
22 节点Budi SJ
加密货币交易
使用Google Vision OCR、Telegram和Google Sheets的自动化财务报告
使用Google Vision OCR、AI和Telegram处理收据到Google表格
If
Set
Code
+12
31 节点Budi SJ
发票处理
每日 WhatsApp 群组智能分析:GPT-4.1 分析与语音消息转录
每日 WhatsApp 群组智能分析:GPT-4.1 分析与语音消息转录
If
Set
Code
+20
52 节点Daniel Lianes
杂项
监控业务日常指标
使用Gemini从表格自动生成销售洞察并即时通知团队
Code
Gmail
Trello
+9
15 节点Rully Saputra
客户关系管理
通过Telegram的DOI URL自动导入研究论文到Zotero
从Telegram导入研究论文到Zotero,含AI摘要总结
If
Set
Code
+7
25 节点Budi SJ
AI 摘要总结
通过AI分析HubSpot和Google Sheets数据预测客户流失
通过AI分析HubSpot和Google Sheets数据预测客户流失
Set
Code
Merge
+17
27 节点PollupAI
客户关系管理
工作流信息
难度等级
高级
节点数量16
分类2
节点类型9
作者
Budi SJ
@budisjI’m a Product Designer who also works as an Automation Developer. With a background in product design and systems thinking, I build user-centered workflows. My focus is on helping teams and businesses work more productively through impactful automation systems.
外部链接
在 n8n.io 查看 →
分享此工作流