前言
公司最近做到了国外支付功能,最后选型使用stripe进行支付,实现目标:使用stripe支付可以让国外用户自己选择支付方式并订阅支付。
用到的语言和技术
先说下用到的开发技术:
前端:Vue
后端:net6.0
后端需要引入的nuget包:Stripe.net
stripe平台有多种支付方式,目前选择的是在stripe平台生成价格表,然后把价格表的js脚本放在vue前端。
整体流程
要在自己的项目里面实现stripe价格表支付的话
1.
首选在stripe平台切换到【测试模式】,在开发环境下,使用测试模式,不要使用正式模式,然后在stripe支付平台添加价格,产品,然后在价格表添加上产品以及产品下面的价格,保存后价格表会生成一个js脚本,把这个脚本放到自己项目前端,用于直接生成固定的价格(注:价格表只能设置几个固定价格让用户去购买,如果自己的项目需求是不同的价格,比如商城里面商品很多,商品型号也很多,这样的需求就不太适合使用价格表来做支付了,最好是在自己的项目后台通过stripe的api生成不同的价格来实现支付,目前这篇文章主要讲解的是价格表支付以及对接stripe的一个思路)
2.
拿到stripe平台生成的价格表js脚本,放到vscode前端页面里面,运行项目就会显示一个配置好的价格表,价格表的每个价格都会有一个 支付/订阅 按钮 ,点击按钮会自动进入支付页面,支付页面包含 让用户选择邮箱,支付类型,比如银行卡支付,银行卡号等,stripe平台提供了测试模式下可以支付成功的银行卡号:4242424242424242
在支付填写完支付信息点击提交按钮后,会进入支付回调,回调需要在stripe平台配置回调地址(注:正式的回调地址必须是https开头的协议地址+域名+项目后台回调路径
才会生效),测试模式下在本地开发stripe支付不需要配置https的回调地址,可以在本地电脑安装stripe cli
这个可以自己搜索下如何在本地安装,比较简单。安装完成后使用cmd命令行输入 stripe login 然后校验一下权限
,再输入需要监听的本地路径,例如:stripe listen --load-from-webhooks-api --forward-to localhost:9081/api/webhook
输入后会返回一个webhook的测试秘钥,把这个秘钥配置在后台项目里。在回调时候需要用到这个秘钥,只有秘钥验证成功才能继续执行后面的回调事件。
3
.配置好stripe前期工作后,在前端vue代码里面放入stripe价格表js脚本,注意:这个价格表里面的每个价格下面的支付/订阅按钮是不能在vue前端触发到点击事件的,因为这是stripe生成的页面。
我们能做的是在前端页面的created里面调用后端接口 生成stripe客户会话
,把后端接口返回客户会话字段customer-session-client-secret
放在前端js脚本里面,会有时效限制,好像是半小时时效。这个customer-session-client-secret字段是后端接口里面通过调用stripe平台api生成的值,customer-session-client-secret客户会话字段的作用:身份验证,会话管理,安全性
。
4
.在后端,我使用的是net开发,首先需要在appsettings.json里面配置stripe平台的一些秘钥信息:
"Stripe": {
"StripeSecretKey": "sk_test_xxxx这是你的stripe开发秘钥",
"EndpointSecret": "whsec_8fxxx这是stripe上面Webhook回调的安全密钥,开发期间使用stripe cli监听后可以得到测试秘钥"
}
然后写一个 生成客户支付会话
的接口,在讲解接口前,需要先了解一些前置信息。
首先stripe平台里面是有一个 客户信息页面的,这个页面包含 名称,邮箱 客户ID(StripeCustomId后面会用到)
,创建时间,购买的产品信息等,所以我们后续是可以在stripe平台看到哪些客户购买了哪些产品以及购买的时间等信息的,当然也可以把这些客户信息以及购买信息在支付回调时候获取到存储到数据库中。
然后在自己的数据库里面创建一些跟stripe支付有关联的表,主要讲:stripe客户信息表,这个表里面主要是把自己项目的userid和stripe的客户id(StripeCustomId)进行关联,在后面的 生成客户支付会话信息接口 会往这个客户信息表存储数据。
下面讲解 生成客户支付会话 接口里面的大概逻辑:
- 获取客户信息(查询数据库 stripe客户信息表 ,根据用户ID查询当前用户是否已经关联stripe上面生成的客户信息)
- 如果当前用户没有stripe客户信息,说明是第一次进行支付,那么就调用stripe的api生成一个客户ID保存到stripe客户信息表里面,在这一步可以在生成客户信息的时候添加一些元数据到客户信息里,元数据不能是银行卡号等这些敏感数据,可以传输比如userid,userName,roleid,roleName这些业务逻辑数据,元数据主要是用于支付回调时候进行使用。
- 根据 查询/生成的客户ID(StripeCustomId)生成一个客户支付会话字段值,并且返回前端。
5
.支付回调处理。在到第五步之前,已经是可以进行支付了,在支付完成后,在自己后台写的回调页面里面找到对应的触发事件,我用的是invoice.payment_succeeded支付完成事件来获取支付完成信息进行处理后续逻辑。
下面是具体操作步骤
stripe平台
第一步首先打开stripe平台,然后选择 产品目录-价格表(不是中文的可以在页面右上角设置里面转换一下语言,转成简体中文)
添加完成价格表后,把价格表生成的脚本代码复制到vue前端
到这里,stripe平台的工作基本就算是完成了(一些其他配置也很多,但是每个人用到的不一样,比如开发秘钥在哪里等等,用到哪些具体再去找就好了),后面主要是前端和后端接口的使用了。stripe平台的东西很多,但是并不复杂,详细分析的话一篇文章分析完不太现实,主要是提供一种思路。
vue前端
在前端,虽然说是引入js脚本,但是从stripe价格表复制过来后还是做了一些更改:
首选pricing-table-id(价格表ID)做成了动态赋值,这样做的目的是可以根据自己的业务需求来显示不同的价格表,比如检查到用户网站是中文模式就显示中文的价格表,否则就显示英文的价格表(同一个价格表是不能自动转换语言的,所以如果需要多个语言的价格就需要配置多个价格表)publishable-key(前端通信公钥)不需要改动,多个价格表使用的公钥相同。
然后在mounted里面动态加载价格表:
附上代码:
mounted() {
// 动态加载 Stripe 定价表脚本
const script = document.createElement('script');
script.src = 'https://js.stripe.com/v3/pricing-table.js';
script.async = true;
script.onload = () => {
this.isScriptLoaded = true; // 脚本加载完成后设置标志
};
document.body.appendChild(script);
},
然后在created里面调用接口:
created() {
/*前面是其他业务逻辑处理...*/
//生成客戶会话
this.createCustomerSession().then((session) => {
this.customerSessionClientSecret = session.clientSecret;
})
}
methods: {
createCustomerSession() {
return this.http.post('/api/Sys_Customers/createCustomerSession', {
}).then((res) => {
if (res) {
return res; // 返回成功的数据
}
}).catch((err) => {
// 处理错误
console.error("创建客户会话失败:", err);
throw err; // 抛出错误以供外部捕获
});
},
}
这个时候页面就直接显示出来价格表了,类似这样子:
·点击价格下面的支付/订阅按钮会跳转到支付页面(不需要自己写代码,stripe已经生成,反过来讲这个页面的样式,输入框等也不能自己使用代码控制,只能在stripe平台去更改配置,并且这个页面不能直接反填一些你自己的配置信息,比如说你想在这个支付页面显示一个新增加的输入框,进入页面的时候输入框里面就显示值:AA,这个是不能实现的,可以在进入页面的时候就显示的值只有email输入框的值,而且显示后是只读的,不能更改)
下面就是困扰我时间比较长的问题了,如何去调用后台接口?在我选择支付的时候我肯定需要把userid等用户信息一起带到支付信息里面,不然支付了也不清楚是哪个用户去支付的
那么在什么时机去传输用户信息到支付信息里面呢?当时想做的是点击 支付/订阅 按钮的时候去调用接口传输用户信息的,但是价格表是直接生成的页面,所以我们没办法在vue前端去抓取到支付按钮的点击事件。
最后找到的实现方法是:在前端created里面直接添加一个接口,这个接口的实现逻辑为:生成stripe客户会话(上面有讲到这块流程),在生成客户会话的时候创建stripe元数据(键值对格式),把用户信息保存到元数据里面,这样子我们回调时候就能通过获取客户元数据里面的信息来获取到用户信息了。
Net后端
在开发接口前,需要先引入NuGet包:Stripe.net(我使用的版本是47.0.0)
/// <summary>
/// 生成客户支付会话
/// </summary>
/// <param name="Customer"></param>
/// <returns></returns>
[HttpPost, Route("createCustomerSession")]
public async Task<IActionResult> CreateCustomer([FromBody] CustomerRequest Customer)
{
var user = UserContext.Current.UserInfo;
//获取客户信息
var CustomerInfo = await _Repository.FindAsyncFirst(x => x.User_Id == user.User_Id);
string customerId;
if (CustomerInfo == null)
{
//创建客户信息并保存到数据库
var options = new CustomerCreateOptions
{
Name = user.UserName,
//Email = Customer.email,
//Metadata里面为元数据
Metadata = new Dictionary<string, string>
{
//{ "email", Customer.email },
{ "name", user.UserName },
{ "userId", user.User_Id.ToString() },
{ "farmId", user.CurrentFarmId.ToString() },
{ "PayRoleName", user.RoleName },
{ "PayRoleId", user.CurrentRoleId.ToString() },
}
};
//通过stripe的api生成客户信息
var service = new CustomerService();
var customer = await service.CreateAsync(options);
customerId = customer.Id;//客户ID
//保存客户信息到数据库
Sys_Customer Customer = new Sys_Customer
{
User_Id = user.User_Id,
StripeCustomId = customer.Id,
StripeName = user.UserName,
Creator = user.UserTrueName,
CreateDate = DateTime.Now,
Modifier = user.UserTrueName,
ModifyDate = DateTime.Now
};
_Repository.Add(sys_Customer);
_Repository.SaveChanges();
}
else
{
customerId = CustomerInfo.StripeCustomId;
}
//调用stripe的api生成stripe客户支付会话
var sessionOptions = new CustomerSessionCreateOptions
{
Customer = customerId,
Components = new CustomerSessionComponentsOptions
{
PricingTable = new CustomerSessionComponentsPricingTableOptions
{
Enabled = true
}
}
};
var sessionService = new CustomerSessionService();
var session = await sessionService.CreateAsync(sessionOptions);
return Json(new { status = true, clientSecret = session.ClientSecret });
}
下面webhook回调页面的代码:主要看 if (stripeEvent.Type == EventTypes.InvoicePaymentSucceeded)里面的代码
[Route("/api/webhook")]
public class WebhookController : Controller
{
//前面根据自己的框架进行配置获取数据上下文,获取appsettings.json里面的秘钥值等等
[HttpPost]
public async Task<IActionResult> Index()
{
StripeConfiguration.ApiKey = AppSetting.Stripe.StripeSecretKey;
string endpointSecret = AppSetting.Stripe.EndpointSecret;
var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();
try
{
var stripeEvent = EventUtility.ConstructEvent(json,
Request.Headers["Stripe-Signature"], endpointSecret);
//var stripeEvent = EventUtility.ParseEvent(json);
// 创建订阅时触发
if (stripeEvent.Type == EventTypes.CustomerSubscriptionCreated)
{
Logger.Info($"webhook事件类型:{EventTypes.CustomerSubscriptionCreated},打印stripeEvent:{stripeEvent}");
}
// 支付成功,不包含价格编号,发票等信息
if (stripeEvent.Type == EventTypes.PaymentIntentSucceeded)
{
Logger.Info($"webhook事件类型:{EventTypes.PaymentIntentSucceeded},打印stripeEvent:{stripeEvent}");
}
//支付成功,包含价格,发票等信息,后续主要用这个事件进行回调处理。
if (stripeEvent.Type == EventTypes.InvoicePaymentSucceeded)
{
Logger.Info($"webhook事件类型:{EventTypes.InvoicePaymentSucceeded},打印stripeEvent:{stripeEvent}");
var invoice = stripeEvent.Data.Object as Invoice;
var customerId = invoice.CustomerId; // 获取客户 ID
var lineItem = invoice.Lines.Data[0]; // 获取第一个行项目(这块是因为我的项目需求每次只能购买一个价格,所以只取第一行,如果多个价格一起支付的话就需要循环获取了)
var priceId = lineItem.Price.Id; // 获取价格 ID
// 使用 CustomerService 获取客户信息
var customerService = new CustomerService();
var customer = await customerService.GetAsync(customerId); // 获取客户
// 访问客户的元数据
var metadata = customer.Metadata;
var userId = metadata.ContainsKey("userId") ? metadata["userId"] : null;
var userName = metadata.ContainsKey("userName") ? metadata["userName"] : null;
var farmId = metadata.ContainsKey("farmId") ? metadata["farmId"] : null;
var PayRoleName = metadata.ContainsKey("PayRoleName") ? metadata["PayRoleName"] : null;
var PayRoleId = metadata.ContainsKey("PayRoleId") ? metadata["PayRoleId"] : null;
//获取支付意图ID
var paymentIntentId = invoice.PaymentIntentId.ToString();
//根据支付意图ID获取支付方式ID
var paymentIntentService = new PaymentIntentService();
var paymentIntent = await paymentIntentService.GetAsync(paymentIntentId);
// 获取支付方式 ID
var paymentMethodId = paymentIntent.PaymentMethodId;
// 获取支付方式类型
var paymentMethodService = new PaymentMethodService();
var paymentMethod = await paymentMethodService.GetAsync(paymentMethodId);
var paymentMethodType = paymentMethod.Type;
// 计算总税费
decimal totalTax = 0;
foreach (var taxAmount in invoice.TotalTaxAmounts)
{
totalTax += taxAmount.Amount; // 累加税费
}
var stripeStartTime = lineItem.Period.Start; // 获取开始时间 (UTC)
var stripeEndTime = lineItem.Period.End; // 获取结束时间 (UTC)
// 计算时间间隔
TimeSpan interval = stripeEndTime - stripeStartTime;
TimeSpan serverOffset = TimeZoneInfo.Local.GetUtcOffset(DateTime.UtcNow); // 获取服务器当前时区偏移量
//转换为服务器时间
DateTimeOffset serverTimeStart = new DateTimeOffset(stripeStartTime, TimeSpan.Zero).ToOffset(serverOffset);
DateTimeOffset serverTimeEnd = new DateTimeOffset(stripeEndTime, TimeSpan.Zero).ToOffset(serverOffset);
var sys_PayLog = new Sys_PayLog();
//根据自己业务需求添加字段...
sys_PayLog .PayDate = serverTimeStart.DateTime;
sys_PayLog .ExpirDate = serverTimeEnd.DateTime;
sys_PayLog .PayNo = invoice.Id;
sys_PayLog .User_Id = int.TryParse(userId, out var id) ? id : 0;
sys_PayLog .RoleName = PayRoleName;
sys_PayLog .RoleId = int.TryParse(PayRoleId, out var roleId) ? roleId : 0;
// 保存到数据库
_Repository.Add(sys_PayLog);
await _Repository.SaveChangesAsync();
}
// 结账完成
if (stripeEvent.Type == EventTypes.CheckoutSessionCompleted)
{
Logger.Info($"webhook事件类型:{EventTypes.CheckoutSessionCompleted},打印stripeEvent:{stripeEvent}");
}
// ... handle other event types
else
{
Logger.Info($"webhook事件类型:{stripeEvent.Type},事件不存在");
}
return Ok(new { received = true });
}
catch (StripeException e)
{
Logger.Info($"webhook报异常:{e}");
return BadRequest();
}
}
}
注意
:这个里面当初我根据stripe官网得到的代码是 if (stripeEvent.Type == Event.InvoicePaymentSucceeded),但是会报错,检查了半天没发现问题,并且在国内国外都没搜到解决办法,最后还是自己反编译引入的Nuget包Stripe.net发现47.0.0版本的Event已经改成了EventTypes的方式了
,但是官网demo还是使用的之前的Event.方式,真的有点无语了。不管怎么样,代码改成if (stripeEvent.Type == EventTypes.InvoicePaymentSucceeded)就没问题了,如果你也遇到类似的问题,也可以试着反编译你引用的包查看最正确的用法。
遇到的问题思考
1.stripe上面订阅和支付的区别?
答:订阅相当于购买产品后自动续费,如果订阅的是月付包,那么到了一个月这个时间就是自动扣款续费。支付是一次性的,不会自动续费。
2.一个客户在订阅了产品A以后,隔了一分钟这个客户是否还能订阅产品A?
答:在stripe平台设置里面可以设置-产品设置-付款 里面可以设置客户是否可以多个订阅。
3.stripe支付页面是否可以自动反填一些跟自己项目业务相关的值?
答:不可以,stripe支付页面除了email,其他的值只能让客户自己去填写。
4.email反填到支付页后,还能够让客户自己更改吗?
答:不可以,如果email设置为反填的话,在支付页面就显示为只读了。
5.一个用户如果多次购买产品,如何只生成一个stripe客户信息?
答:在后台接口把用户id和stripe客户id关联上,一个用户始终使用同一个stripe客户ID去生成客户支付会话。
6.在本地开发stipe支付的时候,我使用了stripe cli命令行进行模拟webhook回调地址,那么如何确认回调地址是否被成功监听到呢?
答:本地运行后台项目,并且在回调地址上打上断点,本地运行前端包含价格表的项目,打开cmd运行stripe cli监听后台webhook地址路径,前端通过价格表正常支付,这个时候后台就会进入回调断点。
总结
stripe的几种支付方式,实现逻辑其实差距不大,使用价格表主要是不需要调用stripe的api去生成价格了,可以直接通过嵌入价格表到前端的方式直接使用stripe支付,但是局限性也会有,如果是很多不同的价格,比如商城商品,那么就不适合使用价格表,看个人的业务需求去使用。