第一次使用 Stripe:完全无服务器的票务销售
[点击此处查看本博客的日文版](https://dev.to/aws-builders/chu-metenostripe-wan-quan-nisabaresunotiketutofan-mai-2780)。 ]
此博客条目是Stripe Advent Calendar 2021的一部分,最初于 2021 年 12 月 3 日以日语发布。
大家好!我叫 Michael Tedder,是 Tokyo Demo Fest 的主要组织者之一。我已经使用 AWS 超过 9 年,并帮助在日本运行 JAWS-UG Sapporo AWS 用户组,并协助组织更大的 AWS 社区活动,例如JAWS DAYS 2021和JAWS PANKRATION 2021.自 2020 年以来,我也一直是 AWS 社区构建者。
在这篇文章中,我将解释我们如何使用 Stripe on AWS 为今年的 Tokyo Demo Fest 2021 实现在线售票。虽然这篇文章确实关注技术方面,但我将尝试涵盖所有必要的内容,就好像这是您第一次使用 Stripe。此处提供的所有示例代码都在 Node.js 14.x 中。
关于东京演示节
Tokyo Demo Fest(也称为“TDF”)是目前日本唯一活跃的演示派对。在演示派对上,对计算机编程和艺术感兴趣的人们聚集在一起——不仅来自日本,而且来自世界各地——并举行有关演示制作的比赛和研讨会。
对于过去的所有 TDF,我们之前一直使用 PayPal 进行门票销售,但今年我们终于改用了 Stripe。通过使用 Stripe Checkout,我们能够在短短几个小时内完成一个实施。
系统设计
下图显示了系统的整体设计。请注意,由于这篇文章仅涉及 Stripe 和门票销售,为了清楚起见,TDF 系统的其他部分(例如直播)已被省略。
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--fIPhTsSR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to -uploads.s3.amazonaws.com/uploads/articles/di5bdgrkerfx41kzi69h.png)
访问者首先访问通过 Amplify 发布的TDF 网站。在选择购买两种类型的门票中的哪一种后,访问者将被转发到 Stripe Checkout。付款完成后,Stripe 会调用我们的 webhook,然后我们通过电子邮件将 votekey 发送到结帐时输入的地址。访客然后使用 votekey 访问和登录运行在 ECS/Fargate 上的芜湖党系统。
在条纹中创建产品
今年的 TDF 有两种不同类型的门票可供购买。
- 参观票(1,000日元)
2.青铜支持者(10,000日元,包括T恤+运费)
由于访客票不需要什么特别的,它只是作为产品直接添加到 Stripe 仪表板中。
青铜支持者票的价格与访客票不同,因此需要在 Stripe 中注册不同的产品。但是,由于青铜支持者门票包括一件 T 恤(可选择 S/M/L/XL 尺码),我们还需要为每个 T 恤尺码添加单独的产品,从而总共增加了四个产品,并且所有的价格都为零(0日元),因为T恤包含在门票价格中。由于 T 恤是实物商品,需要发货,我们还将要求购买者在结帐时输入他们的地址。
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--5okryU2e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to -uploads.s3.amazonaws.com/uploads/articles/x84n72dkdyacy92hpz7l.png)
为了要求在结帐页面上显示地址字段,需要提供运费。由于运费包含在我们的青铜支持者票价中,因此这里的金额也设置为零(0 日元)。
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--eOSIBIA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to -uploads.s3.amazonaws.com/uploads/articles/ve3m61fk3cimm5851dhu.png)
条带 API 密钥安全
您的 Stripe API 密钥需要保密,确保您永远不会在源代码中使用它或不小心将其提交到源代码管理中,这一点很重要。在将机密与 Lambda 函数一起使用时,我通常使用 AWS Systems Manager 的Parameter Store功能来存储机密,我将在这里做同样的事情。
Stripe 有各种密钥和值——例如你的 API 密钥、Webhook 机密和价格 ID——都需要保密。您可以将这些中的每一个存储到单独的 SecureStrings 中,但由于 Standard 参数最多可以存储 4KB 的数据,因此将所有必要的键和值编码到 JSON 块中,并将 JSON 作为单个 SecureString 存储在参数存储。
我在下面隐藏了我们的键的值,但这是我们用来存储 Stripe 数据的 JSON:
{"stripe_api_secret_key":"sk_test_51JU2XXXXXXXXXXXX",
"webhook_signing_secret":"whsec_TqW4TXXXXXXXXXXXX",
"product_visitor_ticket":"price_1JX1zXXXXXXXXXXXX",
"product_bronze_ticket":"price_1JrKaXXXXXXXXXXXX",
"product_tshirt_s":"price_1JrKlXXXXXXXXXXXX",
"product_tshirt_m":"price_1JrKmXXXXXXXXXXXX",
"product_tshirt_l":"price_1JrKnXXXXXXXXXXXX",
"product_tshirt_xl":"price_1JrKoXXXXXXXXXXXX",
"success_url":"https://tokyodemofest.jp/success.html",
"cancel_url":"https://tokyodemofest.jp#registration",
"shipping_rate":"shr_1JrKNXXXXXXXXXXXX",
"shipping_countries":"US,JP,IE,GB,NO,SE,FI,RU,PT,ES,FR,DE,CH,IT,PL,CZ,AT,HU,BA,BY,UA,RO,BG,GR,AU,NZ,KR,TW,IS"}
进入全屏模式 退出全屏模式
为了从 Parameter Store 读取 JSON,我们将使用 Node.js Lambda 函数运行时附带的aws-sdk包。读取 JSON 配置后,我们会将 Stripe API 密钥传递给 Stripe API 初始化。
const loadConfig = async function() {
const aws = require('aws-sdk');
const ssm = new aws.SSM();
const res = await ssm.getParameter( { Name: '/tdf/config-stripe', WithDecryption: true } ).promise();
return JSON.parse(res.Parameter.Value);
}
exports.handler = async (event) => {
const config = await loadConfig();
const stripe = require('stripe')(config.stripe_api_secret_key);
// ...
}
进入全屏模式 退出全屏模式
添加 HTML 购买按钮
以下是我们用于今年 TDF 的购票按钮。
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--1KH3P2ox--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to -uploads.s3.amazonaws.com/uploads/articles/ijk8naj9ixj4uoxo2yxh.png)
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--Bb_2Nu9h--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to -uploads.s3.amazonaws.com/uploads/articles/h6pgpv0vlypkn1q8pif4.png)
请注意,即使有两种不同的票证类型,它们都通过相同的端点 POST,如您在下面的 HTML 中所见。
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--gMX4Re3K--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to -uploads.s3.amazonaws.com/uploads/articles/z90o2ltnbje9dcyddxtt.png)
Lambda 函数确定票证类型之间差异的方式是通过<input type="hidden" name="type" value="bronze">标签。当设置为type=bronze时,T 恤尺寸也可通过tshirt值获得。
我将在下面详细解释这些值如何指向 Stripe Checkout 中的正确项目。
生成条带结帐 URL
一旦访问者点击 TDF 网站上的购票按钮,我们就会使用专门生成的 URL 将他们引导至 Stripe Checkout。此生成的 URL 包含执行结帐所需的所有信息,包括要购买的产品,以及进行购买所需的任何其他信息(例如是否需要实际地址)。 Stripe SDK 自动处理此 URL 的生成,我们使用HTTP 303将浏览器转发给它(参见其他)。
为了生成 Checkout 会话 URL 并将浏览器转发给它,我们在 Lambda 中使用以下代码:
exports.handler = async (event) => {
const config = await loadConfig();
const stripe = require('stripe')(config.stripe_api_secret_key);
const session = await stripe.checkout.sessions.create( {
line_items: /* TODO */,
payment_method_types: [ 'card' ],
mode: 'payment',
success_url: config.success_url,
cancel_url: config.cancel_url
} );
const response = {
statusCode: 303,
headers: {
'Location': session.url
}
};
return response;
}
进入全屏模式 退出全屏模式
会话数据中的line_items指定要购买哪些产品。我们查看从浏览器 POST 到 Lambda 的数据,并更改应该将什么产品添加到line_items值。请注意,由于来自浏览器的有效负载可以进行 Base64 编码,因此我们会对此进行检查并根据需要进行解码。
if (event.body) {
let payload = event.body;
if (event.isBase64Encoded)
payload = Buffer.from(event.body, 'base64').toString();
const querystring = require('querystring');
const res = querystring.parse(payload);
if ((res.type) && (res.type == 'bronze')) {
// ...
}
}
进入全屏模式 退出全屏模式
如果这是一张访客票,我们只需将其产品添加到项目数组中。
let items = [ {
price: config.product_visitor_ticket,
quantity: 1
} ];
进入全屏模式 退出全屏模式
购买访客票时生成的 Stripe Checkout 页面如下所示:
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--eS5A0lIs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to -uploads.s3.amazonaws.com/uploads/articles/4po7ld6thtf25au7br7h.png)
对于青铜支持者票,我们将选定的 T 恤尺寸与价格 ID 匹配,并将两个产品(票和 T 恤)添加到line_items数组中。
let tshirt_type = config.product_tshirt_s;
if (res.tshirt) {
if (res.tshirt == 's') tshirt_type = config.product_tshirt_s;
if (res.tshirt == 'm') tshirt_type = config.product_tshirt_m;
if (res.tshirt == 'l') tshirt_type = config.product_tshirt_l;
if (res.tshirt == 'xl') tshirt_type = config.product_tshirt_xl;
}
items = [ {
price: config.product_bronze_ticket,
quantity: 1
}, {
price: tshirt_type,
quantity: 1
} ];
进入全屏模式 退出全屏模式
接下来,我们还需要输入实际地址,因为青铜支持者票包括 T 恤。在 Checkout 会话数据中,这通过shipping_address_collection中的allowed_countries和shipping_rates和受支持的国家(您希望允许运送到的国家/地区)指定。
将它们放在一起,生成的代码如下所示:
const session = await stripe.checkout.sessions.create( {
line_items: [ {
price: config.product_bronze_ticket,
quantity: 1
}, {
price: tshirt_type,
quantity: 1
} ],
payment_method_types: [ 'card' ],
mode: 'payment',
success_url: config.success_url,
cancel_url: config.cancel_url,
shipping_rates: [ config.shipping_rate ],
shipping_address_collection: {
allowed_countries: config.shipping_countries.split(',')
}
} );
进入全屏模式 退出全屏模式
将运输字段添加到会话数据后,您现在可以看到物理地址输入字段现在在 Stripe Checkout 表单上可见:
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--z5lLQT-t--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev -to-uploads.s3.amazonaws.com/uploads/articles/xl82a4iy6uu47hot3lml.png)
只需执行这几行代码,您现在就可以通过 Stripe Checkout 接受付款。在下一个会话中,我将介绍如何通过 Stripe 配置 Webhook,它可用于发送电子邮件或执行其他处理以响应来自 Stripe 的事件,例如一旦付款成功完成。
实现条纹 Webhook
如上所述,一旦购票成功,TDF 需要通过电子邮件将购票信息(包含登录芜湖党系统的信息)发送给购买者。这是通过使用单独的 Lambda 函数(通过 API 网关访问)作为 Stripe Webhook 来实现的。
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--DQSEVMY4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to -uploads.s3.amazonaws.com/uploads/articles/te3z7rdltkaagb64lqk2.png)
由于实现 webhook 会根据您需要的任何功能而有所不同,因此我将介绍正确解码来自 Stripe 的 POST 数据所需的内容。
首先要注意的是,由于您的 webhook URL 是公开的,因此任何人都可以访问它。 Stripe 将始终使用包含在 HTTP 标头中的签名调用您的 webhook,您可以使用 Webhook 密钥来验证有效负载数据是否正确和有效。
exports.handler = async (event) => {
// require Stripe signature in header
if (!event.headers['stripe-signature']) {
console.log('no Stripe signature received in header, returning 400 Bad Request');
return {
statusCode: 400
};
}
const sig = event.headers['stripe-signature'];
// require an event body
if (!event.body) {
console.log('no event body received in POST, returning 400 Bad Request');
return {
statusCode: 400
};
}
// decode payload
let payload = event.body;
if (event.isBase64Encoded)
payload = Buffer.from(event.body, 'base64').toString();
// construct a Stripe Webhook event
const config = await loadConfig();
const stripe = require('stripe')(config.stripe_api_secret_key);
try {
let ev = stripe.webhooks.constructEvent(payload, sig, config.webhook_signing_secret);
} catch (err) {
console.log('error creating Stripe Webhook event');
console.log(err);
return {
statusCode: 400
};
}
// ...TODO...
return {
statusCode: 200
};
}
进入全屏模式 退出全屏模式
从有效负载、签名和签名密钥成功构建 Stripe Webhook 事件后,您可以检查事件类型以确定状态如何更改。对于使用 Checkout 的简单付款,处理以下三个事件是典型的:
1.checkout.session.completed: 通过 Stripe Checkout 完成购买。根据付款方式,实际交易可能尚未完成。对于信用卡,payment_status通常设置为paid,这意味着交易已经完成。
2.checkout.session.async_payment_succeeded:通过之前的completed事件通知的不完整购买已成功。
3.checkout.session.async_payment_failed:通过之前的completed事件通知的不完整购买失败。
为了实现这三个事件的功能,我们大多遵循Stripe Sample Code文档中所示的相同代码。
const createOrder = async function(session) {
// we (TDF) don't need to do anything here
}
const fulfillOrder = async function(session) {
// send ticket info to customer by email
console.log('customer email is: ' + session.customer_details.email);
}
const emailCustomerAboutFailedPayment = async function(session) {
// send email about failed payment
}
exports.handler = async (event) => {
// ...
const session = ev.data.object;
switch (ev.type) {
case 'checkout.session.completed':
// save an order in your database, marked as 'awaiting payment'
await createOrder(session);
// check if the order is paid (e.g., from a card payment)
// a delayed notification payment will have an `unpaid` status
if (session.payment_status === 'paid') {
await fulfillOrder(session);
}
break;
case 'checkout.session.async_payment_succeeded':
// fulfill the purchase...
await fulfillOrder(session);
break;
case 'checkout.session.async_payment_failed':
// send an email to the customer asking them to retry their order
await emailCustomerAboutFailedPayment(session);
break;
}
进入全屏模式 退出全屏模式
实现createOrder()、fulfillOrder()、emailCustomerAboutFailedPayment()这三个函数后,你的Webhook就完成了。
如果您的 Webhook 出现错误或非HTTP 2xx响应,Stripe 将自动等待并根据需要处理重试。有关详细信息,请参阅Stripe Webhook 最佳实践文档。
......就是这样!您刚刚完成了使用 Stripe Checkout 处理付款所需的一切。
将多个端点与 API Gateway 自定义域合并
对于此处介绍的实现,有两个端点:Stripe Checkout URL 生成和 Stripe Webhook。使用由 API Gateway (https://7q6f1e5os2.execute-api.ap-northeast-1...) 提供的生成的端点 URL 完全没问题,但是将其配置为stripe.tokyodemofest.jp作为域中的子域对最终用户来说看起来更好。
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--f9wIkQM---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev -to-uploads.s3.amazonaws.com/uploads/articles/mpvoy2tl8f32h3ipbkm9.png)
使用上述设置,我们已将checkout和fulfillLambda 函数和 API 网关配置到单个自定义域中。
第一次使用Stripe做TDF的思考
老实说,在无服务器上实现 Stripe Checkout 非常容易。所需的代码量很小,任何人都可以通过几行代码从他们的网站接受付款。
此外,Stripe Dashboard 显示完整的 HTTP 请求和响应日志以及有用且详细的错误消息,使调试 Checkout 或 Webhook 实现变得容易。
到目前为止,我们多年来一直在努力保持 PayPal 的活跃,我唯一的遗憾是没有尽快切换到使用 Stripe。 :)
感谢您阅读这篇文章,希望它对某人有所帮助。如果您有任何疑问,请随时在下面的评论部分中提问!
更多推荐
所有评论(0)