stripe 支付
stripe 一体化全球支付平台,有良好的用户体验和界面设计,支持多种接入方式,且功能比较灵活,也易于集成。
注册和 API 密钥
账户注册验证激活
- 访问 Stripe 网站:https://stripe.com/zh-hk ,并点击登录按钮登录,没有账号的可以先注册一个。
- 填写必要信息:提供您的电子邮件地址、密码等信息以注册账户,并完成身份验证步骤,以确保账户的安全性。
- 集成 stripe 支付,一开始账户处于开发模式,所有数据都是模拟的,上线时录入商家或公司信息激活账户进入真实模式。
API 密钥获取
- 登录 Stripe 控制台:https://dashboard.stripe.com ,使用您的账户信息登录 Stripe 控制台。
- 进入开发人员界面,单击 API 密钥选项卡,在下面可以找到公钥和密钥。
- 在开发阶段,使用测试密钥,在生产环境中使用生产密钥,注意保管好密钥。
妥善保管这些凭据,并根据需要定期更新它们,以维护支付系统的安全性
集成前后端
这里使用 go 语言为后端语言,react 为前端语言。
一、设置服务器
将依赖项添加到您的项目并导入库。
go get -u github.com/stripe/stripe-go/v78
接下来创建一个结账接口,前端客户请求这个接口时,按客户的要购买的商品来创建一个结账会话,并将这个会话的ClientSecret
返回给客户。
import (
"log"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/stripe/stripe-go/v78"
"github.com/stripe/stripe-go/v78/checkout/session"
)
func init() {
// This is your test secret API key.
stripe.Key = "sk_test_51P12rrI3yx14f44QTHOPqliw9So8GBpZ6EIPi7v1iDbdlgr5NH0vXIH3zUjQJwxLI7pbAsfSErQOy75sAVr7q4ra00AODgXKD0"
}
func CreateCheckoutSession(c *gin.Context) {
var req struct {
Email string `json:"email"`
Amount int64 `json:"amount"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"msg": "param decode error:" + err.Error(),
})
}
params := &stripe.CheckoutSessionParams{
LineItems: []*stripe.CheckoutSessionLineItemParams{
{
PriceData: &stripe.CheckoutSessionLineItemPriceDataParams{
Currency: stripe.String("usd"),
UnitAmount: stripe.Int64(10000 * req.Amount),
ProductData: &stripe.CheckoutSessionLineItemPriceDataProductDataParams{
Name: stripe.String("AI Tools"),
},
},
Quantity: stripe.Int64(1),
},
},
CustomerEmail: stripe.String(req.Email),
Metadata: map[string]string{"email": req.Email, "amount": strconv.Itoa(int(req.Amount))},
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
UIMode: stripe.String("embedded"),
ReturnURL: stripe.String("http://localhost:5173/#/payment?session_id={CHECKOUT_SESSION_ID}"),
}
s, err := session.New(params)
if err != nil {
log.Printf("session.New: %v", err)
}
c.JSON(http.StatusOK, gin.H{
"clientSecret": s.ClientSecret,
})
}
按客户实际请求填充stripe.CheckoutSessionParams
,可以参考上面代码,修改产品和价格即可。
另外需要一个会话详情接口,用来辅助前端查看结账会话是否已经支付。
func RetrieveCheckoutSession(c *gin.Context) {
s, _ := session.Get(c.Query("session_id"), nil)
c.JSON(http.StatusOK, gin.H{
"status": string(s.Status),
"customer_email": string(s.CustomerDetails.Email),
})
}
最后再配置路由端口
router := gin.Default()
router.POST("/create-checkout-session", CreateCheckoutSession)
router.GET("/session-status", RetrieveCheckoutSession)
二、设置前端
将 Stripe 添加到 React 应用程序中
npm install --save @stripe/react-stripe-js @stripe/stripe-js
编写一个支付组件和结果组件,要付款时展示支付组件,支付完成后展示结果组件,调用后端结账接口完成支付功能。
import { Button, Card, Checkbox, Form, Input, InputNumber, message, theme } from 'antd'
import { useCallback, useEffect, useState } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { Api } from '@api/index'
import { loadStripe } from '@stripe/stripe-js';
import {
EmbeddedCheckoutProvider,
EmbeddedCheckout
} from '@stripe/react-stripe-js';
import { getLocalStorage } from '@utils/publicFn'
const stripePromise = loadStripe("pk_test_51P12rrI3yx14f44QGrlx7wtiyuwt969ttal5rfsSKOqxvoL3iUt3QlTft9b05ZFNea6KQvSriOeESbbBbJOba6nN00LXBpdQ9r");
const CheckoutForm = ({ email, amount }) => {
const fetchClientSecret = useCallback(async () => {
return (await Api.createCheckoutSession({ email, amount })).clientSecret
}, []);
return (
<div id="checkout">
<EmbeddedCheckoutProvider
stripe={stripePromise}
options={{ fetchClientSecret }}
>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
</div>
)
}
const Complete = ({ customerEmail }) => {
const navigate = useNavigate();
return (
<section id="success">
<p>
We appreciate your business! A confirmation email will be sent to {customerEmail}.
</p>
<p>
If you have any questions, please email <a href="mailto:orders@example.com">orders@example.com</a>.
</p>
<Button onClick={() => navigate('/payment')}>返回</Button>
</section>
)
}
const Payment = () => {
const navigate = useNavigate();
const {
token: { colorBgContainer },
} = theme.useToken()
const [open, setOpen] = useState(false);
const [status, setStatus] = useState(null);
const [expiration, setExpiration] = useState(0);
const [customerEmail, setCustomerEmail] = useState('');
const [email, setEmail] = useState(getLocalStorage('user'));
const [amount, setAmount] = useState(1);
const queryString = useLocation().search;
useEffect(() => {
Api.userProfile()
.then((data) => {
setExpiration(data.expiration)
})
}, [])
useEffect(() => {
const urlParams = new URLSearchParams(queryString);
const sessionId = urlParams.get('session_id');
if (sessionId) {
Api.sessionStatus({ session_id: sessionId })
.then((data) => {
setStatus(data.status);
setCustomerEmail(data.customer_email);
});
} else {
setStatus(null);
setCustomerEmail('');
setOpen(false)
}
}, [queryString]);
const handlePay = ({ email, amount }) => {
setEmail(email)
setAmount(parseInt(amount))
setOpen(true)
}
return (
<div>
{(!open && !status) && <>
<Card>
<p>套餐到期时间:{new Date(expiration * 1000).toLocaleString()}</p>
<Button onClick={() => navigate('/')}>返回首页</Button>
</Card>
<Card>
<Form onFinish={handlePay} initialValues={{ email, amount }}>
<Form.Item label="邮箱" name="email"><Input /></Form.Item>
<Form.Item label="数量" name="amount"><InputNumber style={{ width: "100%" }} addonAfter={"年"} min={1} max={100} /></Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">支付</Button>
</Form.Item>
</Form>
</Card>
</>}
{(open && !status) && <CheckoutForm email={email} amount={amount} />}
{status && <Complete customerEmail={customerEmail} />}
</div>
)
}
export default Payment
测试时可以使用测试卡号完成支付:4242424242424242,日期任意一个未来日期即可,CVC 为任意三位数
三、履行订单
在用户支付完成后,我们需要履行订单,比如给用户发货、充值卡点、续费套餐等等。stripe 推荐使用 Webhook HTTP 端点来接收事件,也可以使用事件接口轮询。
下面的示例中,处理结账会话支付完成事件,给用户的 VIP 套餐续期。
import (
"encoding/json"
"log"
"net/http"
"strconv"
"time"
"server/model"
"github.com/gin-gonic/gin"
"github.com/stripe/stripe-go/v78"
"github.com/stripe/stripe-go/v78/checkout/session"
"github.com/stripe/stripe-go/v78/event"
)
func init() {
stripe.Key = "sk_test_51P12rrI3yx14f44QTHOPqliw9So8GBpZ6EIPi7v1iDbdlgr5NH0vXIH3zUjQJwxLI7pbAsfSErQOy75sAVr7q4ra00AODgXKD0"
go func() {
d := 5 * time.Second
for {
time.Sleep(d)
events := event.List(&stripe.EventListParams{
CreatedRange: &stripe.RangeQueryParams{
GreaterThanOrEqual: time.Now().Add(-d).Unix(),
},
Type: stripe.String(string(stripe.EventTypeCheckoutSessionCompleted)),
})
for events.Next() {
event := events.Event()
if event.Type == stripe.EventTypeCheckoutSessionCompleted {
var session stripe.CheckoutSession
json.Unmarshal(event.Data.Raw, &session)
id, email, amountStr := session.ID, session.Metadata["email"], session.Metadata["amount"]
amount, _ := strconv.Atoi(amountStr)
log.Println("order completed:", id, email, amount)
expiration, err := model.SelectUserExpiration(email)
if err == nil {
now := time.Now().Unix()
if expiration == nil || *expiration < now {
expiration = &now
}
*expiration = time.Unix(*expiration, 0).AddDate(amount, 0, 0).Unix()
model.UpdateUserExpiration(email, *expiration)
} else {
log.Println("Select Expiration Error:", err)
}
}
}
}
}()
}