Stripe 订单与退款查询:yansongda/pay 中 Pay::stripe()->query() 的用法与插件管道解析

原创2026-10-14 18:12:031,441 阅读
文章标签:后端金融科技

Stripe 订单与退款查询:yansongda/pay 中 Pay::stripe()->query() 的用法与插件管道解析

本篇围绕 Stripe 查询订单文档 展开,介绍在 yansongda/pay 中通过 Pay::stripe()->query() 查询 Stripe PaymentIntent 与退款记录两种订单状态的完整用法,并结合 QueryShortcut、QueryPlugin、AddRadarPlugin 等源码,讲清 _action 路由、请求拼装与响应校验的底层机制,帮助你既能正确调用查询接口,也能理解其插件化实现以便自行扩展任意 Stripe API。

注意:根据文档原文提示,Stripe 的实现由 AI 生成并经人工审核,但由于测试资源限制,尚未经过实际测试,请在充分验证后再用于生产环境。

方法签名与总体用法

query 是 Stripe Provider 直接暴露的查询入口,其签名为:

方法名 参数 返回值
query array $order Collection

调用入口位于 Stripe Provider:

public function query(array $order): Collection|Rocket
{
    Event::dispatch(new MethodCalled(Pay::PROVIDER_STRIPE, __METHOD__, $order, null));

    return $this->__call('query', [$order]);
}

从源码结构看,query() 会先派发 MethodCalled 事件(便于监听器记录调用行为),再通过 __call 把方法名 query 映射到快捷方式插件 \Yansongda\Pay\Shortcut\Stripe\QueryShortcut(见 Provider 的 __call 实现),最终返回一个封装了 API 响应 JSON 的 Collection 对象,可直接用 $result->get('status') 等方式取字段。

查询支付订单(PaymentIntent)

查询已创建的 PaymentIntent(支付意图)状态时,只需传入 payment_intent_id:

Pay::config($config);

$order = [
    'payment_intent_id' => 'pi_3OxxxxxxxxxxxxxxxxxxxxY', // Stripe PaymentIntent ID
    // '_action' => 'order', // 查询支付订单,默认
];

$result = Pay::stripe()->query($order);

订单配置参数与 Stripe 官方 API 无任何差别,兼容所有功能;除 payment_intent_id 外的官方请求参数都可以直接放入 $order,完整参数清单请参考 Stripe 官方 API「PaymentIntents — Retrieve」的「请求参数」一栏。

对应的底层业务插件是 QueryPlugin,核心逻辑为:

$paymentIntentId = $params['payment_intent_id'] ?? '';

if (empty($paymentIntentId)) {
    throw new InvalidParamsException(Exception::PARAMS_NECESSARY_PARAMS_MISSING, '参数异常: Stripe 查询 PaymentIntent,缺少 payment_intent_id 参数');
}

$rocket->mergePayload([
    '_method' => 'GET',
    '_url' => '/v1/payment_intents/'.$paymentIntentId,
]);

// PaymentIntent ID 已拼入 `_url`,需要从 payload 中剔除,避免残留在最终请求 query string 中被 Stripe 拒绝
$rocket->exceptPayload('payment_intent_id');

两个值得注意的实现细节:

  1. 必填校验前置:缺少 payment_intent_id 时直接抛出 InvalidParamsException,错误码为 Exception::PARAMS_NECESSARY_PARAMS_MISSING,这一点由 QueryPluginTest 中的 testMissingPaymentIntentId 用例明确验证;
  2. 参数剔除(exceptPayload):ID 拼进 URL 路径后会被从 payload 中移除,防止它以 query string 形式残留在最终请求中被 Stripe 拒绝。

查询退款订单

通过 _action 参数切换到退款查询模式:

Pay::config($config);

$order = [
    'refund_id' => 're_3OxxxxxxxxxxxxxxxxxxxxY', // Stripe 退款 ID
    '_action' => 'refund',
];

$result = Pay::stripe()->query($order);

同样,除 refund_id 外的官方请求参数都可以直接传入,完整参数请参考 Stripe 官方 API「Refunds — Retrieve」的「请求参数」一栏。

底层由 QueryRefundPlugin 拼装请求,逻辑与 PaymentIntent 查询对称:

$refundId = $params['refund_id'] ?? '';

if (empty($refundId)) {
    throw new InvalidParamsException(Exception::PARAMS_NECESSARY_PARAMS_MISSING, '参数异常: Stripe 查询退款,缺少 refund_id 参数');
}

$rocket->mergePayload([
    '_method' => 'GET',
    '_url' => '/v1/refunds/'.$refundId,
]);

$rocket->exceptPayload('refund_id');

QueryRefundPluginTest 验证了正常路径会生成 GET /v1/refunds/{id} 的请求,缺少 refund_id 时抛出参数缺失异常。

_action 路由机制

query() 支持一次入口查询两类资源,其分派逻辑在 QueryShortcut::getPlugins():

$action = $params['_action'] ?? StripeAction::QUERY_DEFAULT;

return match ($action) {
    StripeAction::QUERY_DEFAULT, StripeAction::QUERY_ORDER => $this->orderPlugins(),
    StripeAction::QUERY_REFUND => $this->refundPlugins(),
    default => throw new InvalidParamsException(
        Exception::PARAMS_SHORTCUT_ACTION_INVALID,
        '不支持的 _action ['.($params['_action'] ?? '').']',
    ),
};

_action 的合法取值定义在 StripeAction:

/* QUERY */
public const QUERY_DEFAULT = 'default';
public const QUERY_ORDER = 'order';
public const QUERY_REFUND = 'refund';

即不传 _action 时默认为 default(查 PaymentIntent);order 与 default 等价;refund 走退款查询;传入其他值(如 'foo')会抛出 PARAMS_SHORTCUT_ACTION_INVALID 异常——该行为在 QueryShortcutTest 的 testFoo、testDefault、testOrder、testRefund 四个用例中被完整覆盖,同时确认了三条插件链的精确顺序。

插件管道:一次 query 调用的完整链路

根据 QueryShortcutTest 断言的插件顺序,查询支付订单与查询退款订单分别执行以下管道:

阶段 插件 职责
1 StartPlugin 初始化 Rocket,把用户传入的 $order 参数装载进 payload
2 QueryPlugin / QueryRefundPlugin 校验必填 ID,拼装 _method(GET)与 _url(/v1/payment_intents/{id} 或 /v1/refunds/{id}),并剔除已拼入 URL 的 ID 字段
3 AddRadarPlugin 依据 payload 与配置构建最终 HTTP 请求(方法、URL、请求头、Body)
4 ResponsePlugin 校验响应状态码是否为 2xx
5 ParserPlugin 解析响应 JSON 为 Collection 返回给调用方

请求组装的核心是 AddRadarPlugin,它把 payload 中的 _method、_url 转换成一次真实的 GuzzleHttp\Psr7\Request:

$rocket->setRadar(new Request(
    get_radar_method($payload),
    self::getStripeUrl($config, $payload).$this->getQueryString($payload),
    $this->getHeaders($config, $payload),
    $this->getBody($payload),
));

与查询相关的几个关键行为:

  • 鉴权方式:getHeaders() 使用 Stripe 要求的 Bearer Token 认证,Authorization: Bearer {secret_key},同时携带 Accept: application/json 与 User-Agent: yansongda/pay-v3;GET 请求不设置 Content-Type,也不携带 Body(getBody() 对 GET 直接返回空字符串)。
  • 自定义请求头注入:源码注释明确支持通过订单参数中的 _headers 注入自定义请求头(如 Idempotency-Key、Stripe-Version),且可覆盖默认值。因此查询时可以传入 '_headers' => ['Stripe-Version' => '2025-01-01'] 之类的固定版本请求头。
  • URL 前缀补全:StripeTrait::getStripeUrl() 会把业务插件写入的相对路径 /v1/payment_intents/pi_xxx 拼接到 Provider 的基地址上。从 Stripe Provider 的 URL 常量 看,normal、sandbox、service 三种 mode 的基地址均为 https://api.stripe.com——即 Stripe 的环境切换实际由 sk_test_ / sk_live_ 前缀的密钥本身决定,mode 在此处不影响请求域名。若 payload 中 _url 缺失(例如插件顺序用错),会抛出带明确提示的 InvalidParamsException:「你可能用错插件顺序,应该先使用 业务插件」。

响应侧由 ResponsePlugin 兜底校验:只要 HTTP 状态码落在 200 之外(4xx 参数错误、401/403 密钥错误、404 资源不存在等),都会抛出 InvalidResponseException(错误码 RESPONSE_CODE_WRONG),提示「Stripe 返回状态码异常,请检查参数是否错误」。这意味着你不需要手工判断响应码,查询失败会以异常形式抛出,异常信息中携带原始响应体($rocket->getDestination())可供排查。

前置配置要求

调用 query() 之前需要先完成 Pay::config() 注册 Stripe 配置。从 StripeConfig 的 validateRequired() 及 StripeConfigTest 看,唯一必填项是 secret_key,缺失时抛出 配置异常: 缺少 Stripe 配置 -- [secret_key]:

use Yansongda\Pay\Config\StripeConfig;
use Yansongda\Pay\Pay;

$config = new StripeConfig([
    'secret_key' => 'sk_test_xxx', // 必填:可写为 sk_test_(测试环境)或 sk_live_(生产环境)
    // 'webhook_secret' => 'whsec_xxx', // 选填:仅回调验签需要
    // 'notify_url' => 'https://example.com/stripe/notify', // 选填
    // 'success_url' => 'https://example.com/stripe/success', // 选填
    // 'cancel_url' => 'https://example.com/stripe/cancel',   // 选填
    // 'mode' => PayMode::NORMAL, // 选填:normal/sandbox/service,默认 normal
]);

Pay::config($config);

查询场景本身不依赖 webhook_secret、notify_url 等选填项,它们分别服务于回调验签与 Checkout 跳转流程;secret_key 会以 Bearer Token 形式随每次查询请求发出,请务必按环境区分测试/生产密钥并妥善保管。

异常与边界行为汇总

结合源码与测试用例,query() 的失败场景可归纳为:

场景 异常 触发位置
缺少 payment_intent_id(订单查询) InvalidParamsException(PARAMS_NECESSARY_PARAMS_MISSING) QueryPlugin
缺少 refund_id(退款查询) InvalidParamsException(PARAMS_NECESSARY_PARAMS_MISSING) QueryRefundPlugin
_action 取值非法(非 default/order/refund) InvalidParamsException(PARAMS_SHORTCUT_ACTION_INVALID) QueryShortcut
HTTP 状态码非 2xx(401/403/404/4xx 等) InvalidResponseException(RESPONSE_CODE_WRONG) ResponsePlugin
payload 中缺少 _url(插件顺序错误) InvalidParamsException(PARAMS_STRIPE_URL_MISSING) StripeTrait

另需注意:Stripe Provider 不支持 close API(close() 直接抛异常提示「Stripe 不支持 close API」),如需关闭支付单请使用 Stripe 的 cancel 入口(见 cancel.md 对应插件 CancelPlugin)。

延伸阅读:用插件自行组装任意 Stripe API

得益于 pay 的基础架构和插件机制,你可以不依赖快捷方式、直接组合内置插件调用 Stripe 的任何 API。查询 PaymentIntent 的等价写法是(见 Stripe 更多方便的插件文档):

use Yansongda\Artful\Plugin\ParserPlugin;
use Yansongda\Artful\Plugin\StartPlugin;
use Yansongda\Pay\Plugin\Stripe\V1\AddRadarPlugin;
use Yansongda\Pay\Plugin\Stripe\V1\Pay\QueryPlugin;
use Yansongda\Pay\Plugin\Stripe\V1\ResponsePlugin;

Pay::config($config);

$params = [
    'payment_intent_id' => 'pi_3OxxxxxxxxxxxxxxxxxxxxY',
];

$allPlugins = [StartPlugin::class, QueryPlugin::class, AddRadarPlugin::class, ResponsePlugin::class, ParserPlugin::class];

$result = Pay::stripe()->pay($allPlugins, $params);

内置的支付产品插件一览(完整清单见 all.md):

场景 插件
创建 PaymentIntent \Yansongda\Pay\Plugin\Stripe\V1\Pay\PayPlugin
查询 PaymentIntent \Yansongda\Pay\Plugin\Stripe\V1\Pay\QueryPlugin
取消 PaymentIntent \Yansongda\Pay\Plugin\Stripe\V1\Pay\CancelPlugin
退款申请 \Yansongda\Pay\Plugin\Stripe\V1\Pay\RefundPlugin
查询退款 \Yansongda\Pay\Plugin\Stripe\V1\Pay\QueryRefundPlugin
创建 Checkout Session \Yansongda\Pay\Plugin\Stripe\V1\Pay\WebPlugin

掌握上述插件顺序与各自职责后,你就可以以同样的方式组合出查询 Balance、订阅等其他任意 Stripe 接口的调用链。

登录后查看全文
pay