“优惠码已删除”提示

何时使用此工作流

当折扣码被移除且您需要立即记录受影响的内容时,请使用此工作流。它可帮助您记录被移除的折扣码、其使用次数、活动详情,并生成一份摘要,您可将其通过 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 变量向您选择的频道发送消息。

一个由 webhook 触发并将文本变量发送至 Slack 频道的 Slack 工作流
一个由 webhook 触发并将文本变量发送至 Slack 频道的 Slack 工作流

在Workflow Trigger Extensions中授予折扣访问权限

在Shopify Flow中显示该触发器之前,请在应用中启用折扣访问权限。

在 Shopify 管理后台中,进入**“应用”>“Workflow Trigger Extensions**”。

仪表盘上,查找设置检查清单。

启用“授予折扣访问权限”。这将启用与折扣相关的触发器,例如“折扣已创建”、“折扣已更新”、“折扣已删除”“折扣码已添加”、“折扣码已移除”和**“折扣已过期**”。

Workflow Trigger Extensions 仪表盘显示“授予折扣访问权限”已启用
Workflow Trigger Extensions 仪表盘显示“授予折扣访问权限”已启用

构建Shopify Flow

打开 Shopify Flow 并创建一个新的工作流。

搜索与折扣相关的触发条件,并选择“Workflow Trigger Extensions”中的**“折扣代码已移除**”。

Shopify Flow 触发器选择器:已移除折扣码,已取消选中
Shopify Flow 触发器选择器:已移除折扣码(已选中)

添加Shopify操作“获取折扣数据”。

在**“选择查询以筛选数据**”中,选择**“高级**”。在**“编辑查询**”中,使用触发器中的折扣 ID,并删除Shopify GID 前缀:

bash
id:"{{discount.id|remove:'gid://shopify/DiscountCodeNode/'}}"
使用经过清理的折扣 ID 进行高级查询,获取折扣数据操作
使用经过清理的折扣 ID 进行高级查询,获取折扣数据操作

在**“获取折扣数据**”之后添加一个**“运行代码**”步骤。

在**“从前几个步骤中选择输入**”中,请使用以下 GraphQL 查询:

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
            }
          }
        }
      }
    }
  }
}

请按以下方式定义输出:

typescript
"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!
}

然后将以下代码粘贴到**“编写代码**”区域中:

javascript
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,
  };
}
在Shopify Flow中运行代码步骤,其中包含查询、输出结果和JavaScript报表生成器
在Shopify Flow中运行代码步骤,其中包含查询、输出结果和JavaScript报表生成器

添加**“HTTP 请求**”操作。在**“变量”或“正文覆盖(JSON)”字段中,发送“运行代码**”步骤返回的摘要:

css
{
  "body": {
    "text": "{{runCode.summary}}"
  }
}

这会使用text变量,将生成的摘要发布到Slack webhook工作流中。

Shopify Flow 中的 HTTP 请求操作,将 runCode 摘要发送至文本字段
Shopify Flow 中的 HTTP 请求操作,用于将 runCode 摘要发送至文本字段

完成所有步骤后,请保存该工作流。

要进行测试,请从您的折扣活动中共删除一个折扣码,并确认 Slack 频道是否收到了汇总消息。

显示折扣码已被移除的 Slack 消息摘要
显示折扣码已被移除的 Slack 消息摘要

Slack 通知包含哪些内容

  • 已删除的折扣码
  • 无论是否是批量移除
  • 活动标题
  • 折扣金额
  • 开始和结束日期
  • 已删除代码所用的时间
  • 已删除代码的预计影响
  • 整个活动范围内的使用情况和影响
  • 如有相关数据,将按代码逐一细分
  • 处理删除操作时的时间戳

故障排除

返回“应用>“Workflow Trigger Extensions”,确认“授予折扣访问权限”已启用。只有在授予访问权限后,折扣触发条件才会显示。

请在**“编辑查询**”中仔细核对高级查询。ID 应完全按照如下所示进行清理:

bash
id:"{{discount.id|remove:'gid://shopify/DiscountCodeNode/'}}"

请确保 Slack 工作流由一个 Webhook 触发,该 Webhook 的 URL 与您的 HTTP 请求中使用的 URL 一致,且请求正文中包含 text 字段。

某些删除操作可能是批量操作的一部分,或者可能无法返回完整的使用详情。在这些情况下,摘要仍会发送现有的最佳信息,并明确标注缺失的值。

最佳实践

  • 请将这些提醒发送至专门的财务或运营渠道,而非一般支持渠道。
  • 请为工作流命名时尽量具体,例如“折扣码已移除”,这样在 Shopify Flow 和 Slack 中就能轻松识别。
  • 请先使用非生产环境的折扣码进行测试,以确认消息格式和 webhook 的行为。