merchant = $merchant; } public function createNativeTransaction(string $outTradeNo, string $description, int $amountFen, string $notifyUrl): array { $body = $this->buildPayBody($outTradeNo, $description, $amountFen, $notifyUrl); $resp = $this->request('POST', '/v3/pay/transactions/native', $body); $data = json_decode($resp['body'], true) ?: []; if ($resp['status'] >= 200 && $resp['status'] < 300) { return $data; } $message = $data['message'] ?? ('微信支付下单失败 HTTP ' . $resp['status']); throw new \RuntimeException($message); } public function createJsapiTransaction(string $outTradeNo, string $description, int $amountFen, string $notifyUrl, string $openid): array { $body = $this->buildJsapiPayBody($outTradeNo, $description, $amountFen, $notifyUrl, $openid); $resp = $this->request('POST', '/v3/pay/transactions/jsapi', $body); $data = json_decode($resp['body'], true) ?: []; if ($resp['status'] >= 200 && $resp['status'] < 300) { return $data; } $message = $data['message'] ?? ('微信支付下单失败 HTTP ' . $resp['status']); throw new \RuntimeException($message); } public function buildJsapiPayParams(string $appId, string $prepayId): array { $mchId = (string)$this->merchant->mch_id; $serialNo = (string)($this->merchant->serial_no ?? ''); $privateKeyPem = (string)($this->merchant->private_key_pem ?? ''); if ($privateKeyPem === '') { $privateKeyPem = $this->loadKeyPemFromFile(); } if ($mchId === '' || $serialNo === '' || $privateKeyPem === '') { throw new \RuntimeException('微信支付密钥未配置(mch_id/serial_no/private_key_pem)'); } $timeStamp = (string)time(); $nonceStr = bin2hex(random_bytes(16)); $package = 'prepay_id=' . $prepayId; $signStr = $appId . "\n" . $timeStamp . "\n" . $nonceStr . "\n" . $package . "\n"; $privateKey = openssl_pkey_get_private($privateKeyPem); if (!$privateKey) { throw new \RuntimeException('私钥格式错误'); } openssl_sign($signStr, $signature, $privateKey, OPENSSL_ALGO_SHA256); return [ 'appId' => $appId, 'timeStamp' => $timeStamp, 'nonceStr' => $nonceStr, 'package' => $package, 'signType' => 'RSA', 'paySign' => base64_encode($signature), ]; } public function verifyPlatformSignature(string $timestamp, string $nonce, string $body, string $signature): bool { $certPath = getenv('WECHATPAY_PLATFORM_CERT_PATH') ?: ''; if ($certPath === '' || !file_exists($certPath)) { throw new \RuntimeException('平台证书未配置'); } $cert = file_get_contents($certPath); $publicKey = openssl_pkey_get_public($cert); if (!$publicKey) { throw new \RuntimeException('平台证书读取失败'); } $message = $timestamp . "\n" . $nonce . "\n" . $body . "\n"; $ok = openssl_verify($message, base64_decode($signature), $publicKey, OPENSSL_ALGO_SHA256); return $ok === 1; } public function decryptNotifyResource(array $resource, string $apiV3Key): array { $ciphertext = (string)($resource['ciphertext'] ?? ''); $nonce = (string)($resource['nonce'] ?? ''); $aad = (string)($resource['associated_data'] ?? ''); if ($ciphertext === '' || $nonce === '') { throw new \RuntimeException('回调报文不完整'); } $cipherRaw = base64_decode($ciphertext); $tag = substr($cipherRaw, -16); $data = substr($cipherRaw, 0, -16); $plain = openssl_decrypt($data, 'aes-256-gcm', $apiV3Key, OPENSSL_RAW_DATA, $nonce, $tag, $aad); if ($plain === false) { throw new \RuntimeException('回调报文解密失败'); } return json_decode($plain, true) ?: []; } private function buildPayBody(string $outTradeNo, string $description, int $amountFen, string $notifyUrl): array { $mode = (string)($this->merchant->mode ?? 'direct'); $mchId = (string)$this->merchant->mch_id; $appId = (string)($this->merchant->app_id ?? ''); $subMchId = (string)($this->merchant->sub_mch_id ?? ''); $subAppId = (string)($this->merchant->sub_app_id ?? ''); if ($mode === 'direct') { if ($appId === '') { throw new \RuntimeException('商户 AppID 未配置'); } return [ 'appid' => $appId, 'mchid' => $mchId, 'description' => $description, 'out_trade_no' => $outTradeNo, 'notify_url' => $notifyUrl, 'amount' => ['total' => $amountFen, 'currency' => 'CNY'], ]; } if ($mode === 'service_provider') { if ($appId === '' || $subMchId === '') { throw new \RuntimeException('服务商模式配置不完整'); } $body = [ 'sp_appid' => $appId, 'sp_mchid' => $mchId, 'sub_mchid' => $subMchId, 'description' => $description, 'out_trade_no' => $outTradeNo, 'notify_url' => $notifyUrl, 'amount' => ['total' => $amountFen, 'currency' => 'CNY'], ]; if ($subAppId !== '') { $body['sub_appid'] = $subAppId; } return $body; } throw new \RuntimeException('第三方支付模式未配置下单方式'); } private function buildJsapiPayBody(string $outTradeNo, string $description, int $amountFen, string $notifyUrl, string $openid): array { $openid = trim($openid); if ($openid === '') { throw new \RuntimeException('openid 不能为空'); } $mode = (string)($this->merchant->mode ?? 'direct'); $mchId = (string)$this->merchant->mch_id; $appId = (string)($this->merchant->app_id ?? ''); $subMchId = (string)($this->merchant->sub_mch_id ?? ''); $subAppId = (string)($this->merchant->sub_app_id ?? ''); if ($mode === 'direct') { if ($appId === '') { throw new \RuntimeException('商户 AppID 未配置'); } return [ 'appid' => $appId, 'mchid' => $mchId, 'description' => $description, 'out_trade_no' => $outTradeNo, 'notify_url' => $notifyUrl, 'amount' => ['total' => $amountFen, 'currency' => 'CNY'], 'payer' => ['openid' => $openid], ]; } if ($mode === 'service_provider') { if ($appId === '' || $subMchId === '') { throw new \RuntimeException('服务商模式配置不完整'); } $body = [ 'sp_appid' => $appId, 'sp_mchid' => $mchId, 'sub_mchid' => $subMchId, 'description' => $description, 'out_trade_no' => $outTradeNo, 'notify_url' => $notifyUrl, 'amount' => ['total' => $amountFen, 'currency' => 'CNY'], 'payer' => [], ]; if ($subAppId !== '') { $body['sub_appid'] = $subAppId; $body['payer']['sub_openid'] = $openid; } else { $body['payer']['sp_openid'] = $openid; } return $body; } throw new \RuntimeException('第三方支付模式未配置下单方式'); } private function request(string $method, string $path, array $body): array { $mchId = (string)$this->merchant->mch_id; $serialNo = (string)($this->merchant->serial_no ?? ''); $privateKeyPem = (string)($this->merchant->private_key_pem ?? ''); if ($privateKeyPem === '') { $privateKeyPem = $this->loadKeyPemFromFile(); } if ($mchId === '' || $serialNo === '' || $privateKeyPem === '') { throw new \RuntimeException('微信支付密钥未配置(mch_id/serial_no/private_key_pem)'); } $bodyJson = json_encode($body, JSON_UNESCAPED_UNICODE); $timestamp = (string)time(); $nonceStr = bin2hex(random_bytes(16)); $signStr = $method . "\n" . $path . "\n" . $timestamp . "\n" . $nonceStr . "\n" . $bodyJson . "\n"; $privateKey = openssl_pkey_get_private($privateKeyPem); if (!$privateKey) { throw new \RuntimeException('私钥格式错误'); } openssl_sign($signStr, $signature, $privateKey, OPENSSL_ALGO_SHA256); $signature = base64_encode($signature); $authorization = sprintf( 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",timestamp="%s",serial_no="%s",signature="%s"', $mchId, $nonceStr, $timestamp, $serialNo, $signature ); $ch = curl_init('https://api.mch.weixin.qq.com' . $path); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Accept: application/json', 'Content-Type: application/json', 'Authorization: ' . $authorization, 'User-Agent: anxinyan-webman', ]); curl_setopt($ch, CURLOPT_POSTFIELDS, $bodyJson); $respBody = curl_exec($ch); $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($respBody === false) { $err = curl_error($ch); curl_close($ch); throw new \RuntimeException('微信支付请求失败: ' . $err); } curl_close($ch); return [ 'status' => $status, 'body' => $respBody, ]; } private function loadKeyPemFromFile(): string { $path = (string)($this->merchant->apiclient_key_path ?? ''); if ($path === '') return ''; $real = realpath($path); $base = realpath(runtime_path() . '/wechatpay/merchants'); if (!$real || !$base) return ''; if (strpos($real, $base) !== 0) return ''; if (!is_file($real)) return ''; $pem = file_get_contents($real); return is_string($pem) ? $pem : ''; } }