You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Stripe订阅支付后无法更新为ACTIVE状态问题排查

问题描述

我用Java+React开发固定价格订阅功能,参照Stripe官方固定价格订阅示例开发,但遇到问题:客户完成卡片支付后,Stripe后台显示支付已处理,但订阅仍处于INCOMPLETE状态。已经添加了ON_SUBSCRIPTION设置,问题依旧。相关代码如下:

Java后端代码

@GetMapping("/stripe/prices")
public Response<PricesResponse> prices() {

    RequestOptions options = RequestOptions.builder()
            .setApiKey(apiKey)
            .build();

    // search customer
    try {
        CustomerSearchParams params =
                CustomerSearchParams.builder()
                        .setQuery("name:'aaa bbb'")
                        .build();

        CustomerSearchResult customers = Customer.search(params, options);

        List<String> names = customers.getData()
                .stream()
                .map(Customer::getName)
                .toList();

        logger.info(String.valueOf(names));

    } catch (StripeException e) {
        throw new RuntimeException(e);
    }

    // create customer
    Customer customer;
    try {
        String customerName = "Jenny Rosen " + System.currentTimeMillis();
        System.out.println(customerName);

        CustomerCreateParams params =
                CustomerCreateParams.builder()
                        .setName(customerName)
                        .setEmail("jennyrosen@example.com")
                        .build();
        customer = Customer.create(params, options);
    } catch (StripeException e) {
        throw new RuntimeException(e);
    }

    // get prices
    PriceCollection prices = new PriceCollection();
    try {
        PriceListParams params = PriceListParams
                .builder()
                .build();

        prices = Price.list(params, options);

    } catch (StripeException e) {
        throw new RuntimeException(e);
    }

    // create subscriptions
    Subscription subscription;
    try {
        SubscriptionCreateParams.PaymentSettings paymentSettings =
                SubscriptionCreateParams.PaymentSettings
                        .builder()
                        .setSaveDefaultPaymentMethod(SubscriptionCreateParams.PaymentSettings.SaveDefaultPaymentMethod.ON_SUBSCRIPTION)
                        .build();

        SubscriptionCreateParams subCreateParams = SubscriptionCreateParams
                .builder()
                .setCustomer(customer.getId())
                .addItem(
                        SubscriptionCreateParams
                                .Item.builder()
                                .setPrice(prices.getData().get(0).getId())
                                .build()
                )
                .setPaymentSettings(paymentSettings)
                .setPaymentBehavior(SubscriptionCreateParams.PaymentBehavior.DEFAULT_INCOMPLETE)
                .addAllExpand(Arrays.asList("latest_invoice.payment_intent"))
                .build();

        subscription = Subscription.create(subCreateParams, options);
    } catch (StripeException e) {
        throw new RuntimeException(e);
    }

    // create payment intent
    PaymentIntent paymentIntent;
    try {
        PaymentIntentCreateParams params =
                PaymentIntentCreateParams.builder()
                        .setCustomer(customer.getId())
                        .setAmount(prices.getData().get(0).getUnitAmount())
                        .setCurrency("usd")
                        .build();

        paymentIntent = PaymentIntent.create(params, options);

    } catch (StripeException e) {
        throw new RuntimeException(e);
    }
    
    return new Response<>(new PricesResponse(publishableKey,  paymentIntent.getClientSecret(), prices.getData()
            .stream()
            .map(price -> new PriceResponse(price.getId(), price.getNickname(), price.getUnitAmount()))
            .toList()));
}

React前端代码

价格选择页面

const Prices = () => {
const navigate = useNavigate();
const [prices, setPrices] = useState([]);
const [clientSecret, setClientSecret] = useState("");

useEffect(() => {
    doRestCall('/stripe/prices', 'get', null, null,
        (response) => {
            setPrices(response.body.prices)
            setClientSecret(response.body.clientSecret)
        })
}, [])

function toCheckout() {
    navigate('/checkout', {
        state: {
            clientSecret
        }
    })
}

return (
    <div>
        <h1>Select a plan</h1>

        <div className="price-list">
            {prices.map((price) => {
                return (
                    <div key={price.id}>
                        <h3>{price.name}</h3>

                        <p>
                            ${price.amount / 100} / month
                        </p>

                        <button onClick={() => toCheckout()}>
                            Select
                        </button>

                    </div>
                )
            })}
        </div>
    </div>
)}

结账页面

const Checkout = () => {

const {
    state: {
        clientSecret,
    }
} = useLocation();

const stripe = useStripe();
const elements = useElements();

const [name, setName] = useState('Jenny Rosen');
const [messages, setMessages] = useState('');

const navigate = useNavigate();

const handleSubmit = async (e) => {
    e.preventDefault();

    const cardElement = elements.getElement(CardElement);

    const { error } = await stripe.confirmCardPayment(clientSecret, {
        payment_method: {
            card: cardElement,
            billing_details: {
                name: name,
            }
        }
    });

    if(error) {
        // show error and collect new card details.
        setMessages(error.message);
        return;
    }

    navigate('/complete', {
        state: {
            clientSecret
        }
    });

};

return (<>
    <h1>Subscribe</h1>

    <p>
        Try the successful test card: <span>4242424242424242</span>.
    </p>

    <p>
        Try the test card that requires SCA: <span>4000002500003155</span>.
    </p>

    <p>
        Use any <i>future</i> expiry date, CVC,5 digit postal code
    </p>

    <hr/>
    <form onSubmit={handleSubmit}>
        <label>
            Full name
            <input type="text" id="name" value={name} onChange={(e) => setName(e.target.value)}/>
        </label>

        <CardElement/>

        <button>
            Subscribe
        </button>

        <div>{messages}</div>
    </form>

</>);

另外,关于Stripe订阅流程中提到的Invoice,是否需要手动处理?我在Java示例中看到/invoice-preview接口,但未找到React端的调用位置。


问题分析与解决方案

1. 订阅状态INCOMPLETE的核心原因

你当前代码的关键错误是:手动创建了独立的PaymentIntent,而非使用订阅创建时自动生成的Invoice关联的PaymentIntent。

当设置PaymentBehavior.DEFAULT_INCOMPLETE创建订阅时,Stripe会自动生成对应Invoice和关联的PaymentIntent,你需要将这个PaymentIntent的clientSecret返回给前端,而非重新创建一个无关的支付意图。前端完成独立PaymentIntent的支付后,Stripe无法将其关联到订阅的Invoice,导致订阅一直处于INCOMPLETE状态。

修复步骤

修改Java后端代码,移除手动创建PaymentIntent的逻辑,直接从订阅中提取关联的PaymentIntent的clientSecret:

// 移除手动创建PaymentIntent的代码块
/*
PaymentIntent paymentIntent;
try {
    PaymentIntentCreateParams params =
            PaymentIntentCreateParams.builder()
                    .setCustomer(customer.getId())
                    .setAmount(prices.getData().get(0).getUnitAmount())
                    .setCurrency("usd")
                    .build();
    paymentIntent = PaymentIntent.create(params, options);
} catch (StripeException e) {
    throw new RuntimeException(e);
}
*/

// 从订阅中提取关联的PaymentIntent的clientSecret
String clientSecret = null;
Invoice latestInvoice = subscription.getLatestInvoice();
if (latestInvoice != null && latestInvoice.getPaymentIntent() != null) {
    clientSecret = latestInvoice.getPaymentIntent().getClientSecret();
}

// 返回使用该clientSecret
return new Response<>(new PricesResponse(publishableKey, clientSecret, prices.getData()
        .stream()
        .map(price -> new PriceResponse(price.getId(), price.getNickname(), price.getUnitAmount()))
        .toList()));

前端代码无需修改,确保接收的是订阅关联的PaymentIntent的clientSecret即可。

2. Invoice相关疑问

  • 是否需要手动处理Invoice?
    固定价格订阅的常规流程中,Stripe会自动处理Invoice的生成、付款尝试和状态更新,无需手动干预。仅当出现付款失败(如卡片过期、余额不足)时,需通过Webhook监听invoice.payment_failed事件,提醒用户更新支付方式。

  • 关于/invoice-preview接口
    该接口用于在用户选择订阅方案后预览即将生成的Invoice(如显示税费、折扣后的最终金额),属于可选体验优化步骤。官方示例未在React端调用,是因为这不是流程必需环节。若需添加,可在用户选择价格后调用后端/invoice-preview接口,传入Customer ID和Price ID,后端调用Stripe的Invoice.create()并设置preview=true生成预览,再将数据返回前端展示。


内容的提问来源于stack exchange,提问作者Alex

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 14:53:15