动态优惠码

通过专属链接选择 Stripe Coupon,在首页展示对应优惠,并在创建 Checkout 时自动应用。

Saavo 内置了链接型动态优惠功能。用户打开 /offer/:id 后,服务器会记住这次优惠选择,首页随即展示对应的价格和促销文案;用户购买指定套餐时,系统会自动把预先配置的 Stripe Coupon 带入 Checkout。

当前 config/products.ts 没有配置任何动态优惠,因此功能入口虽然已经存在,但默认不会产生优惠。要使用这项功能,需要先在 Stripe 创建 Coupon,再把 Coupon ID 和公开活动 ID 配置到具体套餐的 couponOffers 中。

这里的“优惠码”不需要用户输入

动态优惠通过专属链接选择,用户不会在网站上手动输入代码。链接中的 id 是网站公开的活动标识;真正提交给 Stripe 的是服务器配置中的 coupon。

工作流程

一次完整流程如下:

用户打开 /offer/autumn-20-off
→ 服务器确认活动存在且尚未过期
→ 写入 30 分钟有效的 HttpOnly Cookie
→ 跳转到当前语言的首页
→ 首页显示该套餐的活动价格和文案
→ 用户创建 Checkout
→ 系统向 Stripe 提交活动对应的 Coupon ID
→ Checkout 创建成功后清除优惠 Cookie

项目默认支持以下功能:

功能当前行为
活动入口GET /offer/:id,支持语言前缀
选择记录saavo_coupon_offer Cookie,有效期 30 分钟
首页展示可覆盖指定套餐的价格、标签、卖点和购买区文案
Checkout自动应用匹配活动的 Stripe Coupon
到期控制可通过 expiresAt 设置活动截止时间
无效活动清除旧选择并跳转首页,不显示错误页

响应会设置 Cache-Control: private, no-store,避免带有个人优惠选择的页面被共享缓存。

在 Stripe 中创建优惠券

先在 Stripe Dashboard 创建 Coupon,并确定折扣比例或金额、币种、适用产品、持续时间和兑换限制。保存后记下 Coupon ID,例如 coupon_launch_20_off。

这里需要的是 Stripe Coupon ID,不是给用户输入的 Promotion Code。项目只负责把已有 Coupon 应用到 Checkout,不会通过配置自动创建、更新或停用 Stripe Coupon。

网站的活动截止时间与 Stripe Coupon 的有效规则相互独立。即使 expiresAt 已到期,Stripe 中的 Coupon 仍可能继续有效;反过来,Stripe Coupon 已失效但网站活动仍有效时,Checkout 创建会失败。两边必须分别配置并保持一致。

配置动态优惠活动

在 config/products.ts 中找到需要参与活动的套餐,为它增加 couponOffers:

license: {
    type: 'recurring',
    interval: 'month',
    intervalCount: 1,
    priceId: 'price_hobby_monthly_replace_me',
    salePrice: '$19',
    // 其余套餐配置省略

    couponOffers: [
        {
            id: 'autumn-20-off',
            coupon: 'coupon_launch_20_off',
            expiresAt: Date.parse('2026-10-01T00:00:00Z'),
            salePrice: '$15.20',
            listPrice: '$19',
            discountLabel: { value: '限时 8 折' },
            badgeLabel: { value: '秋季优惠' },
            valueHint: { value: '活动截止至 2026 年 9 月 30 日' },
        },
    ],
},

couponOffers 可以配置多项活动,但每个活动只能属于一个套餐。

必填配置

字段说明
id公开活动 ID,用在 /offer/:id 链接中
coupon创建 Checkout 时提交的 Stripe Coupon ID

id 在整个产品目录中必须唯一,最长 64 个字符,只能使用小写字母、数字和单个连字符,例如 autumn-20-off。

同一套餐中的每个动态活动必须使用不同的 Coupon,并且不能与套餐默认的 coupon 相同。配置不符合这些要求时,项目会在启动或构建阶段报错。

可选配置

expiresAt 是毫秒级 Unix 时间戳,表示活动的排他截止时间:当前时间达到该值时,活动立即失效。不配置则长期有效。

活动还可以覆盖以下展示字段:

  • title、description、features;
  • salePrice、listPrice、discountLabel、billingLabel;
  • savingsLabel、badgeLabel、valueHint;
  • presentation 中的购买标题、说明、事实列表、按钮、规格和亮点。

这些字段只改变页面展示,不会修改 Stripe Price、实际折扣或用户最终获得的权限。展示金额必须由你根据 Coupon 规则计算并填写,项目不会自动用折扣比例换算 salePrice。

为了避免活动链接改变产品结构,动态优惠不能覆盖以下内容:

  • priceId、套餐类型和计费周期;
  • 角色、权益和试用期;
  • 是否推荐、是否默认、是否允许重复购买;
  • 套餐启用状态或嵌套的 couponOffers。

完成配置并部署后,可以直接投放活动链接:

https://example.com/offer/autumn-20-off

指定语言时使用相应前缀:

https://example.com/zh-Hans/offer/autumn-20-off

访问成功后,用户会被重定向到相同语言的首页。活动 ID 不存在、已过期或格式错误时,系统同样跳转首页,同时清除浏览器中原有的动态优惠选择。

当前配置只有截止时间,没有开始时间。如果活动需要定时开始,应在开始时间再发布链接或部署对应配置,不能通过尚未提供的 startsAt 字段提前安排。

优惠选择何时失效

saavo_coupon_offer Cookie 具有以下属性:

  • HttpOnly:前端脚本无法读取或修改;
  • SameSite=Lax:允许用户从外部营销链接进入网站;
  • Secure:生产环境仅通过 HTTPS 发送;
  • Path=/:首页和 Checkout 都能读取;
  • Max-Age=1800:选择保存 30 分钟。

服务器每次使用时都会重新核对活动是否仍然存在且未过期,不能通过手工伪造 Cookie 使用未配置的 Coupon。

不同情况的处理方式如下:

情况使用的优惠Cookie 处理
活动与当前套餐匹配动态活动的 couponCheckout 创建成功后清除
没有活动选择套餐默认 coupon,没有则不自动应用不写入
活动不存在或已过期回退到套餐默认配置立即清除
活动属于另一个套餐当前套餐使用默认配置暂时保留
Stripe 拒绝 Coupon,Checkout 创建失败未创建 Checkout保留,便于修正后重试

优惠是在 Checkout Session 创建成功后被消费,而不是付款成功后才消费。用户进入 Stripe 页面后放弃付款,网站中的优惠 Cookie 也已经清除,但已创建的 Checkout Session 仍保留该 Coupon。

与其他优惠方式的区别

套餐本身可以设置固定 coupon,表示每次购买该套餐都自动使用同一个 Stripe Coupon。动态活动匹配时,会用活动的 coupon 覆盖这个默认值。

config/payment.ts 中的 stripe.enablePromoCodes 控制 Stripe Checkout 是否允许用户手动输入 Promotion Code。只要当前 Checkout 已经带有固定 Coupon——无论来自套餐默认值还是动态活动——项目就不会同时开启 Promotion Code 输入框,因为 Stripe 不允许两种方式同时提交。

选择优惠方式时可按以下原则处理:

  • 面向所有购买者的长期折扣:使用套餐级 coupon;
  • 通过广告、邮件或合作渠道投放专属价格:使用 couponOffers;
  • 让用户自行输入公开或私发代码:开启 enablePromoCodes,并且不要为该 Checkout 预设 Coupon。

当前限制

动态优惠目前只接入首页产品展示和常规支付 Checkout。OAuth2 授权流程中的升级 Checkout 仍读取套餐默认 Coupon,不会使用 /offer/:id 选择的动态活动。

首页的 JSON-LD 商品结构化数据也仍使用默认套餐价格,不包含针对单次请求的动态展示价格。

项目也没有内置以下能力:

  • 自动创建或停用 Stripe Coupon;
  • 活动开始时间、领取次数或每位用户的使用次数限制;
  • 动态优惠活动管理后台;
  • 按活动 ID 统计访问、Checkout 和成交转化。

Stripe 回调同步的交易记录会保存实际折扣金额,但不会把动态活动 ID 作为独立营销归因字段。如果需要活动级报表,应另外设计归因与统计方案,不能只依赖 saavo_coupon_offer Cookie。

该 Cookie 在用户主动打开优惠链接时写入,不受当前 Cookie 同意组件的分类开关控制。上线前应根据实际用途更新 Cookie 政策,并在Cookie 同意管理中核对说明。

上线检查

  • Stripe 中的 Coupon 已创建,ID 与 couponOffers[].coupon 完全一致。
  • Coupon 的折扣、币种、适用产品、持续时间和限制符合套餐规则。
  • 活动 id 全局唯一,并符合小写字母、数字和连字符格式。
  • expiresAt 使用毫秒时间戳,时区和截止时刻已经核对。
  • 页面展示价格与 Stripe Checkout 的实际金额一致。
  • 活动链接会跳回正确语言的首页,并显示预期文案。
  • 购买正确套餐时自动应用活动 Coupon,购买其他套餐时不会误用。
  • Checkout 创建失败时不会清除优惠选择,创建成功后会清除。
  • 固定 Coupon 与 enablePromoCodes 的组合符合产品预期。
  • Cookie 政策已说明 saavo_coupon_offer 的用途和有效期。

常见问题

接下来