Stripe 支付系统通用技术文档
Stripe 支付系统通用技术文档
目录
系统概述
本系统基于 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