Stripe 支付系统通用技术文档

Stripe 支付系统通用技术文档

Stripe 支付系统通用技术文档

目录

  1. 系统概述

  2. 架构设计

  3. 环境与配置

  4. 支付流程

  5. 接口设计

  6. 数据库设计

  7. 核心逻辑

  8. 多语言示例代码

  9. Webhook 回调与签名验证

  10. 支付结果页面

  11. 调试与常见问题

  12. 更新日志


系统概述

本系统基于 Stripe 支付网关 构建标准化的订单支付流程,适用于各类 Web、App 或后端服务。

主要特性:

  • Stripe Checkout 页面支付

  • 支持信用卡 / 借记卡 / Alipay

  • 回调自动更新订单状态

  • 环境独立配置(开发/生产)


架构设计


┌──────────────────────────────┐

│          客户端(Web/App)     │

└──────────────┬───────────────┘

               │

               ▼

       ┌──────────────┐

       │ 应用服务端 API │

       └───────┬────────┘

               │ 创建 Stripe Checkout Session

               ▼

       ┌────────────────────┐

       │ Stripe 平台         │

       │ (Checkout + Webhook)│

       └───────┬────────────┘

               │ Webhook 通知

               ▼

       ┌────────────────────┐

       │ 服务端验证+更新订单 │

       └───────┬────────────┘

               ▼

       ┌────────────────────┐

       │ 成功/失败页面展示    │

       └────────────────────┘


环境与配置

环境变量示例

| 名称 | 示例值 | 说明 |

| --------------------- | -------------------------------------- | ---------------- |

| STRIPE_SECRET_KEY | sk_live_xxx | Stripe 生产密钥 |

| STRIPE_SECRET_KEY_DEV | sk_test_xxx | Stripe 测试密钥 |

| STRIPE_WEBHOOK_SECRET | whsec_xxx | Webhook 签名密钥 |

| STRIPE_SUCCESS_URL | https://yourdomain.com/payment-success | 成功回调页面 |

| STRIPE_CANCEL_URL | https://yourdomain.com/payment-cancel | 失败回调页面 |

| APP_ENV | dev / prod | 环境标识 |

注意:

  • 必须区分测试与生产密钥。

  • Stripe Webhook 验证需使用 Raw Body

  • 所有金额以「分/美分」为单位保存。


支付流程


用户发起支付请求

       │

       ▼

创建 Stripe Checkout Session

       │

       ▼

返回支付链接 (session.url)

       │

       ▼

用户跳转至 Stripe 支付页

       │

       ▼

支付完成 → Stripe 调用 Webhook

       │

       ▼

服务端验证签名并更新订单状态

       │

       ▼

跳转成功/失败页展示结果


接口设计

1. 创建支付订单

POST /api/payment/create

请求体


{

  "orderNo": "ORD-2025-001",

  "amount": "99.99",

  "description": "购买年度会员"

}

响应


{

  "url": "https://checkout.stripe.com/pay/cs_test_12345"

}


2. Webhook 回调接口

POST /api/payment/webhook

请求头


Content-Type: application/json

Stripe-Signature: <签名字符串>

常见事件类型

| 事件 | 含义 |

| ---------------------------- | -------- |

| payment_intent.succeeded | 支付成功 |

| checkout.session.completed | 支付完成 |


数据库设计

表:order_info(订单表)

| 字段 | 类型 | 说明 |

| ---------------- | ------------- | -------------------------- |

| id | bigint / UUID | 主键 |

| order_no | varchar | 订单号 |

| user_id | bigint | 用户 ID |

| title | varchar | 商品/订单名称 |

| currency | varchar(10) | 货币代码 |

| amount | integer | 金额(以分为单位) |

| stripe_intent_id | varchar | Stripe Intent ID |

| status | tinyint | 1=待支付,2=已支付,3=取消 |

| created_at | datetime | 创建时间 |

| paid_at | datetime | 支付时间 |


核心逻辑

创建 Checkout Session


function createPaymentSession(orderNo, amount, description):

    stripeKey = APP_ENV == "prod" ? STRIPE_SECRET_KEY : STRIPE_SECRET_KEY_DEV

    stripe.init(stripeKey)



    session = stripe.checkout.create({

        mode: "payment",

        currency: "hkd",

        line_items: [{

            price_data: {

                product_data: { name: description },

                currency: "hkd",

                unit_amount: toCents(amount)

            },

            quantity: 1

        }],

        metadata: { order_no: orderNo },

        success_url: STRIPE_SUCCESS_URL + "?orderNo=" + orderNo,

        cancel_url: STRIPE_CANCEL_URL + "?orderNo=" + orderNo

    })

    return session.url


多语言示例代码

Python (Flask)


import stripe

from flask import Flask, request, jsonify



app = Flask(__name__)

stripe.api_key = "sk_test_xxx"



@app.route("/api/payment/create", methods=["POST"])

def create_payment():

    data = request.json

    session = stripe.checkout.Session.create(

        mode="payment",

        payment_method_types=["card"],

        line_items=[{

            "price_data": {

                "currency": "hkd",

                "product_data": {"name": data["description"]},

                "unit_amount": int(float(data["amount"]) * 100)

            },

            "quantity": 1

        }],

        metadata={"order_no": data["orderNo"]},

        success_url=f"https://example.com/payment-success?orderNo={data['orderNo']}",

        cancel_url=f"https://example.com/payment-cancel?orderNo={data['orderNo']}"

    )

    return jsonify({"url": session.url})


Java (Spring Boot)


@PostMapping("/api/payment/create")

public Map<String, String> createPayment(@RequestBody PaymentRequest req) {

    Stripe.apiKey = System.getenv("STRIPE_SECRET_KEY");

    SessionCreateParams params = SessionCreateParams.builder()

        .setMode(SessionCreateParams.Mode.PAYMENT)

        .addPaymentMethodType("card")

        .addLineItem(

            SessionCreateParams.LineItem.builder()

                .setQuantity(1L)

                .setPriceData(

                    SessionCreateParams.LineItem.PriceData.builder()

                        .setCurrency("hkd")

                        .setUnitAmount((long)(req.getAmount() * 100))

                        .setProductData(

                            SessionCreateParams.LineItem.PriceData.ProductData.builder()

                                .setName(req.getDescription()).build())

                        .build())

                .build())

        .setSuccessUrl("https://example.com/payment-success?orderNo=" + req.getOrderNo())

        .setCancelUrl("https://example.com/payment-cancel?orderNo=" + req.getOrderNo())

        .build();

    Session session = Session.create(params);

    return Map.of("url", session.getUrl());

}


Go (Gin)


r.POST("/api/payment/create", func(c *gin.Context) {

	var req struct {

		Amount      string `json:"amount"`

		OrderNo     string `json:"orderNo"`

		Description string `json:"description"`

	}

	c.BindJSON(&req)

	params := &stripe.CheckoutSessionParams{

		Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),

		PaymentMethodTypes: stripe.StringSlice([]string{"card"}),

		LineItems: []*stripe.CheckoutSessionLineItemParams{{

			PriceData: &stripe.CheckoutSessionLineItemPriceDataParams{

				Currency: stripe.String("hkd"),

				ProductData: &stripe.CheckoutSessionLineItemPriceDataProductDataParams{

					Name: stripe.String(req.Description),

				},

				UnitAmount: stripe.Int64(int64(toCents(req.Amount))),

			},

			Quantity: stripe.Int64(1),

		}},

		SuccessURL: stripe.String("https://example.com/payment-success?orderNo=" + req.OrderNo),

		CancelURL:  stripe.String("https://example.com/payment-cancel?orderNo=" + req.OrderNo),

	}

	session, _ := session.New(params)

	c.JSON(200, gin.H{"url": session.URL})

})


TypeScript (Express)


import express from "express";

import Stripe from "stripe";



const app = express();

app.use(express.json());

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2024-10-01" });



app.post("/api/payment/create", async (req, res) => {

  const { orderNo, amount, description } = req.body;

  const session = await stripe.checkout.sessions.create({

    mode: "payment",

    payment_method_types: ["card"],

    line_items: [{

      price_data: {

        currency: "hkd",

        product_data: { name: description },

        unit_amount: Math.round(parseFloat(amount) * 100)

      },

      quantity: 1

    }],

    metadata: { order_no: orderNo },

    success_url: `https://example.com/payment-success?orderNo=${orderNo}`,

    cancel_url: `https://example.com/payment-cancel?orderNo=${orderNo}`,

  });

  res.json({ url: session.url });

});


Rust (actix-web)


use actix_web::{post, web, App, HttpServer, Responder, HttpResponse};

use serde::Deserialize;

use stripe::{Client, CheckoutSession, CreateCheckoutSession};



#[derive(Deserialize)]

struct PaymentRequest {

    orderNo: String,

    amount: f64,

    description: String,

}



#[post("/api/payment/create")]

async fn create_payment(req: web::Json<PaymentRequest>) -> impl Responder {

    let client = Client::new("sk_test_xxx");

    let params = CreateCheckoutSession {

        mode: Some(stripe::CheckoutSessionMode::Payment),

        payment_method_types: Some(vec!["card".to_string()]),

        line_items: Some(vec![stripe::CreateCheckoutSessionLineItem {

            price_data: Some(stripe::CreateCheckoutSessionLineItemPriceData {

                currency: "hkd".to_string(),

                product_data: Some(stripe::CreateCheckoutSessionLineItemPriceDataProductData {

                    name: Some(req.description.clone()),

                    ..Default::default()

                }),

                unit_amount: Some((req.amount * 100.0) as i64),

                ..Default::default()

            }),

            quantity: Some(1),

            ..Default::default()

        }]),

        metadata: Some(std::collections::HashMap::from([

            ("order_no".to_string(), req.orderNo.clone()),

        ])),

        success_url: Some(format!("https://example.com/payment-success?orderNo={}", req.orderNo)),

        cancel_url: Some(format!("https://example.com/payment-cancel?orderNo={}", req.orderNo)),

        ..Default::default()

    };

    match CheckoutSession::create(&client, params).await {

        Ok(session) => HttpResponse::Ok().json(serde_json::json!({ "url": session.url })),

        Err(e) => HttpResponse::InternalServerError().body(format!("Stripe error: {:?}", e)),

    }

}



#[actix_web::main]

async fn main() -> std::io::Result<()> {

    HttpServer::new(|| App::new().service(create_payment))

        .bind(("127.0.0.1", 8080))?

        .run()

        .await

}


Webhook 回调与签名验证

逻辑概览


sig = request.headers["Stripe-Signature"]

event = Stripe.webhooks.construct_event(raw_body, sig, STRIPE_WEBHOOK_SECRET)



if event.type == "payment_intent.succeeded":

    intent = event.data.object

    update order_info set

        status = 2,

        stripe_intent_id = intent.id,

        paid_at = now()

    where order_no = intent.metadata.order_no and status = 1

幂等性

  • Stripe 可能多次发送同一事件

  • 更新时应使用 order_no + status 条件防止重复修改


支付结果页面

成功页 /payment-success

展示:

  • 订单号

  • 支付金额

  • 支付时间

失败页 /payment-cancel

展示:

  • 支付失败提示

  • 重新支付入口


调试与常见问题

| 问题 | 说明 | 解决方案 |

| ---------------- | ---------------- | ---------------------- |

| Webhook 验证失败 | 请求体被解析 | 使用 Raw Body 验证签名 |

| 金额错误 | 浮点未转整数 | 使用分/美分单位 |

| 回调多次触发 | Stripe 自动重试 | 设置幂等逻辑 |

| 页面跳转失败 | URL 不可公网访问 | 使用可访问 URL |


本文由萧兮的博客原创发布,欢迎转载,转载务必保留原文链接。

萧兮的博客https://www.20010515.xyz · 原文:https://www.20010515.xyz/posts/d7b35a88-afcc-4dac-b522-73343d5d9ecf