// +---------------------------------------------------------------------- declare (strict_types=1); namespace app\common\library\wechat\payment; use WeChatPay\Builder; use WeChatPay\Crypto\AesGcm; use WeChatPay\ClientDecoratorInterface; use GuzzleHttp\Middleware; use GuzzleHttp\Exception\RequestException; use Psr\Http\Message\ResponseInterface; use cores\exception\BaseException; /** * 下载「微信支付平台证书」 * PlatformCertDown class * @package app\common\library\wechat */ class PlatformCertDown { // 微信支付API网关 private const DEFAULT_BASE_URI = 'https://api.mch.weixin.qq.com/'; // 配置参数 private array $opts = []; /** * 构造方法 * PlatformCertDown constructor. * @param array $opts * @throws BaseException */ public function __construct(array $opts) { $this->opts = $opts + [ // 读取公钥中的序列号 'serialno' => $this->serialno($opts['publicKey']) ]; } /** * 执行下载 */ public function run(): void { // 执行下载 $this->job($this->opts); } /** * 读取公钥中的序列号 * @return mixed * @throws BaseException */ public function getPlatformCertSerial() { $outputDir = $this->opts['output'] ?? \sys_get_temp_dir(); return $this->serialno($outputDir . \DIRECTORY_SEPARATOR . $this->opts['fileName']); } /** * 读取公钥中的序列号 * @param string $publicKey * @return mixed * @throws BaseException */ private function serialno(string $publicKey) { $content = file_get_contents($publicKey); $plaintext = !empty($content) ? openssl_x509_parse($content) : false; empty($plaintext) && throwError('证书文件(CERT)不正确'); return $plaintext['serialNumberHex']; } /** * @param array $opts * * @return void */ private function job(array $opts): void { static $certs = ['any' => null]; $outputDir = $opts['output'] ?? \sys_get_temp_dir(); $apiv3Key = (string)$opts['key']; $instance = Builder::factory([ 'mchid' => $opts['mchid'], 'serial' => $opts['serialno'], 'privateKey' => \file_get_contents((string)$opts['privatekey']), 'certs' => &$certs, 'base_uri' => (string)($opts['baseuri'] ?? self::DEFAULT_BASE_URI), ]); /** @var \GuzzleHttp\HandlerStack $stack */ $stack = $instance->getDriver()->select(ClientDecoratorInterface::JSON_BASED)->getConfig('handler'); // The response middle stacks were executed one by one on `FILO` order. $stack->after('verifier', Middleware::mapResponse(self::certsInjector($apiv3Key, $certs)), 'injector'); $stack->before('verifier', Middleware::mapResponse( self::certsRecorder((string)$outputDir, $opts['fileName'], $certs)), 'recorder' ); $instance->chain('v3/certificates')->getAsync( // ['debug' => true] )->otherwise(static function ($exception) { if ($exception instanceof RequestException && $exception->hasResponse()) { /** @var ResponseInterface $response */ $response = $exception->getResponse(); throwError((string)$response->getBody()); } throwError($exception->getMessage()); })->wait(); } /** * 在`verifier`执行之前, 解密平台证书 * * @param string $apiv3Key * @param array $certs * * @return callable(ResponseInterface) */ private static function certsInjector(string $apiv3Key, array &$certs): callable { return static function (ResponseInterface $response) use ($apiv3Key, &$certs): ResponseInterface { $body = (string)$response->getBody(); /** @var object{data:array} $json */ $json = \json_decode($body); $data = \is_object($json) && isset($json->data) && \is_array($json->data) ? $json->data : []; \array_map(static function ($row) use ($apiv3Key, &$certs) { $cert = $row->encrypt_certificate; try { $certs[$row->serial_no] = AesGcm::decrypt($cert->ciphertext, $apiv3Key, $cert->nonce, $cert->associated_data); } catch (\Throwable $e) { throwError('支付密钥(APIKEY) 或 证书文件(KEY)不正确,请重新输入'); } }, $data); return $response; }; } /** * 将平台证书写入硬盘 * * @param string $outputDir * @param string fileName * @param array $certs * * @return callable(ResponseInterface) */ private static function certsRecorder(string $outputDir, string $fileName, array &$certs): callable { return static function (ResponseInterface $response) use ($outputDir, $fileName, &$certs): ResponseInterface { $body = (string)$response->getBody(); /** @var object{data:array} $json */ $json = \json_decode($body); $data = \is_object($json) && isset($json->data) && \is_array($json->data) ? $json->data : []; \array_walk($data, static function ($row, $index, $certs) use ($outputDir, $fileName) { $serialNo = $row->serial_no; $outpath = $outputDir . \DIRECTORY_SEPARATOR . $fileName; // self::prompt( // 'Certificate #' . $index . ' {', // ' Serial Number: ' . $serialNo, // ' Not Before: ' . (new \DateTime($row->effective_time))->format('Y-m-d H:i:s'), // ' Not After: ' . (new \DateTime($row->expire_time))->format('Y-m-d H:i:s'), // ' Saved to: ' . $outpath, // ' Content:', $certs[$serialNo] ?? '', // '}' // ); \file_put_contents($outpath, $certs[$serialNo]); }, $certs); return $response; }; } /** * 输出信息 * @param string $messages */ private static function prompt(...$messages): void { \array_walk($messages, static function (string $message): void { \printf('%s%s', $message, \PHP_EOL); }); } }