Use when reviewing PRs that add or modify a payment Provider in yansongda/pay - covers plugin pipeline, multi-tenant safety, signature verification, docs, and naming conventions.
yansongda/pay 新增/修改 Provider 时的 Code Review 专用检查清单。基于 Airwallex PR #1140 review 经验沉淀。
按以下阶段顺序审查,确保覆盖完整:
对照以下清单逐项检查:
| # | 检查项 | 位置 | 说明 |
|---|---|---|---|
| 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 | 更新配置 |
注意:src/Functions.php 已不存在,URL/签名等方法已下沉到 src/Traits/*Trait.php。
| 检查项 | 要求 |
|---|---|
declare(strict_types=1) | 每个文件必须有 |
use 导入 | 除动态类名字符串/反射场景外,禁止直接写完整命名空间(如 \Yansongda\Pay\...) |
| 多行条件 | && / ` |
| 类型 | 格式 | 示例 |
|---|---|---|
| 插件 | {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 版本一致 |
[Provider][V{n}][Category][Plugin],使用中文消息PARAMS_{PROVIDER}_*、CONFIG_{PROVIDER}_*高危:Artful::artful() 第二参数必须是 params(含 _config),不能是 payload。
// ❌ 错误 — payload 不含 _config,多租户必崩
$result = Artful::artful([...], $confirmPayload);
// ✅ 正确 — params 含 _config 租户标识
$result = Artful::artful([...], $confirmParams);
检查方法:搜索 Artful::artful( 调用,追踪第二参数来源。
检查最终发送的 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()。
安全强制:所有 CallbackPlugin 必须验签。
| 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 |
检查点:
@see 链接)hash_equals 防时序攻击仅适用于 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 取值)ServerRequest微信回调的 resource 字段需解密:
$body['resource'] = self::decryptWechatResource($body['resource'] ?? [], $config);
检查 CallbackPlugin 是否调用此方法。
通用骨架:
StartPlugin → [前置插件] → 业务插件 → [后置插件] → ParserPlugin
各 Provider 差异:
| Provider | 前置插件 | 后置插件 |
|---|---|---|
| Stripe | 无 | AddRadarPlugin → ResponsePlugin |
| PayPal | ObtainAccessTokenPlugin | AddPayloadBodyPlugin → AddRadarPlugin → ResponsePlugin |
| 微信 | 无 | AddPayloadBodyPlugin → AddPayloadSignaturePlugin → AddRadarPlugin → VerifySignaturePlugin → ResponsePlugin |
| 支付宝 | 无 | FormatPayloadBizContentPlugin → AddPayloadSignaturePlugin → AddRadarPlugin → VerifySignaturePlugin → ResponsePlugin |
注意:不同 Provider 管道差异较大,不要按固定模板审查。
Provider 类必须实现此方法,返回完整管道:
public function mergeCommonPlugins(array $plugins): array
{
return array_merge(
[StartPlugin::class, /* 前置插件 */],
$plugins,
[/* 后置插件 */, ParserPlugin::class],
);
}
新增 Provider 应复用 Trait 方法而非重新实现:
| 功能 | Trait 方法 |
|---|---|
| URL 构建 | get{Provider}Url() |
| 签名验证 | verify{Provider}WebhookSign() / verify{Provider}Sign() |
| 配置获取 | ProviderConfigTrait::getProviderConfig()、getTenant() |
| 加密解密 | WechatTrait::decryptWechatResource() |
必须定义 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_BYTEMCH_SECRET_KEY_LENGTH_BYTEcallback 方法必须触发事件:
// 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));
Trait 静态方法调用需添加注释:
/* @phpstan-ignore-next-line */