From ca18f905d1bdeeb858c13a0dabe175e9e85b8bdc Mon Sep 17 00:00:00 2001 From: Anyon Date: Fri, 8 May 2026 00:29:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(alipay):=20=E5=A4=8D=E7=94=A8=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E5=AE=9D=E7=BD=91=E5=85=B3=E7=AD=BE=E5=90=8D=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E6=9E=84=E9=80=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 抽取 buildGatewayParams 统一开放平台网关公共参数、扩展参数和签名生成。 - 电脑网站支付跳转地址复用同一套签名参数,避免 request/page 逻辑重复。 - 同步补齐支付宝平台与支付客户端的中文 PHPDoc。 --- src/Platform/Alipay/PaymentClient.php | 37 +++++--- src/Platform/Alipay/PlatformClient.php | 114 ++++++++++++++++++++----- 2 files changed, 117 insertions(+), 34 deletions(-) diff --git a/src/Platform/Alipay/PaymentClient.php b/src/Platform/Alipay/PaymentClient.php index 1f070c9..6f0428d 100644 --- a/src/Platform/Alipay/PaymentClient.php +++ b/src/Platform/Alipay/PaymentClient.php @@ -2,19 +2,33 @@ declare(strict_types=1); +/** + * 支付宝支付客户端。 + */ + namespace We\Platform\Alipay; use GuzzleHttp\ClientInterface; use We\Config\AlipayPaymentConfig; +/** + * 支付宝支付客户端。 + * + * 在支付宝开放平台网关能力之上提供电脑网站支付与交易退款快捷方法。 + */ final class PaymentClient extends PlatformClient { + /** + * 创建支付宝支付客户端。 + */ public function __construct(AlipayPaymentConfig $config, ?ClientInterface $http = null) { parent::__construct($config, $http); } /** + * 调用支付宝交易退款接口 `alipay.trade.refund`。 + * * @param array $bizContent * @param array $extra * @return array @@ -25,29 +39,20 @@ final class PaymentClient extends PlatformClient } /** + * 生成电脑网站支付接口 `alipay.trade.page.pay` 跳转地址。 + * * @param array $bizContent */ public function page(array $bizContent, array $extra = []): string { - $params = [ - 'app_id' => $this->config->appid, - 'method' => 'alipay.trade.page.pay', - 'format' => $this->config->format, - 'charset' => $this->config->charset, - 'sign_type' => $this->config->signType, - 'timestamp' => date('Y-m-d H:i:s'), - 'version' => $this->config->version, - 'biz_content' => json_encode($bizContent, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}', - ]; - foreach ($extra as $key => $value) { - $params[(string)$key] = is_scalar($value) ? (string)$value : json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - } - $params['sign'] = $this->sign($params); + $params = $this->buildGatewayParams('alipay.trade.page.pay', $bizContent, $extra); return $this->config->gateway . '?' . http_build_query($params); } /** + * 通用支付调用入口;特殊操作名用于电脑网站支付和退款快捷调用。 + * * @param array $params * @param array $options * @return array @@ -66,6 +71,8 @@ final class PaymentClient extends PlatformClient } /** + * 按 POST 语义调用支付宝支付接口。 + * * @param array $params * @param array $options * @return array @@ -76,6 +83,8 @@ final class PaymentClient extends PlatformClient } /** + * 按 GET 语义调用支付宝支付接口。 + * * @param array $params * @param array $options * @return array diff --git a/src/Platform/Alipay/PlatformClient.php b/src/Platform/Alipay/PlatformClient.php index 913a5af..64787d1 100644 --- a/src/Platform/Alipay/PlatformClient.php +++ b/src/Platform/Alipay/PlatformClient.php @@ -2,6 +2,10 @@ declare(strict_types=1); +/** + * 支付宝开放平台客户端。 + */ + namespace We\Platform\Alipay; use GuzzleHttp\Client as GuzzleClient; @@ -9,10 +13,18 @@ use GuzzleHttp\ClientInterface; use We\Config\AlipayPlatformConfig; use We\Exception\WechatException; +/** + * 支付宝开放平台客户端。 + * + * 负责组装支付宝开放平台网关公共参数、生成 RSA/RSA2 签名、验证同步响应和异步通知签名。 + */ class PlatformClient { protected ClientInterface $http; + /** + * 创建支付宝开放平台客户端并初始化网关 HTTP 客户端。 + */ public function __construct( protected readonly AlipayPlatformConfig $config, ?ClientInterface $http = null, @@ -20,23 +32,16 @@ class PlatformClient $this->http = $http ?? new GuzzleClient(['timeout' => 20.0]); } - /** @param array $bizContent @param array $extra @return array */ + /** + * 调用支付宝开放平台网关接口。 + * + * @param array $bizContent + * @param array $extra + * @return array + */ public function request(string $apiMethod, array $bizContent = [], array $extra = []): array { - $params = [ - 'app_id' => $this->config->appid, - 'method' => $apiMethod, - 'format' => $this->config->format, - 'charset' => $this->config->charset, - 'sign_type' => $this->config->signType, - 'timestamp' => date('Y-m-d H:i:s'), - 'version' => $this->config->version, - 'biz_content' => json_encode($bizContent, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}', - ]; - foreach ($extra as $key => $value) { - $params[(string)$key] = is_scalar($value) ? (string)$value : json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - } - $params['sign'] = $this->sign($params); + $params = $this->buildGatewayParams($apiMethod, $bizContent, $extra); $response = $this->http->request('POST', $this->config->gateway, [ 'form_params' => $params, 'headers' => ['Accept' => 'application/json'], @@ -60,7 +65,7 @@ class PlatformClient } /** - * 验证支付宝异步通知签名;业务处理回调前应先调用该方法。 + * 验证支付宝异步通知签名;业务处理通知前应先完成验签。 * * @param array $params 支付宝通知完整参数,包含 sign/sign_type。 */ @@ -70,7 +75,7 @@ class PlatformClient } /** - * 验证支付宝参数签名;通知验签会排除 sign 与 sign_type,其他参数按字典序拼接。 + * 验证支付宝参数签名;按开放平台规则排除 sign/sign_type 后排序拼接待验签内容。 * * @param array $params */ @@ -87,6 +92,9 @@ class PlatformClient return $this->verifySignature($this->buildSignContent($params, true), $sign); } + /** + * 生成支付宝开放平台网页授权地址。 + */ public function auth(string $redirectUri, string $scope = 'auth_user', string $state = ''): string { return 'https://openauth.alipay.com/oauth2/publicAppAuthorize.htm?' . http_build_query([ @@ -97,7 +105,11 @@ class PlatformClient ]); } - /** @return array */ + /** + * 解密支付宝小程序等场景返回的 AES 加密数据。 + * + * @return array + */ public function decrypt(string $encryptedData, string $sessionKey, string $iv): array { $plain = openssl_decrypt( @@ -119,6 +131,8 @@ class PlatformClient } /** + * 通用网关调用入口;特殊操作名用于授权地址生成和数据解密。 + * * @param array $params * @param array $options * @return array @@ -145,6 +159,8 @@ class PlatformClient } /** + * 按 POST 语义调用支付宝开放平台接口。 + * * @param array $params * @param array $options * @return array @@ -155,6 +171,8 @@ class PlatformClient } /** + * 按 GET 语义调用支付宝开放平台接口。 + * * @param array $params * @param array $options * @return array @@ -164,7 +182,11 @@ class PlatformClient return $this->call($uriOrPath, $params, 'GET', $options); } - /** @param array $params */ + /** + * 使用应用私钥对支付宝网关请求参数生成签名。 + * + * @param array $params + */ protected function sign(array $params): string { $data = $this->buildSignContent($params); @@ -182,6 +204,36 @@ class PlatformClient return base64_encode($signature); } + /** + * 组装支付宝开放平台网关公共参数并附加签名。 + * + * @param array $bizContent + * @param array $extra + * @return array + */ + protected function buildGatewayParams(string $apiMethod, array $bizContent = [], array $extra = []): array + { + $params = [ + 'app_id' => $this->config->appid, + 'method' => $apiMethod, + 'format' => $this->config->format, + 'charset' => $this->config->charset, + 'sign_type' => $this->config->signType, + 'timestamp' => date('Y-m-d H:i:s'), + 'version' => $this->config->version, + 'biz_content' => json_encode($bizContent, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}', + ]; + foreach ($extra as $key => $value) { + $params[(string)$key] = $this->gatewayValue($value); + } + $params['sign'] = $this->sign($params); + + return $params; + } + + /** + * 使用支付宝公钥校验网关同步响应签名。 + */ private function assertResponseSignature(string $body, string $node, string $sign): void { if ($sign === '') { @@ -195,6 +247,8 @@ class PlatformClient } /** + * 按支付宝开放平台规则排序并拼接待签名字符串。 + * * @param array $params */ private function buildSignContent(array $params, bool $skipSignType = false): string @@ -205,12 +259,23 @@ class PlatformClient if ($key === 'sign' || ($skipSignType && $key === 'sign_type') || $value === null || $value === '') { continue; } - $pairs[] = $key . '=' . (is_scalar($value) ? (string)$value : (json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '')); + $pairs[] = $key . '=' . $this->gatewayValue($value); } return implode('&', $pairs); } + /** + * 将网关扩展参数规范化为支付宝表单字符串。 + */ + private function gatewayValue(mixed $value): string + { + return is_scalar($value) ? (string)$value : (json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: ''); + } + + /** + * 使用支付宝公钥验证 RSA/RSA2 签名。 + */ private function verifySignature(string $source, string $signature): bool { $publicKey = $this->normalizePublicKey($this->config->alipayPublicKey); @@ -227,16 +292,25 @@ class PlatformClient return openssl_verify($source, $decoded, $resource, $algo) === 1; } + /** + * 将应用私钥内容规范化为 PEM 格式。 + */ private function normalizePrivateKey(string $privateKey): string { return str_contains($privateKey, 'BEGIN') ? $privateKey : "-----BEGIN PRIVATE KEY-----\n" . chunk_split($privateKey, 64, "\n") . "-----END PRIVATE KEY-----"; } + /** + * 将支付宝公钥内容规范化为 PEM 格式。 + */ private function normalizePublicKey(string $publicKey): string { return str_contains($publicKey, 'BEGIN') ? $publicKey : "-----BEGIN PUBLIC KEY-----\n" . chunk_split($publicKey, 64, "\n") . "-----END PUBLIC KEY-----"; } + /** + * 从支付宝网关原始 JSON 响应中提取用于验签的响应节点。 + */ private function extractJsonValue(string $json, string $key): string { if (preg_match('/"' . preg_quote($key, '/') . '"\s*:\s*/', $json, $match, PREG_OFFSET_CAPTURE) !== 1) {