Django+React项目Stripe支付方式配置失败求助
支付功能开发问题:Stripe结账流程异常与实现疑问
问题描述
我正在为应用开发支付功能,首次尝试遵循Bryan Dunn的教程实现。已添加SITE_URL,但因package.json配置了proxy且使用axios发起API请求,不确定是否还需SITE_URL,能否改用axios实现?点击“Proceed to checkout”按钮时URL无效,改为form.control后可跳转,但Stripe预构建表单和产品均未显示。
相关代码
Django URL配置
urlpatterns = [ path('', views.getRoutes, name="get-routes"), path('users/register/', views.registerUser, name="register"), path('users/login/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('products/', views.getProducts, name="get-products"), path('products/<str:pk>', views.getProduct, name="get-product"), path('user/profile/', views.getUserProfile, name="get-user-profile"), path('users/', views.getUsers, name="get-users"), path('search/', ProductFilter.as_view(), name="search-product"), path('create-checkout-session/', StripeCheckoutView.as_view()), ]
Django视图
class StripeCheckoutView(APIView): def post(self, request): try: checkout_session = stripe.checkout.Session.create( line_items=[ { 'currency': 'usd', 'price': 'price_1MAtSLJU3RVFqD4TnNYWOxPO', 'quantity': 1, }, ], payment_method_types=['card'], mode='payment', success_url=settings.SITE_URL + '/?success=true&session_id={CHECKOUT_SESSION_ID}', cancel_url=settings.SITE_URL + '/' + '?canceled=true', ) return redirect(checkout_session.url) except: return Response( {'error': 'Something went wrong when creating stripe checkout session'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR )
React购物车组件(cartScreen.js)
import React, { useEffect } from "react"; import { Link } from "react-router-dom"; import { Row, Col, Image, ListGroup, Button, Card, Form, } from "react-bootstrap"; import { useDispatch, useSelector } from "react-redux"; import Message from "../Message"; import { addToCart, removeFromCart } from "../../actions/CartActions"; import { API_URL } from "../../config/config"; function CartSreen({ match, location, history }) { const productId = match.params.id; const quantity = location.search ? Number(location.search.split("=")[1]) : 1; const dispatch = useDispatch(); const cart = useSelector((state) => state.cart); const { cartItems } = cart; useEffect(() => { if (productId) { dispatch(addToCart(productId, quantity)); } }, [dispatch, productId, quantity]); const removeFromCartHandler = (id) => { dispatch(removeFromCart(id)); }; const checkoutHandler = () => { history.push("/api/stripe/create-checkout-session"); }; return ( <Row> <Col md={8}> <h1>Shopping Cart</h1> {cartItems.length === 0 ? ( <Message variant="info"> Your cart is empty <Link to="/">Go Back</Link> </Message> ) : ( <ListGroup variant="flush"> {cartItems.map((item) => ( <ListGroup.Item key={item.product}> <Row> <Col md={2}> <Image src={item.image} alt={item.name} fluid rounded /> </Col> <Col md={3}> <Link to={`/product/${item.product}`}>{item.name}</Link> </Col> <Col md={2}>${item.price}</Col> <Col md={3}> <Form.Control as="select" value={item.quantity} onChange={(e) => dispatch( addToCart(item.product, Number(e.target.value)) ) } > {[...Array(item.countInStock).keys()].map((x) => ( <option key={x + 1} value={x + 1}> {x + 1} </option> ))} </Form.Control> </Col> <Col md={1}> <Button type="button" variant="light" onClick={() => removeFromCartHandler(item.product)} > <i className="fas fa-trash"></i> </Button> </Col> </Row> </ListGroup.Item> ))} </ListGroup> )} </Col> <Col md={4}> <Card> <ListGroup variant="flush"> <ListGroup.Item> <h2> Subtotal ( {cartItems.reduce((acc, item) => acc + item.quantity, 0)}) items </h2> $ {cartItems .reduce((acc, item) => acc + item.quantity * item.price, 0) .toFixed(2)} </ListGroup.Item> </ListGroup> <ListGroup.Item> <Button type="submit" className="btn-block" disabled={cartItems.length === 0} action={checkoutHandler} > Proceed to Checkout </Button> </ListGroup.Item> </Card> </Col> </Row> ); } export default CartSreen;
React配置文件(config.js)
import { useMemo } from "react"; import { useLocation } from "react-router"; export const API_URL = "http://localhost:8000"; export const useQuery = () => { const { search } = useLocation(); return useMemo(() => new URLSearchParams(search), [search]); };
React路由配置(app.js)
<Container> <Route path="/" component={HomeScreen} exact /> <Route path="/login" component={LoginScreen} exact /> <Route path="/register" component={RegisterScreen} exact /> <Route path="/product/:id" component={ProductScreen} exact /> <Route path="/cart/:id?" component={CartScreen} exact /> </Container>
Django配置文件(settings.py)
""" Django settings for backend project. Generated by 'django-admin startproject' using Django 4.1.3. For more information on this file, see https://docs.djangoproject.com/en/4.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.1/ref/settings/ """ from datetime import timedelta from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'django-insecure-gr1@(e6&lq%k8&v+%^xqp0966d6)_!q7(*l+y%8d)rt(6s_*hq' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'app.apps.AppConfig', 'rest_framework', "corsheaders", 'django_filters', ] REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication', 'rest_framework_simplejwt.authentication.JWTAuthentication', # 'rest_framework.permissions.AllowAny', ) } MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', "corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware", ] ... SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(days=30), 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), 'ROTATE_REFRESH_TOKENS': False, 'BLACKLIST_AFTER_ROTATION': False, 'UPDATE_LAST_LOGIN': False, 'ALGORITHM': 'HS256', # 'SIGNING_KEY': SECRET_KEY, 'VERIFYING_KEY': None, 'AUDIENCE': None, 'ISSUER': None, 'JWK_URL': None, 'LEEWAY': 0, 'AUTH_HEADER_TYPES': ('Bearer',), 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', 'USER_ID_FIELD': 'id', 'USER_ID_CLAIM': 'user_id', 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule', 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), 'TOKEN_TYPE_CLAIM': 'token_type', 'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser', 'JTI_CLAIM': 'jti', 'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp', 'SLIDING_TOKEN_LIFETIME': timedelta(days=1), 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=5), } ROOT_URLCONF = 'backend.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'backend.wsgi.application' # Database # https://docs.djangoproject.com/en/4.1/ref/settings/#databases # DATABASES = { # 'default': { # 'ENGINE': 'django.db.backends.sqlite3', # 'NAME': BASE_DIR / 'db.sqlite3', # } # } DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'my_db', 'USER': 'postgres', 'PASSWORD': 'django101', 'HOST': 'localhost', 'PORT': '5432', } } # Password validation # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/4.1/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.1/howto/static-files/ STATIC_URL = 'static/' MEDIA_URL = '/images/' STATICFILES_DIRS = [ BASE_DIR / 'static' ] MEDIA_ROOT = 'static/images' # Default primary key field type # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.contrib.auth.models.BigAutoField' STRIPE_SECRET_KEY = "sk_test_51MAt6AJU3RVFqD4Tz8TCWE7PL3xNn5iYvKe7r9ZUKN6dIPOm2AcFgfbiHlU43H1tEgbMKG93o8KNduJr86UFWbzc00RmWxOLY2 " SITE_URL = "http://127.0.0.1:3000" CORS_ALLOW_ALL_ORIGINS = True
React项目配置(package.json)
"proxy": "http://127.0.0.1:8000/",
问题解决与优化方案
1. SITE_URL的必要性与Axios替代方案
- SITE_URL不能省略:它是Stripe完成支付后跳转回前端的回调地址,和前端的proxy配置无关。proxy仅负责转发前端发起的API请求到后端,不影响Stripe的外部跳转逻辑。
- 完全可以用Axios实现:当前直接跳转后端接口的方式不符合前后端分离的逻辑,改用Axios发送POST请求获取Stripe会话URL后再跳转,是更规范的实现方式。
2. 修复结账按钮跳转与Stripe表单不显示问题
步骤1:修改React购物车组件的结账逻辑
首先安装axios(如果未安装):npm install axios,然后在cartScreen.js中导入并修改checkoutHandler:
import axios from 'axios'; // ... 其他代码 const checkoutHandler = async () => { try { // 利用proxy转发,直接请求相对路径 const response = await axios.post('/create-checkout-session'); // 跳转到Stripe的结账页面 window.location.href = response.data.url; } catch (error) { console.error('创建结账会话失败:', error.response?.data?.error || error.message); } }; // ... 修改Button组件的事件绑定 <Button type="button" className="btn-block" disabled={cartItems.length === 0} onClick={checkoutHandler} // 替换action为onClick > Proceed to Checkout </Button>
步骤2:修改Django视图返回格式
将视图的redirect改为返回JSON数据,让前端自行处理跳转:
class StripeCheckoutView(APIView): def post(self, request): try: checkout_session = stripe.checkout.Session.create( line_items=[ { 'currency': 'usd', 'price': 'price_1MAtSLJU3RVFqD4TnNYWOxPO', 'quantity': 1, }, ], payment_method_types=['card'], mode='payment', success_url=f"{settings.SITE_URL}/?success=true&session_id={{CHECKOUT_SESSION_ID}}", cancel_url=f"{settings.SITE_URL}/?canceled=true", ) // 返回会话URL给前端 return Response({'url': checkout_session.url}, status=status.HTTP_200_OK) except Exception as e: // 返回具体错误信息便于调试 return Response( {'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR )
步骤3:修正URL路径与Stripe配置
- 前端请求路径改为
/create-checkout-session,对应后端urls中的配置,proxy会自动转发到后端。 - 检查Stripe的
price_1MAtSLJU3RVFqD4TnNYWOxPO是否为有效价格ID,确保其关联了已发布的产品。 - 移除settings.py中STRIPE_SECRET_KEY末尾的空格,避免认证失败。
3. 额外调试建议
- 打开浏览器控制台的Network标签,查看API请求的状态码和返回内容,定位请求失败原因。
- 检查Stripe Dashboard中的事件日志,查看结账会话的创建状态和错误信息。
内容的提问来源于stack exchange,提问作者Khaled
相关产品推荐
相关产品推荐

