基于 Cloudflare Workers + KV 实现多 Shopify 站点实时订单推送至飞书群

本文将分享如何利用 Cloudflare Workers 结合 KV 数据库,搭建一个无服务器(Serverless)、零成本、支持多 Shopify 站点的订单实时通知系统,并将精心排版的交互式卡片实时推送到飞书(Feishu / Lark)群聊。


🌟 方案亮点

  1. 多站点支持:基于 Cloudflare KV 灵活管理多店铺密钥,只需部署一个 Worker 节点,即可接收无限个独立站的 Webhook。
  2. 高安全性:严格实现 Shopify HMAC SHA-256 签名校验,并加入防时序攻击(Timing Attack)与防 Stack Overflow 的安全转换补丁。
  3. 数据丰富:卡片包含订单总额、客户姓名/邮箱、目的国家、运费、折扣明细(含优惠码与自动折扣)、商品 SKUs 及客户备注。
  4. 客户画像(新/老客识别):自动提取客户在店铺的历史订单数,精细标识 🔥 第 1 次订购 (新客)🔁 复购
  5. 精炼卡片排版:采用分栏与灰色背景框布局,信息层级清晰,提升团队阅读体验。

🚀 部署步骤

第一步:创建并配置 Cloudflare KV 数据库

  1. 登录 Cloudflare Dashboard。
  2. 进入侧边栏 Storage & Databases -> KV
  3. 点击 Create Namespace,命名为 STORE_PUSH
  4. 进入刚创建的 STORE_PUSH 命名空间,在 KV Pairs 中添加你的店铺配置:
    • Key(键):填入 Shopify 的自带原始域名(格式必须为 xxx.myshopify.com,而非自定义绑定的顶级域名)。
    • Value(值):填入如下 JSON 结构的字符串:
1
2
3
4
5
6
{
"name": "美国官网",
"secret": "shpss_xxx",
"feishu_webhook": "https://..."
}

参数说明:

  • name: 飞书卡片上展示的店铺别名(如:美国站、欧洲站)。
  • secret: Shopify 后台 Webhook 页面最底部生成的 HMAC Secret Key
  • feishu_webhook: 该店铺订单需要推送到的飞书机器人 Webhook 地址。

第二步:创建 Cloudflare Worker 并绑定 KV

  1. 在 Cloudflare 侧边栏进入 Workers & Pages -> Create application -> Create Worker
  2. 命名 Worker(例如 shopify-feishu-notifier),点击 Deploy
  3. 进入该 Worker 的设置界面:Settings -> Bindings(或 Variables)。
  4. KV Namespace Bindings 区域点击 Add
  • Variable name(变量名):必须填写 STORE_PUSH(需与代码中的 env.STORE_PUSH 一致)。
  • KV namespace:选择第一步创建的 STORE_PUSH

第三步:粘贴 Worker 核心代码

点击 Edit code,将原有的模版代码替换为以下完整代码,然后点击 Save and Deploy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
/**
* 安全的 Base64 转换(防止栈溢出)
*/
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}

/**
* 恒定时间比较字符串(防时序攻击)
*/
function safeCompare(a, b) {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}

/**
* 验证 Shopify HMAC 签名
*/
async function verifyShopifySignature(request, rawBody, secret) {
const hmacHeader = request.headers.get('X-Shopify-Hmac-Sha256');
if (!hmacHeader || !secret) return false;

const encoder = new TextEncoder();
const keyBuffer = encoder.encode(secret);
const bodyBuffer = encoder.encode(rawBody);

const cryptoKey = await crypto.subtle.importKey(
'raw',
keyBuffer,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);

const signatureBuffer = await crypto.subtle.sign('HMAC', cryptoKey, bodyBuffer);
const calculatedHmac = arrayBufferToBase64(signatureBuffer);

return safeCompare(calculatedHmac, hmacHeader);
}

export default {
async fetch(request, env, ctx) {
if (request.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405 });
}

// 1. 获取 Shopify 店铺自带域名 (如: rbc-cycling.myshopify.com)
const shopDomain = request.headers.get('X-Shopify-Shop-Domain');
if (!shopDomain) {
return new Response('Bad Request: Missing Shop Domain Header', { status: 400 });
}

// 2. 从 Cloudflare KV 异步查询店铺配置
let currentStoreRaw = null;
try {
currentStoreRaw = await env.STORE_PUSH.get(shopDomain);
} catch (e) {
console.error('KV Storage 读取失败:', e);
return new Response('Internal Server Error', { status: 500 });
}

if (!currentStoreRaw) {
console.error(`未注册的店铺域名: ${shopDomain}`);
return new Response('Unauthorized: Unknown Shop Domain', { status: 403 });
}

const currentStore = JSON.parse(currentStoreRaw);
const rawBody = await request.text();

// 3. 使用 KV 中该店铺专属的 Secret 进行签名校验
const isValid = await verifyShopifySignature(request, rawBody, currentStore.secret);
if (!isValid) {
console.error(`签名校验失败: ${shopDomain}`);
return new Response('Unauthorized: Invalid Signature', { status: 401 });
}

try {
const order = JSON.parse(rawBody);

// 数据提取
const storeName = currentStore.name || shopDomain;
const orderName = order.name || '未知订单';
const totalPrice = order.total_price || '0.00';
const currency = order.currency || 'USD';

// 👤 客户姓名与下单次数提取
let customerName = '游客/无客户信息';
let orderCountText = '游客下单';
if (order.customer) {
const firstName = order.customer.first_name || '';
const lastName = order.customer.last_name || '';
customerName = `${firstName} ${lastName}`.trim() || '匿名客户';

const count = order.customer.orders_count || 1;
orderCountText = count === 1 ? '🔥 第 1 次订购 (新客)' : `🔁 第 ${count} 次复购`;
}

const customerEmail = order.email || (order.customer && order.customer.email) || '无邮箱信息';
const country = order.shipping_address
? order.shipping_address.country
: (order.billing_address ? order.billing_address.country : '未知国家');

// 🏷️ 折扣信息提取(同时兼容折扣码与自动折扣)
let discounts = [];
if (order.discount_codes && order.discount_codes.length > 0) {
order.discount_codes.forEach(d => {
const amountText = d.amount ? ` (-${d.amount} ${currency})` : '';
discounts.push(`\`码: ${d.code}\`${amountText}`);
});
}
if (order.discount_applications && order.discount_applications.length > 0) {
order.discount_applications.forEach(app => {
if (app.type === 'discount_code') {
const alreadyAdded = order.discount_codes && order.discount_codes.some(d => d.code === app.code);
if (alreadyAdded) return;
}
const discountTitle = app.title || app.code || '自动折扣';
let valueText = '';
if (app.value_type === 'percentage') {
valueText = ` (-${app.value}%)`;
} else if (app.value_type === 'fixed_amount') {
valueText = ` (-${app.value} ${currency})`;
}
discounts.push(`\`自动: ${discountTitle}\`${valueText}`);
});
}
const discountText = discounts.length > 0 ? discounts.join(', ') : '未使用折扣';

// 🚚 运费信息提取
let shippingText = '免运费';
if (order.shipping_lines && order.shipping_lines.length > 0) {
shippingText = order.shipping_lines.map(s => {
const price = parseFloat(s.price);
return price > 0 ? `${s.title} (${s.price} ${currency})` : `免运费 (${s.title})`;
}).join(', ');
}

// 📦 商品清单提取
let lineItemsText = '';
if (order.line_items && order.line_items.length > 0) {
lineItemsText = order.line_items.map(item => {
const skuText = item.sku ? `\`SKU: ${item.sku}\`` : '*无 SKU*';
const itemPrice = item.price || '0.00';
return `• **${item.title}** x ${item.quantity} _(${itemPrice} ${currency}/件)_\n ┗ ${skuText}`;
}).join('\n');
} else {
lineItemsText = '无商品数据';
}

// 4. 组装飞书交互式卡片 Payload
const feishuPayload = {
msg_type: "interactive",
card: {
config: { wide_screen_mode: true },
header: {
title: { tag: "plain_text", content: `[${storeName}] 🎉 新订单 ${orderName} (${totalPrice} ${currency})` },
template: "indigo"
},
elements: [
{
tag: "column_set",
background_style: "grey",
columns: [
{
tag: "column",
width: "weighted",
weight: 1,
elements: [
{
tag: "div",
fields: [
{ is_short: true, text: { tag: "lark_md", content: `**来源站点:**\n${storeName}` } },
{ is_short: true, text: { tag: "lark_md", content: `**订单总额:**\n${totalPrice} ${currency}` } },
{ is_short: true, text: { tag: "lark_md", content: `**购买客户:**\n${customerName}` } },
{ is_short: true, text: { tag: "lark_md", content: `**购买频次:**\n${orderCountText}` } },
{ is_short: true, text: { tag: "lark_md", content: `**目的国家:**\n${country}` } },
{ is_short: true, text: { tag: "lark_md", content: `**客户邮箱:**\n${customerEmail}` } },
{ is_short: true, text: { tag: "lark_md", content: `**折扣优惠:**\n${discountText}` } },
{ is_short: true, text: { tag: "lark_md", content: `**配送运费:**\n${shippingText}` } }
]
}
]
}
]
},
{ tag: "hr" },
{ tag: "div", text: { tag: "lark_md", content: `**📦 商品清单:**\n${lineItemsText}` } },
...(order.note ? [{ tag: "hr" }, { tag: "note", elements: [{ tag: "plain_text", content: `💬 客户备注: ${order.note}` }] }] : [])
]
}
};

// 5. 获取目标飞书 Webhook 地址并发送
const targetWebhookUrl = currentStore.feishu_webhook || env.FEISHU_WEBHOOK_URL;
if (!targetWebhookUrl) {
console.error('未配置飞书 Webhook 链接');
return new Response('Configuration Error', { status: 500 });
}

const feishuResponse = await fetch(targetWebhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(feishuPayload)
});

if (!feishuResponse.ok) {
console.error('Feishu Push Failed:', await feishuResponse.text());
}

return new Response('OK', { status: 200 });

} catch (error) {
console.error('Logic Error:', error);
return new Response('Bad Request', { status: 400 });
}
}
};


第四步:在 Shopify 后台配置 Webhook

对于你需要监听订单的每一个 Shopify 站点

  1. 登录该站点的 Shopify 后台,进入 Settings(设置) -> Notifications(通知)
  2. 页面拉至最下方,找到 Webhooks 模块,点击 Create webhook
  • Event(事件):选择 Order creation(订单创建)。
  • Format(格式):选择 JSON
  • URL:填入你在第二步获得的 Cloudflare Worker 部署域名(例:https://shopify-feishu-notifier.yourname.workers.dev)。
  • Webhook API version:选择最新的稳定版。
  1. 点击保存后,复制该页面最底部显示的 Secret Key(“Your webhooks will be signed with…” 后的字符串)。
  2. 确保将该 Key 和对应的 .myshopify.com 域名填入第一步创建的 Cloudflare KV 中。

🛠️ 关键技术解密

1. 为什么不用自定义域名作为 KV Key?

Shopify Webhook 发送请求时,会在 HTTP Header 中自动带上 X-Shopify-Shop-Domain 标头,其值固定为店铺初始的 xxxx.myshopify.com 格式。使用此标头可以精准检索 KV 中的对应密钥,避免绑定自定义顶级域名变更带来的配置失效。

2. 签名安全校验 (HMAC SHA-256)

为防止恶意伪造订单请求,代码通过 Web Crypto API 提取原始 Body 进行签名运算:

  • 防止 Stack Overflow:避免直接使用 String.fromCharCode(...array) 处理大型 Payload 时引发的 JavaScript 栈溢出。
  • 防止 Timing Attack:编写 safeCompare 函数进行恒定时间比对,提升校验安全性。

💡 总结

通过 Cloudflare Workers 的边缘计算能力配合 KV 存储,我们仅用少量代码就构建了一套高效、安全且低成本的多站点订单推送通知系统。不仅能实时把控各站点的销售状态,精美的卡片排版也大大提升了团队协同的效率!