“优惠码已删除”提示
何时使用此工作流
当折扣码被移除且您需要立即记录受影响的内容时,请使用此工作流。它可帮助您记录被移除的折扣码、其使用次数、活动详情,并生成一份摘要,您可将其通过 Slack 发送给财务或运营部门。
这在以下情况下特别有用:网红优惠码泄露、优惠券意外曝光,或者在优惠码被撤下后需要一份有据可查的影响报告的任何情况。
你将构建的内容
- 一个 Slack 工作流,它从 webhook 接收一个名为
text的变量,并将该文本发布到一个 Slack 频道中。 - 由**“折扣码已删除**”触发的Shopify Flow工作流。
- 一个**“获取折扣”数据**步骤,该步骤使用触发器中的 ID 查询折扣。
- 一个用于生成移除报告的**“运行代码**”步骤。
- 一个将报告发送至 Slack 的 HTTP 请求步骤。
开始之前
- 请确保该应用已安装,并且在 Shopify 管理员界面中的**“应用”>“Workflow Trigger Extensions**”下可见。
- 请确保您可以访问 Shopify Flow。
- 创建或选择一个用于显示提醒的 Slack 频道。
- 创建一个以 webhook 为触发条件的 Slack 工作流,并支持接收
text字段。
视频指南
您还可以观看完整的实现视频。
首先创建 Slack 工作流
在 Slack 中,创建一个触发条件为“以……开头”的 Webhook 的工作流。添加一个名为 text 的文本变量,然后添加“发送消息”步骤,使用该 text 变量向您选择的频道发送消息。

在Workflow Trigger Extensions中授予折扣访问权限
在Shopify Flow中显示该触发器之前,请在应用中启用折扣访问权限。
在 Shopify 管理后台中,进入**“应用”>“Workflow Trigger Extensions**”。
在仪表盘上,查找设置检查清单。
启用“授予折扣访问权限”。这将启用与折扣相关的触发器,例如“折扣已创建”、“折扣已更新”、“折扣已删除”、“折扣码已添加”、“折扣码已移除”和**“折扣已过期**”。

构建Shopify Flow
打开 Shopify Flow 并创建一个新的工作流。
搜索与折扣相关的触发条件,并选择“Workflow Trigger Extensions”中的**“折扣代码已移除**”。

添加Shopify操作“获取折扣数据”。
在**“选择查询以筛选数据**”中,选择**“高级**”。在**“编辑查询**”中,使用触发器中的折扣 ID,并删除Shopify GID 前缀:
id:"{{discount.id|remove:'gid://shopify/DiscountCodeNode/'}}"
在**“获取折扣数据**”之后添加一个**“运行代码**”步骤。
在**“从前几个步骤中选择输入**”中,请使用以下 GraphQL 查询:
query {
redeemCode {
code
usageCount
bulkRemoval
}
getDiscountData {
discount {
... on DiscountCodeBasic {
title
startsAt
endsAt
asyncUsageCount
codes {
code
asyncUsageCount
}
customerGets {
value {
... on DiscountAmount {
amount { amount currencyCode }
}
... on DiscountPercentage {
percentage
}
}
}
}
}
}
}请按以下方式定义输出:
"Output of discount code removal report"
type Output {
"Discount code that was removed"
code: String!
"Discount campaign title"
title: String!
"Campaign start date"
startsAt: String!
"Campaign end date"
endsAt: String!
"Number of times it was redeemed"
usageCount: Int!
"Total discount impact as a formatted string"
totalImpact: String!
"When it was removed"
removedAt: String!
"Full summary for alerts"
summary: String!
}然后将以下代码粘贴到**“编写代码**”区域中:
export default function main(input) {
// ── Trigger: removed code ──
const removedCode = input?.redeemCode?.code ?? "Unknown";
const usageCount = input?.redeemCode?.usageCount ?? null; // null = bulk removal or unknown
const bulkRemoval = input?.redeemCode?.bulkRemoval ?? false;
const currency = input?.shop?.currencyCode ?? "";
const shopName = input?.shop?.name ?? "(not bound)";
const removedAt = new Date().toISOString();
// ── Find matching discount ──
const dataItems = input?.getDiscountData ?? [];
let matchedDiscount = null;
for (const item of dataItems) {
const d = item?.discount?.DiscountCodeBasic ?? item?.discount;
if (!d) continue;
const codesArray = Array.isArray(d.codes)
? d.codes.map(c => (typeof c === "string" ? c : c?.code ?? ""))
: [];
if (codesArray.includes(removedCode)) {
matchedDiscount = d;
break;
}
}
// fallback to first entry if removed code already gone from list
if (!matchedDiscount && dataItems.length > 0) {
matchedDiscount =
dataItems[0]?.discount?.DiscountCodeBasic ?? dataItems[0]?.discount ?? null;
}
const title = matchedDiscount?.title ?? "Untitled";
const startsAt = matchedDiscount?.startsAt ?? "N/A";
const endsAt = matchedDiscount?.endsAt ?? "None (no expiry)";
const totalUsageCount = matchedDiscount?.asyncUsageCount ?? null;
// ── Discount value ──
const valueWrapper = matchedDiscount?.customerGets?.value ?? {};
const pctObj = valueWrapper?.DiscountPercentage ?? null;
const amtObj = valueWrapper?.DiscountAmount ?? valueWrapper?.amount ?? null;
let discountLabel = "Unknown";
if (pctObj?.percentage != null) {
discountLabel = `${(pctObj.percentage * 100).toFixed(0)}% off`;
} else if (amtObj?.amount != null) {
const cur = amtObj.currencyCode || currency;
discountLabel = `${cur} ${parseFloat(amtObj.amount).toFixed(2)} off`;
}
// ── This code's impact ──
let thisCodeImpact;
if (bulkRemoval) {
thisCodeImpact = `Not available - removed as part of bulk deletion`;
} else if (usageCount === null) {
thisCodeImpact = `Could not be determined`;
} else if (pctObj?.percentage != null) {
thisCodeImpact = `${discountLabel} × ${usageCount} use(s)`;
} else if (amtObj?.amount != null) {
const perUse = parseFloat(amtObj.amount);
const cur = amtObj.currencyCode || currency;
thisCodeImpact = `${cur} ${(perUse * usageCount).toFixed(2)} (${cur} ${perUse.toFixed(2)} × ${usageCount} uses)`;
} else {
thisCodeImpact = "Unknown";
}
// ── Campaign-wide impact using asyncUsageCount on the discount ──
let campaignImpact;
if (totalUsageCount === null) {
campaignImpact = "Not available";
} else if (pctObj?.percentage != null) {
campaignImpact = `${discountLabel} × ${totalUsageCount} total use(s) across all codes`;
} else if (amtObj?.amount != null) {
const perUse = parseFloat(amtObj.amount);
const cur = amtObj.currencyCode || currency;
campaignImpact = `${cur} ${(perUse * totalUsageCount).toFixed(2)} total (${cur} ${perUse.toFixed(2)} × ${totalUsageCount} uses)`;
} else {
campaignImpact = "Unknown";
}
// ── Per-code breakdown ──
const codesNodes = Array.isArray(matchedDiscount?.codes)
? matchedDiscount.codes
: [];
const allCodes = codesNodes
.map(c => (typeof c === "string" ? c : c?.code ?? ""))
.filter(Boolean);
const codesRoster = codesNodes.length
? codesNodes.map(c => {
const code = typeof c === "string" ? c : c?.code ?? "";
const used = c?.asyncUsageCount ?? "?";
const flag = code === removedCode ? " ← REMOVED" : "";
return `${code} (used ${used}×)${flag}`;
}).join("\n ")
: "N/A";
const usageCountLabel = bulkRemoval
? "N/A (bulk removal)"
: usageCount !== null
? String(usageCount)
: "Unknown";
const summary =
`DISCOUNT CODE REMOVED\n` +
`─────────────────────\n` +
`Store: ${shopName}\n` +
`Code removed: ${removedCode}\n` +
`Bulk removal: ${bulkRemoval ? "Yes" : "No"}\n` +
`Campaign: ${title}\n` +
`Discount: ${discountLabel}\n` +
`Valid from: ${startsAt}\n` +
`Valid until: ${endsAt}\n` +
`\n` +
`THIS CODE\n` +
` Times used: ${usageCountLabel}\n` +
` Impact: ${thisCodeImpact}\n` +
`\n` +
`CAMPAIGN TOTAL\n` +
` Total uses: ${totalUsageCount ?? "Unknown"}\n` +
` Total impact: ${campaignImpact}\n` +
`\n` +
`CODE BREAKDOWN\n` +
` ${codesRoster}\n` +
`\n` +
`Removed at: ${removedAt}`;
const safeSummary = summary
.replace(/\\/g, "\\\\") // escape backslashes first
.replace(/"/g, '\\"') // escape quotes
.replace(/\n/g, "\\n") // escape newlines
.replace(/\r/g, "\\r");
return {
code: removedCode,
bulkRemoval,
title,
discountLabel,
startsAt,
endsAt,
usageCount: usageCount ?? 0,
usageCountLabel,
thisCodeImpact,
totalUsageCount: totalUsageCount ?? 0,
campaignImpact,
allCodes,
codesRoster,
removedAt,
summary: safeSummary,
};
}
添加**“HTTP 请求**”操作。在**“变量”或“正文覆盖(JSON)”字段中,发送“运行代码**”步骤返回的摘要:
{
"body": {
"text": "{{runCode.summary}}"
}
}这会使用text变量,将生成的摘要发布到Slack webhook工作流中。

完成所有步骤后,请保存该工作流。
要进行测试,请从您的折扣活动中共删除一个折扣码,并确认 Slack 频道是否收到了汇总消息。

Slack 通知包含哪些内容
- 已删除的折扣码
- 无论是否是批量移除
- 活动标题
- 折扣金额
- 开始和结束日期
- 已删除代码所用的时间
- 已删除代码的预计影响
- 整个活动范围内的使用情况和影响
- 如有相关数据,将按代码逐一细分
- 处理删除操作时的时间戳
故障排除
返回“应用”>“Workflow Trigger Extensions”,确认“授予折扣访问权限”已启用。只有在授予访问权限后,折扣触发条件才会显示。
请在**“编辑查询**”中仔细核对高级查询。ID 应完全按照如下所示进行清理:
id:"{{discount.id|remove:'gid://shopify/DiscountCodeNode/'}}"请确保 Slack 工作流由一个 Webhook 触发,该 Webhook 的 URL 与您的 HTTP 请求中使用的 URL 一致,且请求正文中包含 text 字段。
某些删除操作可能是批量操作的一部分,或者可能无法返回完整的使用详情。在这些情况下,摘要仍会发送现有的最佳信息,并明确标注缺失的值。
最佳实践
- 请将这些提醒发送至专门的财务或运营渠道,而非一般支持渠道。
- 请为工作流命名时尽量具体,例如“折扣码已移除”,这样在 Shopify Flow 和 Slack 中就能轻松识别。
- 请先使用非生产环境的折扣码进行测试,以确认消息格式和 webhook 的行为。

