--- name: pr-review-provider description: "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." --- # PR Review Checklist — Payment Provider yansongda/pay 新增/修改 Provider 时的 Code Review 专用检查清单。基于 Airwallex PR #1140 review 经验沉淀。 ## Review 规范(强制) ### 1. 提交评论前必须征求用户许可 **绝对禁止**:未经用户明确允许,不得使用 `gh pr comment` 或其他方式向 PR 提交任何评论。 **正确流程**: 1. 完成 review 分析 2. 向用户展示 review 报告内容 3. 询问用户:"是否需要我将此报告提交为 PR 评论?" 4. 仅在用户明确确认后才执行提交 ### 2. 报告末尾必须注明使用的模型及审核状态 在 review 报告的结论部分之后,必须添加以下信息: ```markdown --- *Review by {模型名称} | {YYYY-MM-DD} | 经人工审核确认* ``` 示例: ```markdown --- *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 结构完整性 对照以下清单逐项检查: | # | 检查项 | 位置 | 说明 | |---|--------|------|------| | 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`。 --- ## Phase 2: 代码规范检查 ### 基本规范 | 检查项 | 要求 | |--------|------| | `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}_*` - 异常消息:中文,附带上下文参数 --- ## Phase 3: 安全性检查 ### 1. params vs payload 混淆 **高危**:`Artful::artful()` 第二参数必须是 `params`(含 `_config`),不能是 `payload`。 ```php // ❌ 错误 — payload 不含 _config,多租户必崩 $result = Artful::artful([...], $confirmPayload); // ✅ 正确 — params 含 _config 租户标识 $result = Artful::artful([...], $confirmParams); ``` **检查方法**:搜索 `Artful::artful(` 调用,追踪第二参数来源。 ### 2. 空值处理 检查最终发送的 body/query 是否包含不应出现的 `null`/空字段。 **推荐方式**: - 优先使用 `filter_params()` 函数(artful 库提供) - 嵌套数组场景补充 `array_filter()` ```php // ✅ 优先方式 — 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()`。 ### 3. Webhook 签名验证 **安全强制**:所有 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` | **检查点**: - CallbackPlugin 是否调用 Trait 提供或 Plugin 自身实现的签名验证方法 - 签名算法是否与官方文档一致(对照 `@see` 链接) - 配置缺失时抛异常 - 签名为空时抛异常 - 使用 `hash_equals` 防时序攻击 ### 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 中取值验签 ### 5. 加密资源解密(微信) 微信回调的 `resource` 字段需解密: ```php $body['resource'] = self::decryptWechatResource($body['resource'] ?? [], $config); ``` 检查 `CallbackPlugin` 是否调用此方法。 --- ## 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 管道差异较大,不要按固定模板审查。 ### 2. mergeCommonPlugins 实现 Provider 类必须实现此方法,返回完整管道: ```php 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()` | ### 4. Provider 常量定义 必须定义 `URL` 常量: ```php 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` ### 5. Event 调用 callback 方法必须触发事件: ```php // 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)); ``` 其他方法触发: ```php Event::dispatch(new MethodCalled('provider', __METHOD__, $order, null)); ``` ### 6. PHPStan ignore 注释 Trait 静态方法调用需添加注释: ```php /* @phpstan-ignore-next-line */ self::verifyWechatSign(...); ``` 这是 PHPStan 对 Trait 静态调用的已知限制,非代码质量问题。 --- ## Phase 5: 官方文档对照 结合代码中的 `@see` 链接验证: | # | 检查点 | |---|--------| | 1 | API 端点 URL 是否与官方一致 | | 2 | HTTP 方法(GET/POST)是否正确 | | 3 | 认证 Header 名称、格式是否正确 | | 4 | 请求/响应字段(必填/可选)是否正确 | | 5 | 签名算法、拼接顺序是否与官方完全一致 | | 6 | Base URL(production/sandbox)是否正确 | --- ## Phase 6: 测试覆盖 | # | 检查项 | |---|--------| | 1 | 每个 Plugin 有对应测试 | | 2 | 必填参数缺失的异常测试 | | 3 | 可选参数缺失的边界测试 | | 4 | 多租户场景(`_config` 参数) | | 5 | HTTP client mock,禁止真实 API 调用 | | 6 | Callback 签名验证的正向/反向测试 | --- ## Phase 7: 文档完整性 | # | 检查项 | |---|--------| | 1 | 官方链接可访问且指向正确端点 | | 2 | 示例代码使用原生 PHP,框架无关 | | 3 | 侧边栏已更新 | | 4 | CHANGELOG 已更新 | --- ## 常见误报 ### null-safe 操作符 ```php $payload?->get('a', $payload->get('b')) ``` PHP 8.0 的 null-safe 实现了 **full short-circuiting**:当 `$payload` 为 `null` 时,右侧参数不会被求值。这不是 bug。 参考:[PHP RFC nullsafe_operator](https://wiki.php.net/rfc/nullsafe_operator) --- ## 避免重复造轮子 检查新增功能是否在 `yansongda/supports` 或 `yansongda/artful` 中已有实现: | 功能 | 已有实现 | |------|----------| | UUID 生成 | `Str::uuidV4()` | | 字符串处理 | `Str::*` | | 集合操作 | `Collection::*` | | 空值过滤 | `filter_params()` (artful) | | HTTP 方法获取 | `get_radar_method()` (artful) | | Body 获取 | `get_radar_body()` (artful) | ## Review 报告模板 以下为提交 review 时的标准报告格式: ````markdown # 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/算法 | 官方文档 | 是否一致 | |------|-------------------|----------|----------| | {功能名} | `{代码实现}` | [官方文档]({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} | 经人工审核确认* ````