开发技巧

Stripe Payment Links:活动收费报名与 CSV 导出

不写网站,用 Stripe Payment Links 做一个线下活动的收费报名:后台生成一个付款链接和二维码,报名表里收集姓名、手机号和场次,报名截止后用一个 Node.js 脚本把所有成功报名导出成 Excel 能直接打开的 CSV 名单。

BINBHO · 图文教程

不写网站,用 Stripe Payment Links 做一个线下活动的收费报名:后台生成一个付款链接和二维码,报名表里收集姓名、手机号和场次,报名截止后用一个 Node.js 脚本把所有成功报名导出成 Excel 能直接打开的 CSV 名单。

先看清成品

成品是一个付款链接(可生成二维码贴在海报上)加一个导出脚本。参与者扫码、填写信息、付款;你随时运行 npm run export 得到 signups-日期.csv,列为时间、姓名、手机号、场次、邮箱、金额、币种、付款状态、订单号。本文脚本已实际运行 3 个测试全部通过:自定义字段正确映射到中文列,CSV 转义正确,并通过官方模拟服务 stripe-mock 走通了分页导出。在 Stripe 后台创建链接的操作没有在本文环境中进行。

注意:Stripe 账号必须用你自己公司或个人的真实资料,在 Stripe 支持的国家或地区注册,不要借用或租用他人账号收款。销售前确认你所在地区对线上销售、发票和税务的要求。

注意:手机号、姓名属于个人信息。报名页面要写明收集目的,名单只用于本次活动,不要外传,活动结束后按你承诺的期限删除。

做完之后你会得到什么?

成果 说明
付款链接 一个网址 + 二维码,手机电脑都能付
报名表 最多 3 个自定义字段
名额控制 可设置付款次数上限
导出脚本 自动翻页,输出带 BOM 的 CSV
受限密钥 脚本只拿到读取权限

需要准备哪些环境?

项目 要求 说明
Stripe 账号 在 Stripe 支持的国家或地区注册 先在测试模式完成全部流程
Node.js 20.6 或更高 只有导出脚本需要
stripe(Node SDK) 23.x 本文使用 23.0.0

第一步:怎样创建付款链接?

1. 创建活动商品

测试模式下进入 商品目录 → 添加产品,名称例如“周末烘焙体验课”,价格选一次性。

2. 创建 Payment Link

进入 付款链接 → 新建,选择刚才的商品,然后设置:

设置 建议
自定义字段 1 文本,键 name,标签“姓名”
自定义字段 2 数字,键 phone,标签“手机号”
自定义字段 3 下拉,键 session,选项如 sat_am(周六上午)、sat_pm(周六下午)
限制付款次数 按名额设置,满额后链接自动停止收款
付款后 显示确认信息,写上活动时间、地点和联系方式

字段的“键”要和脚本里的 name、phone、session 一致。

3. 拿到链接和二维码

保存后复制链接,详情页可以下载二维码,贴到海报或群公告里。同时记下链接 ID(plink_ 开头)。

4. 检查结果

用手机扫码,确认页面显示活动名称、价格和三个字段;用测试卡 4242 4242 4242 4242 付一次款,后台 付款 列表里出现这笔记录。

第二步:怎样创建只读的受限密钥?

导出名单只需要读取数据,不要用权限最大的私钥。

进入 开发者 → API 密钥 → 创建受限密钥,只给 Checkout Sessions 读取权限,其余保持“无”。受限密钥以 rk_ 开头。

注意:即使是受限密钥也不要发给别人或贴在群里。不再需要时在后台删除。

第三步:导出脚本怎么写?

1. 项目结构

event-signup/
├── export.js
├── export.test.js
├── package.json
└── .env

2. package.json 和 .env.example

{
  "name": "event-signup-export",
  "private": true,
  "type": "commonjs",
  "scripts": {
    "export": "node --env-file=.env export.js",
    "test": "node --test"
  },
  "dependencies": {
    "stripe": "^23.0.0"
  }
}
# 建议新建一个“受限密钥”,只给 Checkout Sessions 读取权限
STRIPE_SECRET_KEY=rk_test_替换成受限密钥
PAYMENT_LINK_ID=plink_替换成支付链接ID

3. export.js

// 把某个 Payment Link 的全部成功报名导出为 CSV(Excel 可直接打开)
const fs = require("node:fs");
const Stripe = require("stripe");

const csvCell = (v) => {
  const s = v == null ? "" : String(v);
  return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
};

function toRow(session) {
  const fields = Object.fromEntries((session.custom_fields ?? []).map((f) => [f.key, f.text?.value ?? f.dropdown?.value ?? f.numeric?.value ?? ""]));
  return {
    时间: new Date(session.created * 1000).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
    姓名: fields.name ?? session.customer_details?.name ?? "",
    手机号: fields.phone ?? "",
    场次: fields.session ?? "",
    邮箱: session.customer_details?.email ?? "",
    金额: session.amount_total != null ? (session.amount_total / 100).toFixed(2) : "",
    币种: (session.currency ?? "").toUpperCase(),
    付款状态: session.payment_status,
    订单号: session.id,
  };
}

async function exportSignups(stripe, paymentLink, out) {
  const rows = [];
  // 自动翻页,取出该链接下所有已完成的 Checkout Session
  for await (const s of stripe.checkout.sessions.list({ payment_link: paymentLink, status: "complete", limit: 100 })) {
    if (s.payment_status === "paid" || s.payment_status === "no_payment_required") rows.push(toRow(s));
  }
  const header = Object.keys(toRow({ created: 0 }));
  const lines = [header, ...rows.map((r) => header.map((h) => r[h]))].map((r) => r.map(csvCell).join(","));
  fs.writeFileSync(out, "\uFEFF" + lines.join("\n")); // 加 BOM,Excel 打开中文不乱码
  return rows.length;
}

if (require.main === module) {
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
  const out = `signups-${new Date().toISOString().slice(0, 10)}.csv`;
  exportSignups(stripe, process.env.PAYMENT_LINK_ID, out).then((n) => console.log(`导出 ${n} 条报名到 ${out}`));
}
module.exports = { exportSignups, toRow, csvCell };
要点 说明
payment_link + status: "complete" 只取这个链接下已完成的报名
for await SDK 自动翻页,报名超过 100 条也能全部取出
只保留 paid 排除尚未到账的延迟付款
\uFEFF 文件开头加 BOM,Excel 打开中文不乱码
Asia/Shanghai 付款时间按北京时间显示,可按需修改

4. 运行

npm install
cp .env.example .env   # 填入受限密钥和链接 ID
npm run export

期望输出:

导出 N 条报名到 signups-2026-10-21.csv

N 是你的实际报名人数,日期为运行当天。

5. 检查结果

检查项 通过标准
行数 和后台该链接的成功付款数一致
中文 Excel 打开不乱码
字段 姓名、手机号、场次都有值

第四步:怎样验证脚本逻辑?

1. export.test.js

const { test } = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const Stripe = require("stripe");
const { exportSignups, toRow, csvCell } = require("./export");

test("自定义字段映射到中文列", () => {
  const r = toRow({
    id: "cs_1", created: 1792540800, amount_total: 9900, currency: "cny", payment_status: "paid",
    customer_details: { email: "a@example.com", name: "Card Name" },
    custom_fields: [
      { key: "name", type: "text", text: { value: "张三" } },
      { key: "phone", type: "numeric", numeric: { value: "13800000000" } },
      { key: "session", type: "dropdown", dropdown: { value: "sat_am" } },
    ],
  });
  assert.equal(r.姓名, "张三");
  assert.equal(r.手机号, "13800000000");
  assert.equal(r.场次, "sat_am");
  assert.equal(r.金额, "99.00");
  assert.equal(r.币种, "CNY");
});

test("CSV 单元格转义逗号和引号", () => {
  assert.equal(csvCell('a,"b"'), '"a,""b"""');
});

test("用 stripe-mock 走一遍分页导出", { skip: !process.env.STRIPE_MOCK && "需要 stripe-mock" }, async () => {
  const stripe = new Stripe("sk_test_123", { host: "localhost", port: 12111, protocol: "http" });
  const out = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "exp-")), "a.csv");
  const n = await exportSignups(stripe, "plink_123", out);
  const text = fs.readFileSync(out, "utf8");
  assert.ok(text.startsWith("\uFEFF时间,姓名"));
  assert.equal(text.trim().split("\n").length, n + 1);
});

2. 运行

docker run -d --rm -p 12111:12111 stripe/stripe-mock:latest
STRIPE_MOCK=1 npm test

本文实际输出:

ok 1 - 自定义字段映射到中文列
ok 2 - CSV 单元格转义逗号和引号
ok 3 - 用 stripe-mock 走一遍分页导出
# pass 3
# fail 0

stripe-mock 返回的样例数据是未付款状态,所以第 3 个测试只验证请求参数和文件格式,不验证行数。

出问题时该怎么调?

1. 导出 0 条

原因 检查
密钥和链接不在同一模式 测试密钥只能读测试数据
链接 ID 错了 后台链接详情页复制 plink_
付款尚未完成 后台确认付款状态为成功

2. 报 permission 错误

The provided key 'rk_test_...' does not have the required permissions for this endpoint

受限密钥没给 Checkout Sessions 读取权限,回后台编辑密钥。

3. 某一列为空

脚本按字段的“键”取值。后台字段键写成了别的名字时,修改 toRow 里的 fields.name、fields.phone、fields.session。

4. 需要退款

在后台 付款 里找到对应订单号,点击退款。导出的“订单号”列就是为了方便你查找。

常见问题怎么处理?

现象 原因 解决方法
Excel 打开乱码 文件没有 BOM 使用本文脚本,或用“数据 → 从文本导入”
手机号前面的 0 丢失 Excel 当成数字 导入时把该列设为文本
名额满了还想加人 达到付款次数上限 在链接设置里调高上限
想停止报名 活动已截止 在后台停用该付款链接
场次显示英文键 下拉选项存的是键 在脚本里加一个键到中文的对照表
Cannot find module 'stripe' 没安装依赖 npm install

项目资源

用途 地址
Payment Links 文档 https://docs.stripe.com/payment-links
自定义字段 https://docs.stripe.com/payments/checkout/custom-fields
受限 API 密钥 https://docs.stripe.com/keys/restricted-api-keys
Checkout Session 列表接口 https://docs.stripe.com/api/checkout/sessions/list
自动翻页 https://docs.stripe.com/api/pagination/auto
测试卡号 https://docs.stripe.com/testing
stripe-mock https://github.com/stripe/stripe-mock
支持的国家和地区 https://stripe.com/global

建议先在测试模式创建一个付款链接,自己扫码付两次款,再运行导出脚本,确认 CSV 里有这两条记录。

流程没问题后再切换到正式模式,重新创建链接和受限密钥,然后才发布海报。