stripe 支付

admin
admin 4月18日
  • 在其它设备中阅读本文章

stripe 一体化全球支付平台,有良好的用户体验和界面设计,支持多种接入方式,且功能比较灵活,也易于集成。

注册和 API 密钥

账户注册验证激活

  1. 访问 Stripe 网站:https://stripe.com/zh-hk ,并点击登录按钮登录,没有账号的可以先注册一个。
  2. 填写必要信息:提供您的电子邮件地址、密码等信息以注册账户,并完成身份验证步骤,以确保账户的安全性。
  3. 集成 stripe 支付,一开始账户处于开发模式,所有数据都是模拟的,上线时录入商家或公司信息激活账户进入真实模式。

API 密钥获取

  1. 登录 Stripe 控制台:https://dashboard.stripe.com ,使用您的账户信息登录 Stripe 控制台。
  2. 进入开发人员界面,单击 API 密钥选项卡,在下面可以找到公钥和密钥。
  3. 在开发阶段,使用测试密钥,在生产环境中使用生产密钥,注意保管好密钥。

妥善保管这些凭据,并根据需要定期更新它们,以维护支付系统的安全性

集成前后端

这里使用 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)
                    }
                }
            }
        }
    }()
}