| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 |
- <?php
- namespace app\common\service;
- class WechatPaymentService extends BasePaymentService
- {
- // 微信支付API地址
- protected $apiUrl = 'https://api.mch.weixin.qq.com';
- // 配置参数
- protected $config = [];
- /**
- * 初始化微信支付配置
- */
- protected function initConfig()
- {
- $this->config = [
- 'appid' => $this->payConfig['app_id'],
- 'mch_id' => $this->payConfig['mch_id'],
- 'key' => $this->payConfig['key'],
- 'notify_url' => $this->payConfig['notify_url'] ?: url('/api/invitation/payment/wechatNotify', [], true, true)->build(),
- 'cert_path' => $this->getCertPath('cert'),
- 'key_path' => $this->getCertPath('key'),
- ];
- if (empty($this->config['appid']) || empty($this->config['mch_id']) || empty($this->config['key'])) {
- throw new \Exception('微信支付配置不完整');
- }
- }
- /**
- * 获取证书路径
- */
- protected function getCertPath($type)
- {
- // 可以从数据库或文件系统获取证书
- // 这里简化处理,实际需要根据业务获取
- return '';
- }
- /**
- * 生成支付二维码
- */
- public function createQrCode($orderNo, $amount, $subject, $body = '')
- {
- try {
- // 构建请求参数
- $params = [
- 'appid' => $this->config['appid'],
- 'mch_id' => $this->config['mch_id'],
- 'nonce_str' => $this->generateNonceStr(),
- 'body' => mb_substr($subject, 0, 127, 'UTF-8'), // 商品描述,128字节限制
- 'out_trade_no' => $orderNo,
- 'total_fee' => intval($amount * 100), // 单位:分
- 'spbill_create_ip' => request()->ip(),
- 'notify_url' => $this->config['notify_url'],
- 'trade_type' => 'NATIVE', // 扫码支付
- 'product_id' => $orderNo,
- ];
- // 生成签名
- $params['sign'] = $this->generateSign($params);
- // 调用微信统一下单接口
- $result = $this->postXmlCurl($this->apiUrl . '/pay/unifiedorder', $this->arrayToXml($params));
- // 解析结果
- $response = $this->xmlToArray($result);
- if ($response['return_code'] == 'SUCCESS' && $response['result_code'] == 'SUCCESS') {
- return [
- 'qr_code_url' => $response['code_url'],
- 'qr_code_content' => $response['code_url'],
- 'prepay_id' => $response['prepay_id'],
- ];
- } else {
- $errorMsg = $response['return_msg'] ?? $response['err_code_des'] ?? '微信支付下单失败';
- throw new \Exception($errorMsg);
- }
- } catch (\Throwable $e) {
- $this->log('生成微信支付二维码失败', [
- 'order_no' => $orderNo,
- 'error' => $e->getMessage()
- ], 'error');
- throw $e;
- }
- }
- /**
- * 处理支付回调
- */
- public function handleNotify($input)
- {
- try {
- // 解析XML数据
- $data = $this->xmlToArray($input);
- // 验证签名
- if (!$this->verifySign($data, $data['sign'])) {
- throw new \Exception('签名验证失败');
- }
- // 检查支付状态
- if ($data['return_code'] != 'SUCCESS' || $data['result_code'] != 'SUCCESS') {
- throw new \Exception('支付失败: ' . ($data['return_msg'] ?? '未知错误'));
- }
- return [
- 'order_no' => $data['out_trade_no'],
- 'trade_no' => $data['transaction_id'],
- 'pay_amount' => $data['total_fee'] / 100,
- 'pay_time' => $this->formatWxTime($data['time_end']),
- 'raw_data' => $data,
- ];
- } catch (\Throwable $e) {
- $this->log('处理微信支付回调失败', [
- 'input' => $input,
- 'error' => $e->getMessage()
- ], 'error');
- throw $e;
- }
- }
- /**
- * 查询支付订单
- */
- public function queryOrder($orderNo)
- {
- try {
- $params = [
- 'appid' => $this->config['appid'],
- 'mch_id' => $this->config['mch_id'],
- 'out_trade_no' => $orderNo,
- 'nonce_str' => $this->generateNonceStr(),
- ];
- $params['sign'] = $this->generateSign($params);
- $result = $this->postXmlCurl($this->apiUrl . '/pay/orderquery', $this->arrayToXml($params));
- $response = $this->xmlToArray($result);
- if ($response['return_code'] == 'SUCCESS' && $response['result_code'] == 'SUCCESS') {
- $status = $response['trade_state'];
- $payStatus = 0; // 待支付
- if ($status == 'SUCCESS') {
- $payStatus = 1; // 已支付
- } elseif (in_array($status, ['CLOSED', 'REVOKED', 'PAYERROR', 'USERPAYING'])) {
- $payStatus = 2; // 支付失败
- }
- return [
- 'order_no' => $response['out_trade_no'],
- 'trade_no' => $response['transaction_id'],
- 'pay_status' => $payStatus,
- 'pay_amount' => $response['total_fee'] / 100,
- 'pay_time' => isset($response['time_end']) ? $this->formatWxTime($response['time_end']) : null,
- 'trade_state' => $status,
- ];
- }
- throw new \Exception('查询订单失败: ' . ($response['return_msg'] ?? '未知错误'));
- } catch (\Throwable $e) {
- $this->log('查询微信支付订单失败', [
- 'order_no' => $orderNo,
- 'error' => $e->getMessage()
- ], 'error');
- throw $e;
- }
- }
- /**
- * 验证签名
- */
- protected function verifySign($data, $sign)
- {
- $localSign = $this->generateSign($data);
- return $localSign === $sign;
- }
- /**
- * 生成签名
- */
- protected function generateSign($data)
- {
- // 过滤空值和sign参数
- $filterData = array_filter($data, function($value, $key) {
- return $value !== '' && $value !== null && $key !== 'sign';
- }, ARRAY_FILTER_USE_BOTH);
- // 按参数名ASCII码从小到大排序
- ksort($filterData);
- // 拼接字符串
- $string = '';
- foreach ($filterData as $key => $value) {
- $string .= $key . '=' . $value . '&';
- }
- $string .= 'key=' . $this->config['key'];
- // MD5加密并转为大写
- return strtoupper(md5($string));
- }
- /**
- * 生成随机字符串
- */
- protected function generateNonceStr($length = 32)
- {
- $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
- $str = '';
- for ($i = 0; $i < $length; $i++) {
- $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
- }
- return $str;
- }
- /**
- * 格式化微信支付时间
- */
- protected function formatWxTime($timeStr)
- {
- if (empty($timeStr)) return null;
- // 微信时间格式:yyyyMMddHHmmss
- $year = substr($timeStr, 0, 4);
- $month = substr($timeStr, 4, 2);
- $day = substr($timeStr, 6, 2);
- $hour = substr($timeStr, 8, 2);
- $minute = substr($timeStr, 10, 2);
- $second = substr($timeStr, 12, 2);
- return "{$year}-{$month}-{$day} {$hour}:{$minute}:{$second}";
- }
- /**
- * 发送XML请求
- */
- private function postXmlCurl($url, $xml, $useCert = false, $timeout = 30)
- {
- $ch = curl_init();
- curl_setopt($ch, CURLOPT_URL, $url);
- curl_setopt($ch, CURLOPT_POST, true);
- curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
- // 设置SSL
- curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
- curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
- // 如果需要证书
- if ($useCert) {
- curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
- curl_setopt($ch, CURLOPT_SSLCERT, $this->config['cert_path']);
- curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
- curl_setopt($ch, CURLOPT_SSLKEY, $this->config['key_path']);
- }
- $response = curl_exec($ch);
- if (curl_errno($ch)) {
- curl_close($ch);
- throw new \Exception('CURL错误: ' . curl_error($ch));
- }
- curl_close($ch);
- return $response;
- }
- /**
- * 数组转XML
- */
- private function arrayToXml($arr)
- {
- $xml = '<xml>';
- foreach ($arr as $key => $val) {
- if (is_numeric($val)) {
- $xml .= "<{$key}>{$val}</{$key}>";
- } else {
- $xml .= "<{$key}><![CDATA[{$val}]]></{$key}>";
- }
- }
- $xml .= '</xml>';
- return $xml;
- }
- /**
- * XML转数组(兼容PHP 8.0+)
- */
- private function xmlToArray($xml)
- {
- if (empty($xml)) {
- return [];
- }
- try {
- // 方法1:使用DOMDocument(推荐,兼容性好)
- $dom = new \DOMDocument();
- // 针对PHP版本的兼容处理
- if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
- // PHP 8.0+ 不需要禁用实体加载器,直接加载
- $dom->loadXML($xml, LIBXML_NOENT | LIBXML_NOCDATA);
- } else {
- // PHP 8.0以下需要禁用实体加载器
- $previous = libxml_disable_entity_loader(true);
- $dom->loadXML($xml, LIBXML_NOENT | LIBXML_NOCDATA);
- libxml_disable_entity_loader($previous);
- }
- // 转换为数组
- $data = $this->domToArray($dom->documentElement);
- return $data ?: [];
- } catch (\Exception $e) {
- $this->log('XML解析失败', [
- 'xml' => substr($xml, 0, 500), // 只记录前500字符
- 'error' => $e->getMessage()
- ], 'error');
- // 备用解析方法
- return $this->fallbackXmlToArray($xml);
- }
- }
- /**
- * DOMElement转换为数组
- */
- private function domToArray($node)
- {
- $output = [];
- switch ($node->nodeType) {
- case XML_CDATA_SECTION_NODE:
- case XML_TEXT_NODE:
- $output = trim($node->textContent);
- break;
- case XML_ELEMENT_NODE:
- for ($i = 0; $i < $node->childNodes->length; $i++) {
- $child = $node->childNodes->item($i);
- $value = $this->domToArray($child);
- if (isset($child->tagName)) {
- $tag = $child->tagName;
- if (!isset($output[$tag])) {
- $output[$tag] = [];
- }
- if (is_array($value) && isset($value['@value'])) {
- $output[$tag][] = $value['@value'];
- } else {
- $output[$tag][] = $value;
- }
- // 如果是单一元素,不要数组形式
- if (count($output[$tag]) === 1 && !is_array($output[$tag][0])) {
- $output[$tag] = $output[$tag][0];
- }
- } elseif ($value !== '') {
- $output = $value;
- }
- }
- if ($node->attributes->length) {
- $attributes = [];
- foreach ($node->attributes as $attrName => $attrNode) {
- $attributes[$attrName] = (string)$attrNode->value;
- }
- if (!empty($output)) {
- $output['@attributes'] = $attributes;
- } else {
- $output = ['@value' => '', '@attributes' => $attributes];
- }
- }
- break;
- }
- // 如果是数组且只有一个元素,直接返回那个元素
- if (is_array($output) && count($output) == 1 && isset($output[0])) {
- $output = $output[0];
- }
- return $output;
- }
- /**
- * 备用XML解析方法(原来的方法,带版本检查)
- */
- private function fallbackXmlToArray($xml)
- {
- try {
- // 版本检查,PHP 8.0以上不调用已废弃函数
- if (version_compare(PHP_VERSION, '8.0.0', '<')) {
- libxml_disable_entity_loader(true);
- }
- $data = json_decode(json_encode(simplexml_load_string(
- $xml,
- 'SimpleXMLElement',
- LIBXML_NOCDATA | LIBXML_NOENT
- )), true);
- return $data ?: [];
- } catch (\Exception $e) {
- $this->log('备用XML解析失败', [
- 'error' => $e->getMessage()
- ], 'error');
- return [];
- }
- }
- }
|