yansongda/pay 支付 Provider 代码评审实战指南:七阶段检查清单、安全审查要点与标准报告模板

原创2026-10-04 13:43:021,424 阅读
文章标签:金融科技后端

yansongda/pay 支付 Provider 代码评审实战指南:七阶段检查清单、安全审查要点与标准报告模板

本指南源自 yansongda/pay 开源仓库中的 .agents/skills/pr-review-provider/SKILL.md,该文件沉淀自 Airwallex PR #1140 的真实 Review 经验。文章以该检查清单为骨架,结合仓库源码逐一印证每个检查项背后的实现事实,帮助你在为 yansongda/pay 新增或修改支付 Provider(如 Airwallex、Alipay、Wechat、Douyin、Unipay、Jsb、Paypal、Stripe)时,完成一次覆盖结构、规范、安全、架构、文档与测试的完整 Code Review。

在 yansongda/pay 这类以"插件管道"驱动的支付 SDK 中,新增一个支付 Provider 绝非只写几个支付方法那么简单:它牵涉插件按版本组织的目录结构、Provider 类与服务提供者的注册、快捷方式的命名、Trait 方法的下沉复用、Webhook 验签的安全性,以及文档和测试的同步更新。任何一环缺失,都可能在生产环境引入多租户配置错乱、签名伪造或回调解析失败等隐患。

读完本文,你将掌握一套可直接照做的七阶段评审流程(结构完整性 → 代码规范 → 安全性 → 架构一致性 → 官方文档对照 → 测试覆盖 → 文档完整性),识别 params 与 payload 混淆、null 空字段泄漏、Webhook 未验签等高风险问题,并能套用仓库内建的标准 Review 报告模板输出评审结论。


一、Review 规范(强制要求)

在执行任何评审动作之前,有两个强制规范必须遵守,它们直接关系到协作礼仪与报告的可追溯性。

1. 提交评论前必须征求用户许可

绝对禁止:未经用户明确允许,不得使用 gh pr comment 或其他方式向 PR 提交任何评论。

正确流程:

  1. 完成 review 分析;
  2. 向用户展示 review 报告内容;
  3. 询问用户:"是否需要我将此报告提交为 PR 评论?"
  4. 仅在用户明确确认后才执行提交。

2. 报告末尾必须注明使用的模型及审核状态

在 review 报告的结论部分之后,必须添加以下信息:

---
*Review by {模型名称} | {YYYY-MM-DD} | 经人工审核确认*

示例:

---
*Review by deepseek-v4-pro | 2026-05-07 | 经人工审核确认*

二、Review 流程总览

按以下阶段顺序审查,确保覆盖完整:

  1. Phase 1:Provider 结构完整性
  2. Phase 2:代码规范检查
  3. Phase 3:安全性检查(签名验证、加密解密)
  4. Phase 4:架构一致性(管道、Trait、Event)
  5. Phase 5:官方文档对照
  6. Phase 6:测试覆盖
  7. Phase 7:文档完整性

三、Phase 1:Provider 结构完整性

新增或修改 Provider 时,首先对照以下清单逐项检查,确认从插件到文档的完整链路都已就位。清单中的"位置"列给出了各组成部分在仓库中的标准落点:

# 检查项 位置 说明
1 插件 src/Plugin/{Provider}/V{n}/ 按版本组织
2 Provider 类 src/Provider/{Provider}.php 实现 ProviderInterface
3 服务提供者 src/Service/{Provider}ServiceProvider.php 服务注册
4 快捷方式 src/Shortcut/{Provider}/ {Method}Shortcut.php
5 Trait 方法 src/Traits/{Provider}Trait.php get{Provider}Url、verify{Provider}WebhookSign 等
6 Provider 注册 src/Pay.php 添加 {Provider}::class 和入口方法
7 异常常量 src/Exception/Exception.php PARAMS_{PROVIDER}_*、CONFIG_{PROVIDER}_*
8 测试 tests/ 与源码结构对应
9 文档 web/docs/v3/{provider}/ VitePress 文档
10 侧边栏/CHANGELOG web/.vitepress/sidebar/v3.js、CHANGELOG.md 更新配置

以 Stripe 为例,仓库中各个组成部分的落点与清单完全对应:

注意:src/Functions.php 已不存在,URL/签名等方法已下沉到 src/Traits/*Trait.php。评审时不要依据旧版目录结构找文件。


四、Phase 2:代码规范检查

基本规范

检查项 要求
declare(strict_types=1) 每个文件必须有
use 导入 除动态类名字符串/反射场景外,禁止直接写完整命名空间(如 \Yansongda\Pay\...)
多行条件 && / `

以 src/Plugin/Stripe/V1/AddRadarPlugin.php 为例,文件首行即 declare(strict_types=1);,所有 Yansongda\Pay\... 均通过 use 导入;仅当构造动态类名时才允许字符串形式的完整命名空间,例如 src/Provider/Stripe.php 中的 '\Yansongda\Pay\Shortcut\Stripe\\'.Str::studly($shortcut).'Shortcut',这正是"动态类名字符串"场景下的合法例外。

命名规范

类型 格式 示例
插件 {Action}Plugin.php PayPlugin、RefundPlugin
快捷方式 {Method}Shortcut.php WebShortcut、QueryShortcut
Provider {ProviderName}.php Paypal.php、Stripe.php
ServiceProvider {ProviderName}ServiceProvider.php PaypalServiceProvider.php
Trait {Provider}Trait.php WechatTrait、StripeTrait
命名空间 Yansongda\Pay\Plugin\{Provider}\V{n}\Pay\{Plugin} 版本号与 API 版本一致

日志与异常

  • 日志格式:<a href="https://link.gitcode.com/i/327a3619c1c917a754dcb7ed4dd69da1" target="_blank">Provider][V{n}][Category][Plugin],使用中文消息。例如 [CallbackPlugin 中的 Logger::debug('[Stripe][V1][Pay][CallbackPlugin] 插件开始装载', ...);
  • 异常常量:PARAMS_{PROVIDER}_*、CONFIG_{PROVIDER}_*;
  • 异常消息:中文,附带上下文参数。例如 StripeTrait 中配置缺失时抛出 InvalidConfigException(Exception::CONFIG_STRIPE_INVALID, '配置异常: 缺少 Stripe 配置 -- [webhook_secret]'),并把 headers、body 等上下文随异常携带。

五、Phase 3:安全性检查

安全性是支付 Provider 评审的重中之重,SKILL 将其细分为五个检查点,其中前两点直接决定多租户安全与报文质量。

1. params vs payload 混淆(高危)

高危:Artful::artful() 第二参数必须是 params(含 _config),不能是 payload。

// ❌ 错误 — payload 不含 _config,多租户必崩
$result = Artful::artful([...], $confirmPayload);

// ✅ 正确 — params 含 _config 租户标识
$result = Artful::artful([...], $confirmParams);

检查方法:搜索 Artful::artful( 调用,追踪第二参数来源。

这一检查项在仓库中有清晰的实现依据:src/Traits/ProviderConfigTrait.php 的 getTenant() 从 $params<a href="https://link.gitcode.com/i/3e24558333e3daabaebc04f7ef411290" target="_blank">'_config'] 读取租户标识(缺省为 'default'),getProviderConfig() 再用 "{$provider}.{$tenant}" 从容器配置中取出对应租户的配置对象并执行 validate()。如果第二参数误传成不含 _config 的 payload,租户解析将永远命中 default,多租户场景下必崩。这也是为什么 [StripeProvider 的 callback 方法 在调用 pay() 时显式传入 ['_request' => $request, '_params' => $params] 这一"params 形态"的数组。

2. 空值处理

检查最终发送的 body/query 是否包含不应出现的 null/空字段。

推荐方式:

  • 优先使用 filter_params() 函数(artful 库提供);
  • 嵌套数组场景补充 array_filter()。
// ✅ 优先方式 — filter_params
$body = http_build_query(filter_params($payload)->toArray());

// ✅ 嵌套场景 — array_filter
$rocket->mergePayload(array_filter([
    'application_context' => $payload->get('application_context'),
], static fn ($value) => !is_null($value)));

检查位置:AddRadarPlugin::getBody()、getQueryString()、业务 Plugin 的 mergePayload()。

仓库实现印证:src/Plugin/Stripe/V1/AddRadarPlugin.php 的 getBody() 与 getQueryString() 均使用 http_build_query(filter_params($payload)->toArray()),GET 请求自动过滤空参数拼进 query string,非 GET 请求则将过滤后的参数编码为 application/x-www-form-urlencoded 的 body——这正是文档所推荐写法的标准落地。

3. Webhook 签名验证

安全强制:所有 CallbackPlugin 必须验签。

各 Provider 的验签方法及必需配置字段对照表:

Provider Trait 方法 必需配置字段
Stripe StripeTrait::verifyStripeWebhookSign() webhook_secret
PayPal PaypalTrait::verifyPaypalWebhookSign() webhook_id + OAuth
微信 WechatTrait::verifyWechatSign() mch_secret_cert、wechat_public_cert_path(可预置或运行时拉取)
抖音 —(在 CallbackPlugin::verifySign() 中实现) mch_secret_token、mch_secret_salt
银联 UnipayTrait::verifyUnipaySign() unipay_public_cert_path
支付宝 AlipayTrait::verifyAlipaySign() alipay_public_cert_path

检查点:

  • CallbackPlugin 是否调用 Trait 提供或 Plugin 自身实现的签名验证方法;
  • 签名算法是否与官方文档一致(对照 @see 链接);
  • 配置缺失时抛异常;
  • 签名为空时抛异常;
  • 使用 hash_equals 防时序攻击。

仓库源码中的 Trait 验签方法均可逐一对号入座:AirwallexTrait::verifyAirwallexWebhookSign()、AlipayTrait::verifyAlipaySign()、DouyinTrait::verifyDouyinTradeSign()、JsbTrait::verifyJsbSign()、PaypalTrait::verifyPaypalWebhookSign()、StripeTrait::verifyStripeWebhookSign()、UnipayTrait::verifyUnipaySign() 均定义于 src/Traits/ 下。

以 Stripe 为例,verifyStripeWebhookSign() 完整实现了上述检查点:读取 webhook_secret 缺失即抛 CONFIG_STRIPE_INVALID;Stripe-Signature 头为空抛 SIGN_EMPTY;解析 t= 时间戳与 v1= 签名列表,时间戳与当前时间差超过 300 秒判定超时;随后计算 hash_hmac('sha256', $timestamp.'.'.$body, $webhookSecret) 并与每个候选签名做 hash_equals 常量时间比较,全部不匹配才抛 SIGN_ERROR。对应的 CallbackPlugin 在验签后还会对 body 做 JSON 合法性校验(JSON_ERROR_NONE !== json_last_error() 时抛 PARAMS_STRIPE_BODY_INVALID)。

4. 数组回调的处理

仅适用于 Stripe/Wechat/Paypal(构造 ServerRequest 并验签):

getCallbackParams() 处理逻辑:

输入类型 行为
['body' => ..., 'headers' => ...] 构造带 headers 的 ServerRequest,会验签
纯数组(无 headers) 构造无 headers 的 ServerRequest,验签时会抛 SIGN_EMPTY
ServerRequestInterface 直接使用,验签
null 从 ServerRequest::fromGlobals() 获取

结论:数组回调只有提供完整 headers + body 才能通过验签;否则验签阶段抛异常。

Alipay/Douyin/Unipay 不同:

  • getCallbackParams() 返回 Collection(从 query/parsedBody 取值);
  • 直接 merge 到 params,不构造 ServerRequest;
  • CallbackPlugin 从 params 中取值验签。

仓库实现印证:Stripe 的 getCallbackParams() 正是按此逻辑分支——带 body + headers 的数组构造 ServerRequest('POST', 'http://localhost', $headers, $body);纯数组构造无 headers 的请求(后续验签因 Stripe-Signature 头为空必然抛 SIGN_EMPTY);ServerRequestInterface 直接透传;null 则回退到 ServerRequest::fromGlobals()。

5. 加密资源解密(微信)

微信回调的 resource 字段需解密:

$body['resource'] = self::decryptWechatResource($body['resource'] ?? [], $config);

检查 CallbackPlugin 是否调用此方法(对应 WechatTrait::decryptWechatResource(),位于 src/Traits/WechatTrait.php)。


六、Phase 4:架构一致性

1. 插件管道骨架

通用骨架:

StartPlugin → [前置插件] → 业务插件 → [后置插件] → ParserPlugin

各 Provider 差异:

Provider 前置插件 后置插件
Stripe 无 AddRadarPlugin → ResponsePlugin
PayPal ObtainAccessTokenPlugin AddPayloadBodyPlugin → AddRadarPlugin → ResponsePlugin
微信 无 AddPayloadBodyPlugin → AddPayloadSignaturePlugin → AddRadarPlugin → VerifySignaturePlugin → ResponsePlugin
支付宝 无 FormatPayloadBizContentPlugin → AddPayloadSignaturePlugin → AddRadarPlugin → VerifySignaturePlugin → ResponsePlugin

注意:不同 Provider 管道差异较大,不要按固定模板审查。

仓库中的 Stripe 快捷方式 WebShortcut 即为该骨架的实证:StartPlugin → WebPlugin → AddRadarPlugin → ResponsePlugin → ParserPlugin,与表格中"Stripe 无前置插件、后置为 AddRadarPlugin → ResponsePlugin"完全一致。

2. mergeCommonPlugins 实现

Provider 类必须实现此方法,返回完整管道:

public function mergeCommonPlugins(array $plugins): array
{
    return array_merge(
        [StartPlugin::class, /* 前置插件 */],
        $plugins,
        [/* 后置插件 */, ParserPlugin::class],
    );
}

3. Trait 方法复用

新增 Provider 应复用 Trait 方法而非重新实现:

功能 Trait 方法
URL 构建 get{Provider}Url()
签名验证 verify{Provider}WebhookSign() / verify{Provider}Sign()
配置获取 ProviderConfigTrait::getProviderConfig()、getTenant()
加密解密 WechatTrait::decryptWechatResource()

例如 StripeTrait::getStripeUrl() 先调用 ProviderConfigTrait::getRadarUrl() 解析 _url/_sandbox_url/_service_url,缺失时抛 PARAMS_STRIPE_URL_MISSING(提示"你可能用错插件顺序,应该先使用业务插件"),再按模式拼上 Stripe::URL 中的 Base URL——这正是"URL 构建走 Trait"的落地样例。

4. Provider 常量定义

必须定义 URL 常量:

public const URL = [
    Pay::MODE_NORMAL => 'https://api.xxx.com/',
    Pay::MODE_SANDBOX => 'https://sandbox.api.xxx.com/',
    Pay::MODE_SERVICE => 'https://api.xxx.com/',
];

特殊常量(如微信):

  • AUTH_TAG_LENGTH_BYTE
  • MCH_SECRET_KEY_LENGTH_BYTE

仓库实证:src/Provider/Stripe.php 定义了 URL 常量,三种模式(Pay::MODE_NORMAL/MODE_SANDBOX/MODE_SERVICE,常量定义见 src/Pay.php)当前均指向 https://api.stripe.com。

5. Event 调用

callback 方法必须触发事件:

// Stripe/Wechat/Paypal(ServerRequestInterface)
Event::dispatch(new CallbackReceived('provider', clone $request, $params, null));

// Alipay/Douyin/Unipay/Jsb(Collection)
Event::dispatch(new CallbackReceived('provider', $request->all(), $params, null));

其他方法触发:

Event::dispatch(new MethodCalled('provider', __METHOD__, $order, null));

仓库实证:Stripe 的 callback() 在验签前先 Event::dispatch(new CallbackReceived(Pay::PROVIDER_STRIPE, clone $request, $params, null))(事件类见 src/Event/CallbackReceived.php),query()/cancel()/refund() 则触发 MethodCalled(src/Event/MethodCalled.php)。

6. PHPStan ignore 注释

Trait 静态方法调用需添加注释:

/* @phpstan-ignore-next-line */
self::verifyWechatSign(...);

这是 PHPStan 对 Trait 静态调用的已知限制,非代码质量问题。仓库中同样出现在 CallbackPlugin 的 self::verifyStripeWebhookSign(...) 调用之前。


七、Phase 5:官方文档对照

结合代码中的 @see 链接验证:

# 检查点
1 API 端点 URL 是否与官方一致
2 HTTP 方法(GET/POST)是否正确
3 认证 Header 名称、格式是否正确
4 请求/响应字段(必填/可选)是否正确
5 签名算法、拼接顺序是否与官方完全一致
6 Base URL(production/sandbox)是否正确

例如 Stripe 的 AddRadarPlugin::getHeaders() 构造 Authorization: Bearer {secret_key}、Accept: application/json,非 GET 请求追加 Content-Type: application/x-www-form-urlencoded,并支持通过 _headers 注入 Idempotency-Key、Stripe-Version 等自定义请求头覆盖默认值——这些细节都需要在评审时与 Stripe 官方 API 文档逐一核对。


八、Phase 6:测试覆盖

# 检查项
1 每个 Plugin 有对应测试
2 必填参数缺失的异常测试
3 可选参数缺失的边界测试
4 多租户场景(_config 参数)
5 HTTP client mock,禁止真实 API 调用
6 Callback 签名验证的正向/反向测试

仓库中 tests/Plugin/Stripe/V1/Pay/ 等测试目录与源码结构严格对应,CallbackPluginTest、AddRadarPluginTest 等即对应上述检查项 1;测试基类 tests/TestCase.php 与 phpunit.xml 提供了可离线运行的测试环境。


九、Phase 7:文档完整性

# 检查项
1 官方链接可访问且指向正确端点
2 示例代码使用原生 PHP,框架无关
3 侧边栏已更新
4 CHANGELOG 已更新

仓库内对应文档位于 web/docs/v3/stripe/、web/docs/v3/wechat/ 等,每个 Provider 均包含 pay.md、callback.md、query.md、refund.md 等章节;变更记录见 CHANGELOG.md。


十、常见误报:null-safe 操作符

评审中容易把以下写法误判为 bug:

$payload?->get('a', $payload->get('b'))

PHP 8.0 的 null-safe 实现了 full short-circuiting:当 $payload 为 null 时,右侧参数不会被求值。这不是 bug。

(依据:PHP RFC nullsafe_operator,仓库要求 PHP >= 8.0。)


十一、避免重复造轮子

检查新增功能是否在 yansongda/supports 或 yansongda/artful 中已有实现:

功能 已有实现
UUID 生成 Str::uuidV4()
字符串处理 Str::*
集合操作 Collection::*
空值过滤 filter_params() (artful)
HTTP 方法获取 get_radar_method() (artful)
Body 获取 get_radar_body() (artful)

仓库中 AddRadarPlugin 正是通过 use function Yansongda\Artful\filter_params; 等方式直接复用 artful 提供的工具函数,没有在业务代码里重复实现。


十二、Review 报告模板

以下为提交 review 时的标准报告格式,评审完成后按此结构整理输出:

# PR #{number} Code Review — {Provider} Provider

## 一、总览

| 维度 | 内容 |
|------|------|
| PR | #{number} — {title} |
| 涉及文件 | {n} 个 |
| Review 范围 | 全量代码 + 文档 + 测试 |
| 方法 | 逐文件审查 + 官方文档对照 + 跨 Provider 一致性比对 |

## 二、已确认 Bug(含证据链)

### BUG-{n}: {简要描述}

**严重级别**:{高/中高/中/低}

**位置**:`{file_path}` L{line}

**问题**:
{详细描述问题本质}

**证据链**:
1. 代码现状:`{相关代码片段}`
2. 预期行为:{应该怎样}
3. 实际行为:{实际会怎样}
4. 影响范围:{哪些场景会触发}

**修复建议**:
```php
// 修复方案

(重复以上格式,每个 Bug 单独一节)

三、已排除的误报

{误报描述}

结论:非 Bug

原因:{详细解释,附 RFC/文档链接}

四、风险项

# 风险 严重级别 文件 说明
RISK-{n} {风险名} {级别} {file} {说明}

五、改进建议

# 建议 优先级 文件 说明
SUG-{n} {建议名} {级别} {file} {说明}

六、官方文档对照验证

功能 代码中的 URL/算法 官方文档 是否一致
{功能名} {代码实现} 官方文档 ✅/❌

七、代码规范检查

检查项 状态 备注
declare(strict_types=1) ✅/❌
use 导入(无内联命名空间) ✅/❌
日志格式 [Provider][V{n}][Category][Plugin] ✅/❌
异常常量命名 ✅/❌
Plugin/Shortcut 命名规范 ✅/❌
测试覆盖 ✅/❌

八、汇总

类别 数量 详情
已确认 Bug {n} {简要列举}
已排除误报 {n} {简要列举}
风险项 {n} {简要列举}
改进建议 {n} {简要列举}

九、结论

{总体评价:代码质量、架构一致性、安全性等。明确给出 Approve / Request Changes 建议及原因。}


Review by {模型名称} | {YYYY-MM-DD} | 经人工审核确认


---

## 结语

至此,一套完整的支付 Provider 评审方法论已经清晰:从结构完整性、代码规范,到高危的多租户 `params` 安全、Webhook 验签,再到管道架构、测试与文档,每个阶段都有仓库源码(<a href="https://link.gitcode.com/i/fec5d6b6b4d3e5c27a76e737a6940c2e" target="_blank">src/</a>、<a href="https://link.gitcode.com/i/dcfb278663856c58c716b14250e7885e" target="_blank">tests/</a>、<a href="https://link.gitcode.com/i/176651921e6bef31a3361ea670cec3b3" target="_blank">web/docs/</a>)中的真实实现可作对照证据。评审者只需按七阶段顺序推进,将发现的问题填入标准报告模板,即可产出一份结构一致、证据链完整、可直接提交到 PR 的评审结论。这套清单同样适用于自研支付渠道的代码自检——在合入之前,先让自己的 Provider 通过这份检查表。
登录后查看全文
pay