From e51f6c65442315bca87a2fca88487613656a330f Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Mon, 1 Dec 2025 11:36:51 +0800 Subject: [PATCH] =?UTF-8?q?chore(addon):=20=E6=B7=BB=E5=8A=A0=E7=BA=BF?= =?UTF-8?q?=E4=B8=8B=E6=94=AF=E4=BB=98=E5=8F=8A=E5=8D=8E=E4=B8=BA=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E5=9F=BA=E6=9C=AC=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/addon/huaweipay/config/diy_view.php | 33 ++ src/addon/huaweipay/config/event.php | 45 ++ src/addon/huaweipay/config/info.php | 15 + src/addon/huaweipay/config/menu_shop.php | 18 + .../huaweipay/data/sdk/HuaweiPayClient.php | 291 +++++++++++ src/addon/huaweipay/event/AuthcodePay.php | 26 + src/addon/huaweipay/event/Pay.php | 28 + src/addon/huaweipay/event/PayClose.php | 26 + src/addon/huaweipay/event/PayNotify.php | 26 + src/addon/huaweipay/event/PayOrderQuery.php | 26 + src/addon/huaweipay/event/PayRefund.php | 26 + src/addon/huaweipay/event/PayTransfer.php | 26 + src/addon/huaweipay/event/PayType.php | 44 ++ src/addon/huaweipay/event/TransferType.php | 25 + src/addon/huaweipay/icon.png | Bin 0 -> 21351 bytes src/addon/huaweipay/model/Config.php | 79 +++ src/addon/huaweipay/model/Pay.php | 252 +++++++++ src/addon/huaweipay/shop/controller/Pay.php | 82 +++ src/addon/huaweipay/shop/view/pay/config.html | 219 ++++++++ src/addon/offlinepay/api/controller/Pay.php | 71 +++ src/addon/offlinepay/config/diy_view.php | 30 ++ src/addon/offlinepay/config/event.php | 30 ++ src/addon/offlinepay/config/info.php | 12 + src/addon/offlinepay/config/menu_shop.php | 49 ++ src/addon/offlinepay/event/Install.php | 17 + .../event/MessageOfflinepayAuditRefuse.php | 16 + .../event/MessageOfflinepayWaitAudit.php | 16 + src/addon/offlinepay/event/Pay.php | 24 + src/addon/offlinepay/event/PayClose.php | 27 + src/addon/offlinepay/event/PayRefund.php | 24 + src/addon/offlinepay/event/PayType.php | 44 ++ src/addon/offlinepay/event/UnInstall.php | 17 + src/addon/offlinepay/icon.png | Bin 0 -> 946 bytes src/addon/offlinepay/model/Config.php | 71 +++ src/addon/offlinepay/model/Pay.php | 489 ++++++++++++++++++ src/addon/offlinepay/shop/controller/Pay.php | 172 ++++++ .../offlinepay/shop/view/pay/config.html | 193 +++++++ src/addon/offlinepay/shop/view/pay/lists.html | 356 +++++++++++++ src/addon/offlinepay/shop/view/pay/pay.html | 73 +++ src/app/common.php | 232 ++++++++- 40 files changed, 3238 insertions(+), 12 deletions(-) create mode 100644 src/addon/huaweipay/config/diy_view.php create mode 100644 src/addon/huaweipay/config/event.php create mode 100644 src/addon/huaweipay/config/info.php create mode 100644 src/addon/huaweipay/config/menu_shop.php create mode 100644 src/addon/huaweipay/data/sdk/HuaweiPayClient.php create mode 100644 src/addon/huaweipay/event/AuthcodePay.php create mode 100644 src/addon/huaweipay/event/Pay.php create mode 100644 src/addon/huaweipay/event/PayClose.php create mode 100644 src/addon/huaweipay/event/PayNotify.php create mode 100644 src/addon/huaweipay/event/PayOrderQuery.php create mode 100644 src/addon/huaweipay/event/PayRefund.php create mode 100644 src/addon/huaweipay/event/PayTransfer.php create mode 100644 src/addon/huaweipay/event/PayType.php create mode 100644 src/addon/huaweipay/event/TransferType.php create mode 100644 src/addon/huaweipay/icon.png create mode 100644 src/addon/huaweipay/model/Config.php create mode 100644 src/addon/huaweipay/model/Pay.php create mode 100644 src/addon/huaweipay/shop/controller/Pay.php create mode 100644 src/addon/huaweipay/shop/view/pay/config.html create mode 100644 src/addon/offlinepay/api/controller/Pay.php create mode 100644 src/addon/offlinepay/config/diy_view.php create mode 100644 src/addon/offlinepay/config/event.php create mode 100644 src/addon/offlinepay/config/info.php create mode 100644 src/addon/offlinepay/config/menu_shop.php create mode 100644 src/addon/offlinepay/event/Install.php create mode 100644 src/addon/offlinepay/event/MessageOfflinepayAuditRefuse.php create mode 100644 src/addon/offlinepay/event/MessageOfflinepayWaitAudit.php create mode 100644 src/addon/offlinepay/event/Pay.php create mode 100644 src/addon/offlinepay/event/PayClose.php create mode 100644 src/addon/offlinepay/event/PayRefund.php create mode 100644 src/addon/offlinepay/event/PayType.php create mode 100644 src/addon/offlinepay/event/UnInstall.php create mode 100644 src/addon/offlinepay/icon.png create mode 100644 src/addon/offlinepay/model/Config.php create mode 100644 src/addon/offlinepay/model/Pay.php create mode 100644 src/addon/offlinepay/shop/controller/Pay.php create mode 100644 src/addon/offlinepay/shop/view/pay/config.html create mode 100644 src/addon/offlinepay/shop/view/pay/lists.html create mode 100644 src/addon/offlinepay/shop/view/pay/pay.html diff --git a/src/addon/huaweipay/config/diy_view.php b/src/addon/huaweipay/config/diy_view.php new file mode 100644 index 000000000..0c5d86650 --- /dev/null +++ b/src/addon/huaweipay/config/diy_view.php @@ -0,0 +1,33 @@ + '页面类型名称', 'name' => '页面标识', 'path' => '页面路径', 'value' => '页面数据,json格式' ] + 'template' => [], + + // 后台自定义组件——装修 + 'util' => [], + + // 自定义页面路径 + 'link' => [], + + // 自定义图标库 + 'icon_library' => [], + + // uni-app 组件,格式:[ 'name' => '组件名称/文件夹名称', 'path' => '文件路径/目录路径' ],多个逗号隔开,自定义组件名称前缀必须是diy-,也可以引用第三方组件 + 'component' => [], + + // uni-app 页面,多个逗号隔开 + 'pages' => [], + + // 模板信息,格式:'title' => '模板名称', 'name' => '模板标识', 'cover' => '模板封面图', 'preview' => '模板预览图', 'desc' => '模板描述' + 'info' => [], + + // 主题风格配色,格式可以自由定义扩展,【在uni-app中通过:this.themeStyle... 获取定义的颜色字段,例如:this.themeStyle.main_color】 + 'theme' => [], + + // 自定义页面数据,格式:[ 'title' => '页面名称', 'name' => "页面标识", 'value' => [页面数据,json格式] ] + 'data' => [] +]; diff --git a/src/addon/huaweipay/config/event.php b/src/addon/huaweipay/config/event.php new file mode 100644 index 000000000..a18ada701 --- /dev/null +++ b/src/addon/huaweipay/config/event.php @@ -0,0 +1,45 @@ + [ + + ], + + 'listen' => [ + //支付异步回调 + 'PayNotify' => [ + 'addon\huaweipay\event\PayNotify' + ], + //支付方式,后台查询 + 'PayType' => [ + 'addon\huaweipay\event\PayType' + ], + //支付,前台应用 + 'Pay' => [ + 'addon\huaweipay\event\Pay' + ], + 'PayClose' => [ + 'addon\huaweipay\event\PayClose' + ], + 'PayRefund' => [ + 'addon\huaweipay\event\PayRefund' + ], + 'PayTransfer' => [ + 'addon\huaweipay\event\PayTransfer' + ], + 'TransferType' => [ + 'addon\huaweipay\event\TransferType' + ], + 'AuthcodePay' => [ + 'addon\huaweipay\event\AuthcodePay' + ], + 'PayOrderQuery' => [ + 'addon\huaweipay\event\PayOrderQuery' + ], + ], + + 'subscribe' => [ + ], +]; diff --git a/src/addon/huaweipay/config/info.php b/src/addon/huaweipay/config/info.php new file mode 100644 index 000000000..96ac182f8 --- /dev/null +++ b/src/addon/huaweipay/config/info.php @@ -0,0 +1,15 @@ + 'huaweipay', + 'title' => '华为支付', + 'description' => '华为支付功能', + 'type' => 'system', //插件类型 system :系统插件(自动安装), business:业务插件 promotion:扩展营销插件 tool:工具插件 + 'status' => 1, + 'author' => '', + 'version' => '5.3.1', + 'version_no' => '525231212001', + 'content' => '', +]; diff --git a/src/addon/huaweipay/config/menu_shop.php b/src/addon/huaweipay/config/menu_shop.php new file mode 100644 index 000000000..2a2c18eba --- /dev/null +++ b/src/addon/huaweipay/config/menu_shop.php @@ -0,0 +1,18 @@ + 'HUAWEI_PAY_CONFIG', + 'title' => '华为支付编辑', + 'url' => 'huaweipay://shop/pay/config', + 'parent' => 'CONFIG_PAY', + 'is_show' => 0, + 'is_control' => 1, + 'is_icon' => 0, + 'picture' => '', + 'picture_select' => '', + 'sort' => 1, + ], +]; diff --git a/src/addon/huaweipay/data/sdk/HuaweiPayClient.php b/src/addon/huaweipay/data/sdk/HuaweiPayClient.php new file mode 100644 index 000000000..6e4bf3e9f --- /dev/null +++ b/src/addon/huaweipay/data/sdk/HuaweiPayClient.php @@ -0,0 +1,291 @@ +config = $config; + + // 根据配置设置网关地址 + if (isset($config['sandbox']) && $config['sandbox']) { + $this->gatewayUrl = 'https://pay-drcn.cloud.huawei.com/gateway/api/pay'; + } + } + + /** + * 生成签名 + * @param array $params 请求参数 + * @return string 签名结果 + */ + private function generateSign($params) + { + // 移除空值和签名参数 + $params = array_filter($params, function($value) { + return $value !== null && $value !== ''; + }); + unset($params['sign']); + unset($params['sign_type']); + + // 按键名排序 + ksort($params); + + // 拼接参数 + $stringToSign = ''; + foreach ($params as $key => $value) { + $stringToSign .= $key . '=' . $value . '&'; + } + $stringToSign = rtrim($stringToSign, '&'); + + // 根据签名类型生成签名 + switch ($this->signType) { + case 'RSA2': + $privateKey = $this->config['private_key']; + $privateKey = "-----BEGIN PRIVATE KEY-----\n" . wordwrap($privateKey, 64, "\n", true) . "\n-----END PRIVATE KEY-----"; + openssl_sign($stringToSign, $sign, $privateKey, OPENSSL_ALGO_SHA256); + break; + case 'RSA': + $privateKey = $this->config['private_key']; + $privateKey = "-----BEGIN PRIVATE KEY-----\n" . wordwrap($privateKey, 64, "\n", true) . "\n-----END PRIVATE KEY-----"; + openssl_sign($stringToSign, $sign, $privateKey, OPENSSL_ALGO_SHA1); + break; + default: + throw new \Exception('不支持的签名类型'); + } + + return base64_encode($sign); + } + + /** + * 验证签名 + * @param array $params 响应参数 + * @return bool 验证结果 + */ + public function verifySign($params) + { + // 保存签名 + $sign = $params['sign'] ?? ''; + unset($params['sign']); + unset($params['sign_type']); + + // 移除空值 + $params = array_filter($params, function($value) { + return $value !== null && $value !== ''; + }); + + // 按键名排序 + ksort($params); + + // 拼接参数 + $stringToSign = ''; + foreach ($params as $key => $value) { + $stringToSign .= $key . '=' . $value . '&'; + } + $stringToSign = rtrim($stringToSign, '&'); + + // 验证签名 + $huaweiPublicKey = $this->config['huawei_public_key']; + $huaweiPublicKey = "-----BEGIN PUBLIC KEY-----\n" . wordwrap($huaweiPublicKey, 64, "\n", true) . "\n-----END PUBLIC KEY-----"; + + switch ($this->signType) { + case 'RSA2': + $result = openssl_verify($stringToSign, base64_decode($sign), $huaweiPublicKey, OPENSSL_ALGO_SHA256); + break; + case 'RSA': + $result = openssl_verify($stringToSign, base64_decode($sign), $huaweiPublicKey, OPENSSL_ALGO_SHA1); + break; + default: + throw new \Exception('不支持的签名类型'); + } + + return $result === 1; + } + + /** + * 发送HTTP请求 + * @param string $url 请求地址 + * @param array $params 请求参数 + * @param string $method 请求方法 + * @return array 响应结果 + */ + private function httpRequest($url, $params, $method = 'POST') + { + $ch = curl_init(); + + // 设置请求头 + $headers = [ + 'Content-Type: application/x-www-form-urlencoded; charset=utf-8', + ]; + + // 生成签名 + $params['app_id'] = $this->config['app_id']; + $params['sign_type'] = $this->signType; + $params['sign'] = $this->generateSign($params); + + if ($method === 'POST') { + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); + } else { + $url .= '?' . http_build_query($params); + } + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + + $response = curl_exec($ch); + + if (curl_errno($ch)) { + throw new \Exception('HTTP请求错误: ' . curl_error($ch)); + } + + curl_close($ch); + + // 解析响应 + parse_str($response, $result); + + return $result; + } + + /** + * H5支付 + * @param array $params 支付参数 + * @param string $returnUrl 同步回调地址 + * @param string $notifyUrl 异步回调地址 + * @return array 支付结果 + */ + public function h5Pay($params, $returnUrl, $notifyUrl) + { + $requestParams = [ + 'method' => 'h5pay.createPayment', + 'version' => '1.0', + 'timestamp' => date('Y-m-d H:i:s'), + 'out_trade_no' => $params['out_trade_no'], + 'subject' => $params['subject'], + 'total_amount' => $params['total_amount'], + 'body' => $params['body'], + 'return_url' => $returnUrl, + 'notify_url' => $notifyUrl, + 'scene' => 'h5', + ]; + + return $this->httpRequest($this->gatewayUrl, $requestParams); + } + + /** + * APP支付 + * @param array $params 支付参数 + * @param string $notifyUrl 异步回调地址 + * @return array 支付结果 + */ + public function appPay($params, $notifyUrl) + { + $requestParams = [ + 'method' => 'apppay.createPayment', + 'version' => '1.0', + 'timestamp' => date('Y-m-d H:i:s'), + 'out_trade_no' => $params['out_trade_no'], + 'subject' => $params['subject'], + 'total_amount' => $params['total_amount'], + 'body' => $params['body'], + 'notify_url' => $notifyUrl, + ]; + + return $this->httpRequest($this->gatewayUrl, $requestParams); + } + + /** + * 查询订单 + * @param array $params 查询参数 + * @return array 查询结果 + */ + public function queryOrder($params) + { + $requestParams = [ + 'method' => 'unifiedorder.query', + 'version' => '1.0', + 'timestamp' => date('Y-m-d H:i:s'), + ]; + + // 支持通过商户订单号或华为交易号查询 + if (isset($params['out_trade_no'])) { + $requestParams['out_trade_no'] = $params['out_trade_no']; + } elseif (isset($params['trade_no'])) { + $requestParams['trade_no'] = $params['trade_no']; + } else { + throw new \Exception('查询参数错误,必须提供out_trade_no或trade_no'); + } + + return $this->httpRequest($this->gatewayUrl, $requestParams); + } + + /** + * 关闭订单 + * @param array $params 关闭参数 + * @return array 关闭结果 + */ + public function closeOrder($params) + { + $requestParams = [ + 'method' => 'unifiedorder.close', + 'version' => '1.0', + 'timestamp' => date('Y-m-d H:i:s'), + 'out_trade_no' => $params['out_trade_no'], + ]; + + return $this->httpRequest($this->gatewayUrl, $requestParams); + } + + /** + * 退款 + * @param array $params 退款参数 + * @return array 退款结果 + */ + public function refund($params) + { + $requestParams = [ + 'method' => 'refund.apply', + 'version' => '1.0', + 'timestamp' => date('Y-m-d H:i:s'), + 'out_trade_no' => $params['out_trade_no'], + 'trade_no' => $params['trade_no'], + 'refund_amount' => $params['refund_amount'], + 'out_request_no' => $params['out_request_no'], + 'refund_reason' => $params['refund_reason'] ?? '', + ]; + + return $this->httpRequest($this->gatewayUrl, $requestParams); + } + + /** + * 验证回调签名 + * @param array $params 回调参数 + * @return bool 验证结果 + */ + public function verifyNotify($params) + { + return $this->verifySign($params); + } +} diff --git a/src/addon/huaweipay/event/AuthcodePay.php b/src/addon/huaweipay/event/AuthcodePay.php new file mode 100644 index 000000000..126d9c665 --- /dev/null +++ b/src/addon/huaweipay/event/AuthcodePay.php @@ -0,0 +1,26 @@ +authcodePay($param); + return $res; + } + } +} diff --git a/src/addon/huaweipay/event/Pay.php b/src/addon/huaweipay/event/Pay.php new file mode 100644 index 000000000..f409ede05 --- /dev/null +++ b/src/addon/huaweipay/event/Pay.php @@ -0,0 +1,28 @@ +pay($param); + return $res; + } + } + } +} diff --git a/src/addon/huaweipay/event/PayClose.php b/src/addon/huaweipay/event/PayClose.php new file mode 100644 index 000000000..e64cbb86b --- /dev/null +++ b/src/addon/huaweipay/event/PayClose.php @@ -0,0 +1,26 @@ +close($param); + return $res; + } + } +} diff --git a/src/addon/huaweipay/event/PayNotify.php b/src/addon/huaweipay/event/PayNotify.php new file mode 100644 index 000000000..0d19202b6 --- /dev/null +++ b/src/addon/huaweipay/event/PayNotify.php @@ -0,0 +1,26 @@ +notify($param); + return $res; + } + } +} diff --git a/src/addon/huaweipay/event/PayOrderQuery.php b/src/addon/huaweipay/event/PayOrderQuery.php new file mode 100644 index 000000000..e689e39e5 --- /dev/null +++ b/src/addon/huaweipay/event/PayOrderQuery.php @@ -0,0 +1,26 @@ +query($param); + return $res; + } + } +} diff --git a/src/addon/huaweipay/event/PayRefund.php b/src/addon/huaweipay/event/PayRefund.php new file mode 100644 index 000000000..0484a075e --- /dev/null +++ b/src/addon/huaweipay/event/PayRefund.php @@ -0,0 +1,26 @@ +refund($param); + return $res; + } + } +} diff --git a/src/addon/huaweipay/event/PayTransfer.php b/src/addon/huaweipay/event/PayTransfer.php new file mode 100644 index 000000000..ec11831cb --- /dev/null +++ b/src/addon/huaweipay/event/PayTransfer.php @@ -0,0 +1,26 @@ +transfer($param); + return $res; + } + } +} diff --git a/src/addon/huaweipay/event/PayType.php b/src/addon/huaweipay/event/PayType.php new file mode 100644 index 000000000..b44f66d02 --- /dev/null +++ b/src/addon/huaweipay/event/PayType.php @@ -0,0 +1,44 @@ +getPayConfig($param[ 'site_id' ]); + $config = $config_result[ "data" ][ "value" ] ?? []; + $pay_status = $config[ "pay_status" ] ?? 0; + if ($pay_status == 0) { + return ''; + } + } + $info = array ( + "pay_type" => "huaweipay", + "pay_type_name" => "华为支付", + "edit_url" => "huaweipay://shop/pay/config", + "shop_url" => "huaweipay://shop/pay/config", + "logo" => "addon/huaweipay/icon.png", + "desc" => "华为支付(www.huawei.com) 是华为公司提供的网上支付平台。" + ); + return $info; + } +} diff --git a/src/addon/huaweipay/event/TransferType.php b/src/addon/huaweipay/event/TransferType.php new file mode 100644 index 000000000..6c5ce03c0 --- /dev/null +++ b/src/addon/huaweipay/event/TransferType.php @@ -0,0 +1,25 @@ + "huaweipay", + "pay_type_name" => "华为支付", + "logo" => "addon/huaweipay/icon.png", + ); + return $info; + } +} diff --git a/src/addon/huaweipay/icon.png b/src/addon/huaweipay/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a676d699d30c5a12f33d7154f1933d60b1888b77 GIT binary patch literal 21351 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[ 'data' ][ 'value' ] ?? []; + + // 检测数据是否发生变化,如果没有变化,则保持未加密前的数据 + if (!empty($data[ 'app_id' ]) && $data[ 'app_id' ] == $this->encrypt) { + $data[ 'app_id' ] = $original_config[ 'app_id' ]; // 应用ID + } + if (!empty($data[ 'private_key' ]) && $data[ 'private_key' ] == $this->encrypt) { + $data[ 'private_key' ] = $original_config[ 'private_key' ]; // 应用私钥 + } + if (!empty($data[ 'public_key' ]) && $data[ 'public_key' ] == $this->encrypt) { + $data[ 'public_key' ] = $original_config[ 'public_key' ]; // 应用公钥 + } + if (!empty($data[ 'huawei_public_key' ]) && $data[ 'huawei_public_key' ] == $this->encrypt) { + $data[ 'huawei_public_key' ] = $original_config[ 'huawei_public_key' ]; // 华为公钥 + } + + $res = $config->setConfig($data, '华为支付配置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'HUAWEI_PAY_CONFIG' ] ]); + return $res; + } + + /** + * 获取支付配置 + * @param int $site_id + * @param string $app_module + * @param bool $need_encrypt 是否需要加密数据,true:加密、false:不加密 + * @return array + */ + public function getPayConfig($site_id = 0, $app_module = 'shop', $need_encrypt = false) + { + $config = new ConfigModel(); + $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'HUAWEI_PAY_CONFIG' ] ]); + if ($need_encrypt) { + // 加密敏感信息 + if (!empty($res[ 'data' ][ 'value' ][ 'app_id' ])) { + $res[ 'data' ][ 'value' ][ 'app_id' ] = $this->encrypt; // 应用ID + } + if (!empty($res[ 'data' ][ 'value' ][ 'private_key' ])) { + $res[ 'data' ][ 'value' ][ 'private_key' ] = $this->encrypt; // 应用私钥 + } + if (!empty($res[ 'data' ][ 'value' ][ 'public_key' ])) { + $res[ 'data' ][ 'value' ][ 'public_key' ] = $this->encrypt; // 应用公钥 + } + if (!empty($res[ 'data' ][ 'value' ][ 'huawei_public_key' ])) { + $res[ 'data' ][ 'value' ][ 'huawei_public_key' ] = $this->encrypt; // 华为公钥 + } + } + return $res; + } +} diff --git a/src/addon/huaweipay/model/Pay.php b/src/addon/huaweipay/model/Pay.php new file mode 100644 index 000000000..f90c69a2a --- /dev/null +++ b/src/addon/huaweipay/model/Pay.php @@ -0,0 +1,252 @@ +getPayConfig($site_id)['data']['value']; + + if (!empty($config_info)) { + // 初始化华为支付客户端 + $this->hwpay_client = new HuaweiPayClient($config_info); + } + } catch (\Exception $e) { + return $this->error('', '华为支付配置错误: ' . $e->getMessage()); + } + } + + /** + * 生成支付 + * @param $param 支付参数 + * @return array + */ + public function pay($param) + { + try { + // 构造华为支付请求参数 + $parameter = array( + "out_trade_no" => $param["out_trade_no"], + "subject" => substr($param["pay_body"], 0, 15), + "total_amount" => (float)$param["pay_money"], + "body" => substr($param["pay_body"], 0, 60), + ); + + // 绑定商户数据 + $pay_model = new PayModel(); + $pay_model->bindMchPay($param["out_trade_no"], []); + + // 根据不同的应用类型调用不同的华为支付API + switch ($param["app_type"]) { + case "h5": + // H5支付 + $result = $this->hwpay_client->h5Pay($parameter, $param["return_url"], $param["notify_url"]); + if ($result['code'] == '0') { + return $this->success([ + 'type' => 'url', + 'data' => $result['pay_url'] + ]); + } else { + return $this->error('', $result['msg'] ?? '华为支付请求失败'); + } + break; + case "app": + // APP支付 + $result = $this->hwpay_client->appPay($parameter, $param["notify_url"]); + if ($result['code'] == '0') { + return $this->success([ + 'type' => 'params', + 'data' => $result + ]); + } else { + return $this->error('', $result['msg'] ?? '华为支付请求失败'); + } + break; + default: + // 默认返回错误 + return $this->error('', '不支持的支付类型'); + } + } catch (\Exception $e) { + Log::error('华为支付生成支付失败: ' . $e->getMessage()); + return $this->error('', $e->getMessage()); + } + } + + /** + * 支付关闭 + * @param $param 关闭订单参数 + * @return array + */ + public function close($param) + { + try { + $parameter = array( + "out_trade_no" => $param["out_trade_no"] + ); + + // 调用华为支付关闭订单API + $result = $this->hwpay_client->closeOrder($parameter); + + if ($result['code'] == '0') { + return $this->success(); + } else { + return $this->error('', $result['msg'] ?? '关闭订单失败'); + } + } catch (\Exception $e) { + Log::error('华为支付关闭订单失败: ' . $e->getMessage()); + return $this->error('', $e->getMessage()); + } + } + + /** + * 华为支付退款 + * @param array $param 退款参数 + * @return array + */ + public function refund($param) + { + try { + $pay_info = $param["pay_info"]; + $refund_no = $param["refund_no"]; + $out_trade_no = $pay_info["out_trade_no"] ?? ''; + $trade_no = $pay_info["trade_no"] ?? ''; + $refund_fee = $param["refund_fee"]; + + $parameter = array( + 'out_trade_no' => $out_trade_no, + 'trade_no' => $trade_no, + 'refund_amount' => sprintf("%.2f", $refund_fee), + 'out_request_no' => $refund_no, + 'refund_reason' => $param["refund_desc"] ?? '' + ); + + // 调用华为支付退款API + $result = $this->hwpay_client->refund($parameter); + + if ($result['code'] == '0') { + return $this->success(); + } else { + return $this->error('', $result['msg'] ?? '退款失败'); + } + } catch (\Exception $e) { + Log::error('华为支付退款失败: ' . $e->getMessage()); + return $this->error('', $e->getMessage()); + } + } + + /** + * 华为支付转账 + * @param $param 转账参数 + * @return array + */ + public function transfer($param) + { + try { + // 华为支付转账功能需要根据实际API进行实现 + // 目前华为支付客户端未实现转账功能,需要根据官方文档扩展 + return $this->error('', '华为支付转账功能暂未实现'); + } catch (\Exception $e) { + Log::error('华为支付转账失败: ' . $e->getMessage()); + return $this->error('', $e->getMessage()); + } + } + + /** + * 异步完成支付 + * @param $param 回调参数 + */ + public function notify($param) + { + try { + // 验证华为支付回调签名 + $is_valid = $this->hwpay_client->verifyNotify($param); + + if ($is_valid) { // 验证成功 + $out_trade_no = $param['out_trade_no']; + // 华为支付交易号 + $trade_no = $param['trade_no']; + // 交易状态 + $trade_status = $param['trade_status']; + $pay_common = new PayCommon(); + + if ($trade_status == "TRADE_SUCCESS") { + $retval = $pay_common->onlinePay($out_trade_no, "huaweipay", $trade_no, "huaweipay"); + } + echo "success"; + } else { + // 验证失败 + Log::error('华为支付回调签名验证失败: ' . json_encode($param)); + echo "fail"; + } + } catch (\Exception $e) { + Log::error('华为支付回调处理失败: ' . $e->getMessage()); + echo "fail"; + } + } + + /** + * 授权码支付 + * @param $param 授权码支付参数 + * @return array|mixed|void + */ + public function authcodePay($param) + { + try { + // 华为支付授权码支付功能需要根据实际API进行实现 + // 目前华为支付客户端未实现授权码支付功能,需要根据官方文档扩展 + return $this->error('', '华为支付授权码支付功能暂未实现'); + } catch (\Exception $e) { + Log::error('华为支付授权码支付失败: ' . $e->getMessage()); + return $this->error('', $e->getMessage()); + } + } + + /** + * 查询订单信息 + * @param $param 查询参数 + * @return array + */ + public function query($param) + { + try { + // 构造查询请求参数 + $parameter = array( + "out_trade_no" => $param["out_trade_no"], + ); + + // 调用华为支付查询订单API + $result = $this->hwpay_client->queryOrder($parameter); + + if ($result['code'] == '0') { + return $this->success($result['data']); + } else { + return $this->error('', $result['msg'] ?? '查询订单失败'); + } + } catch (\Exception $e) { + Log::error('华为支付查询订单失败: ' . $e->getMessage()); + return $this->error('', $e->getMessage()); + } + } +} diff --git a/src/addon/huaweipay/shop/controller/Pay.php b/src/addon/huaweipay/shop/controller/Pay.php new file mode 100644 index 000000000..296f634f8 --- /dev/null +++ b/src/addon/huaweipay/shop/controller/Pay.php @@ -0,0 +1,82 @@ +isJson()) { + $app_id = input("app_id", "");//华为应用ID, // PETALPAY.APPID, 商户号关联的APPID + $mch_id = input("mch_id", "");//商户号, // PETALPAY.MERC_NO, 商户号 + $private_key = input("private_key", "");//商户应用私钥, // PETALPAY.MERC_PRIVATE_KEY, 商户应用私钥 + $mch_auth_id = input("mch_auth_id", "");//商户证书id, // PETALPAY.MERC_AUTH_ID, 商户证书id + $sign_type = input("sign_type", "RSA");//商户公私钥签名类型, // PETALPAY.SIGN_TYPE, 签名类型, 默认使用RSA + $huawei_public_key = input("huawei_public_key", ""); //华为公钥, // PETALPAY.HW_PAY_PUBLIC_KEY_FOR_CALLBACK, 华为公钥 + $app_type = input("app_type", "");//支持端口 如web app + $pay_status = input("pay_status", 0);//支付启用状态 + $refund_status = input("refund_status", 0);//退款启用状态 + $transfer_status = input("transfer_status", 0);//转账启用状态 + + $data = array ( + "app_id" => $app_id, + "mch_id" => $mch_id, + "private_key" => $private_key, + "mch_auth_id" => $mch_auth_id, + "sign_type" => $sign_type, + "huawei_public_key" => $huawei_public_key, + "refund_status" => $refund_status, + "pay_status" => $pay_status, + "transfer_status" => $transfer_status, + "app_type" => $app_type + ); + $result = $config_model->setPayConfig($data, $this->site_id, $this->app_module); + return $result; + } else { + $info = $config_model->getPayConfig($this->site_id, $this->app_module, true)[ 'data' ][ 'value' ] ?? []; + + if (!empty($info)) { + $app_type_arr = []; + if (!empty($info[ 'app_type' ])) { + $app_type_arr = explode(',', $info[ 'app_type' ]); + } + $info[ 'app_type_arr' ] = $app_type_arr; + } + $this->assign("info", $info); + $this->assign("app_type", Config::get("app_type")); + + return $this->fetch("pay/config"); + } + } + + /** + * 上传华为支付证书 + */ + public function uploadHuaweiCrt() + { + $upload_model = new Upload(); + $site_id = request()->siteid(); + $name = input("name", ""); + $extend_type = [ 'crt', 'pem' ]; + $param = array ( + "name" => "file", + "extend_type" => $extend_type + ); + + $site_id = max($site_id, 0); + $result = $upload_model->setPath("common/huaweipay/crt/" . $site_id . "/")->file($param); + return $result; + } +} diff --git a/src/addon/huaweipay/shop/view/pay/config.html b/src/addon/huaweipay/shop/view/pay/config.html new file mode 100644 index 000000000..0d8b5537b --- /dev/null +++ b/src/addon/huaweipay/shop/view/pay/config.html @@ -0,0 +1,219 @@ + + +
+
+ +
+ +
+
[MERC_NO]华为支付商户号 查看指引
+
+ +
+ +
+ +
+
华为分配给开发者的应用ID 查看指引
+
+ +
+ +
+ {notempty name="$info.private_key"} +

已上传

+ {else/} +

未上传

+ {/notempty} + + +
+
上传商户应用私钥.pem 文件
+
如何获取商户应用私钥.pem文件 查看指引
+
+ +
+ +
+ +
+ +
+ +
+ +
+ {notempty name="$info.huawei_public_key"} +

已上传

+ {else/} +

未上传

+ {/notempty} + + +
+
上传华为支付证书.pem文件
+
如何获取华为支付证书.pem文件,查看指引
+
+ +
+ +
+ {notempty name="$info.huawei_public_key_for_sessionkey"} +

已上传

+ {else/} +

未上传

+ {/notempty} + + +
+
上传华为支付服务加密公钥.pem文件
+
(可选)加密公钥, 没有可以不填 查看指引
+
+ +
+ +
+ {foreach $app_type as $app_type_k => $app_type_v} + {if condition="$app_type_v['name'] !='微信小程序' && $app_type_v['name'] !='微信公众号'"} + {$app_type_v['name']} + {/if} + {/foreach} +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ + +
+
+ + diff --git a/src/addon/offlinepay/api/controller/Pay.php b/src/addon/offlinepay/api/controller/Pay.php new file mode 100644 index 000000000..092a2536b --- /dev/null +++ b/src/addon/offlinepay/api/controller/Pay.php @@ -0,0 +1,71 @@ +getPayConfig($this->site_id); + return $this->response($res); + } + + /** + * 信息 + */ + public function info() + { + $token = $this->checkToken(); + if ($token[ 'code' ] < 0) return $this->response($token); + + $out_trade_no = $this->params['out_trade_no'] ?? ''; + $pay_model = new PayModel(); + $res = $pay_model->getInfo([['out_trade_no', '=', $out_trade_no], ['member_id', '=', $this->member_id]]); + return $this->response($res); + } + + /** + * 支付 + */ + public function pay() + { + $token = $this->checkToken(); + if ($token[ 'code' ] < 0) return $this->response($token); + + $pay_model = new PayModel(); + $res = $pay_model->pay([ + 'member_id' => $this->member_id, + 'out_trade_no' => $this->params['out_trade_no'] ?? '', + 'imgs' => $this->params['imgs'] ?? '', + 'desc' => $this->params['desc'] ?? '' + ]); + return $this->response($res); + } + + /** + * 图片上传 + */ + public function uploadImg() + { + $upload_model = new UploadModel(0); + $param = [ + 'thumb_type' => '', + 'name' => 'file', + 'cloud' => 1, + ]; + $result = $upload_model->setPath('offlinepay/' . date('Ymd') . '/')->image($param); + return $this->response($result); + } +} \ No newline at end of file diff --git a/src/addon/offlinepay/config/diy_view.php b/src/addon/offlinepay/config/diy_view.php new file mode 100644 index 000000000..e36ca9b7d --- /dev/null +++ b/src/addon/offlinepay/config/diy_view.php @@ -0,0 +1,30 @@ + '页面类型名称', 'name' => '页面标识', 'path' => '页面路径', 'value' => '页面数据,json格式' ] + 'template' => [], + + // 后台自定义组件——装修 + 'util' => [], + + // 自定义页面路径 + 'link' => [], + + // 自定义图标库 + 'icon_library' => [], + + // uni-app 组件,格式:[ 'name' => '组件名称/文件夹名称', 'path' => '文件路径/目录路径' ],多个逗号隔开,自定义组件名称前缀必须是diy-,也可以引用第三方组件 + 'component' => [], + + // uni-app 页面,多个逗号隔开 + 'pages' => [], + + // 模板信息,格式:'title' => '模板名称', 'name' => '模板标识', 'cover' => '模板封面图', 'preview' => '模板预览图', 'desc' => '模板描述' + 'info' => [], + + // 主题风格配色,格式可以自由定义扩展,【在uni-app中通过:this.themeStyle... 获取定义的颜色字段,例如:this.themeStyle.main_color】 + 'theme' => [], + + // 自定义页面数据,格式:[ 'title' => '页面名称', 'name' => "页面标识", 'value' => [页面数据,json格式] ] + 'data' => [] +]; \ No newline at end of file diff --git a/src/addon/offlinepay/config/event.php b/src/addon/offlinepay/config/event.php new file mode 100644 index 000000000..e6ecfb2f8 --- /dev/null +++ b/src/addon/offlinepay/config/event.php @@ -0,0 +1,30 @@ + [ + + ], + + 'listen' => [ + //支付方式,后台查询 + 'PayType' => [ + 'addon\offlinepay\event\PayType' + ], + 'Pay' => [ + 'addon\offlinepay\event\Pay' + ], + 'PayClose' => [ + 'addon\offlinepay\event\PayClose' + ], + 'PayRefund' => [ + 'addon\offlinepay\event\PayRefund' + ], + 'SendMessageTemplate' => [ + 'addon\offlinepay\event\MessageOfflinepayWaitAudit', + 'addon\offlinepay\event\MessageOfflinepayAuditRefuse', + ], + ], + + 'subscribe' => [ + ], +]; diff --git a/src/addon/offlinepay/config/info.php b/src/addon/offlinepay/config/info.php new file mode 100644 index 000000000..a8f8555a0 --- /dev/null +++ b/src/addon/offlinepay/config/info.php @@ -0,0 +1,12 @@ + 'offlinepay', + 'title' => '线下支付', + 'description' => '线下支付功能', + 'type' => 'system', //插件类型 system :系统插件(自动安装), business:业务插件 promotion:扩展营销插件 tool:工具插件 + 'status' => 1, + 'author' => '', + 'version' => '5.5.2', + 'version_no' => '552250604001', + 'content' => '', +]; \ No newline at end of file diff --git a/src/addon/offlinepay/config/menu_shop.php b/src/addon/offlinepay/config/menu_shop.php new file mode 100644 index 000000000..947859243 --- /dev/null +++ b/src/addon/offlinepay/config/menu_shop.php @@ -0,0 +1,49 @@ + 'OFFLINE_PAY_CONFIG', + 'title' => '线下支付编辑', + 'url' => 'offlinepay://shop/pay/config', + 'parent' => 'CONFIG_PAY', + 'is_show' => 0, + 'is_control' => 1, + 'is_icon' => 0, + 'sort' => 1, + 'type' => 'button', + ], + [ + 'name' => 'OFFLINE_PAY_LIST', + 'title' => '线下支付', + 'url' => 'offlinepay://shop/pay/lists', + 'parent' => 'ORDER_MANAGE', + 'is_show' => 1, + 'is_control' => 1, + 'is_icon' => 0, + 'sort' => 8, + 'child_list' => [ + [ + 'name' => 'OFFLINE_PAY_AUDIT_PASS', + 'title' => '审核通过', + 'url' => 'offlinepay://shop/pay/auditpass', + 'is_show' => 0, + 'is_control' => 1, + 'is_icon' => 0, + 'sort' => 1, + 'type' => 'button', + ], + [ + 'name' => 'OFFLINE_PAY_AUDIT_REFUSE', + 'title' => '审核拒绝', + 'url' => 'offlinepay://shop/pay/auditrefuse', + 'is_show' => 0, + 'is_control' => 1, + 'is_icon' => 0, + 'sort' => 2, + 'type' => 'button', + ], + ] + ], +]; diff --git a/src/addon/offlinepay/event/Install.php b/src/addon/offlinepay/event/Install.php new file mode 100644 index 000000000..f55504021 --- /dev/null +++ b/src/addon/offlinepay/event/Install.php @@ -0,0 +1,17 @@ +messageAuditRefuse($param); + } + } +} \ No newline at end of file diff --git a/src/addon/offlinepay/event/MessageOfflinepayWaitAudit.php b/src/addon/offlinepay/event/MessageOfflinepayWaitAudit.php new file mode 100644 index 000000000..0fc2678dd --- /dev/null +++ b/src/addon/offlinepay/event/MessageOfflinepayWaitAudit.php @@ -0,0 +1,16 @@ +messageWaitAudit($param); + } + } +} \ No newline at end of file diff --git a/src/addon/offlinepay/event/Pay.php b/src/addon/offlinepay/event/Pay.php new file mode 100644 index 000000000..6935069e3 --- /dev/null +++ b/src/addon/offlinepay/event/Pay.php @@ -0,0 +1,24 @@ +clearMchPay($params[ "out_trade_no" ], 'offlinepay'); + if($clear_res['code'] < 0) return $clear_res; + return success(); + } + } +} \ No newline at end of file diff --git a/src/addon/offlinepay/event/PayClose.php b/src/addon/offlinepay/event/PayClose.php new file mode 100644 index 000000000..42b14820b --- /dev/null +++ b/src/addon/offlinepay/event/PayClose.php @@ -0,0 +1,27 @@ +close([['out_trade_no', '=', $params['out_trade_no']]]); + return $result; + } + } +} \ No newline at end of file diff --git a/src/addon/offlinepay/event/PayRefund.php b/src/addon/offlinepay/event/PayRefund.php new file mode 100644 index 000000000..ce8d98a71 --- /dev/null +++ b/src/addon/offlinepay/event/PayRefund.php @@ -0,0 +1,24 @@ +refund($params['pay_info']['out_trade_no'], $params['refund_fee']); + } + } +} \ No newline at end of file diff --git a/src/addon/offlinepay/event/PayType.php b/src/addon/offlinepay/event/PayType.php new file mode 100644 index 000000000..a9a96fa49 --- /dev/null +++ b/src/addon/offlinepay/event/PayType.php @@ -0,0 +1,44 @@ +getPayConfig($params[ 'site_id' ] ?? 1); + $config = $config_result[ "data" ][ "value" ] ?? []; + $pay_status = $config[ "pay_status" ] ?? 0; + + $app_type = $params['app_type'] ?? ''; + if (!empty($app_type)) { + $app_type_array = [ 'h5', 'wechat', 'weapp', 'pc' ]; + if (!in_array($app_type, $app_type_array)) { + return ''; + } + if ($pay_status == 0) { + return ''; + } + } + $info = array ( + "pay_type" => "offlinepay", + "pay_type_name" => "线下支付", + "edit_url" => "offlinepay://shop/pay/config", + "shop_url" => "offlinepay://shop/pay/config", + "logo" => "addon/offlinepay/icon.png", + "desc" => "通过银行卡、支付宝或微信收款码线下收款。", + "pay_status" => $pay_status + ); + return $info; + + } +} \ No newline at end of file diff --git a/src/addon/offlinepay/event/UnInstall.php b/src/addon/offlinepay/event/UnInstall.php new file mode 100644 index 000000000..63e703c43 --- /dev/null +++ b/src/addon/offlinepay/event/UnInstall.php @@ -0,0 +1,17 @@ +*!?{*W2|ksJSo4F8=f|CAp8egpr8 z4ga7l|Cb{FjTZlm6aSzs{*D*_s!jjdwg2(s|A`O(jTQf$FaPb>|F>@c!hrvtDgX23 z|9t}g>eT;z1OJyK|9%7ipDX{LEdQV?|9AlZd;tIE%m3A@|KGg-%98(x692Vf|NQv> z`SSnu>i_WH|LD*EdjS8tdH>Ct|IwlU|B@d6!+!pH z0RN^!|APttnI!+6D*m4=|C%NLdjS8UF8_iF|C%QMp)UV?0RMpp|9%4hoF@N$0RMgi z|J$_x>eBz{&i{@c|FB>GtXTitw*RC?|CuoV&6NMWb^nzm|ClTPsY?H>Q2(1K|Ns9t zNt>?#003xoQchF%*4+Kn&;a__&Gqc$;qubo`|!`&=jzJlME@f-a4{qNqU ziD643A^Q6F_Vwfb{rmg+`uXlPm)5gTWy0^2Om3Vb^WL#VJ_4V}f z@$l~M?C9v{?##xd-m`zz{e9t!fJu^96AT%*nDf%rdR$?Nd0VPqt+d=-Qh$w#PcGzWHMKSl7 zWd;x4Y@KS;dbLl>fgqQjg;35c$4&RinYySb}Fxr4hhMF@~&sE4ai zQbD9C@ffK|A5=AI)NxWxeY!P5T@DL1@R68^NKS|pNf1N|s}@eyt-$(8p_(AHZ^qtq UeqyXayZ`_I07*qoM6N<$g7K;nZvX%Q literal 0 HcmV?d00001 diff --git a/src/addon/offlinepay/model/Config.php b/src/addon/offlinepay/model/Config.php new file mode 100644 index 000000000..0c011585e --- /dev/null +++ b/src/addon/offlinepay/model/Config.php @@ -0,0 +1,71 @@ +handleConfigData($data); + $config = new ConfigModel(); + $res = $config->setConfig($data, '线下支付配置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'OFFLINE_PAY_CONFIG' ] ]); + return $res; + } + + /** + * 获取支付配置 + * @param $site_id + * @param $app_module + * @return array + */ + public function getPayConfig($site_id = 0, $app_module = 'shop') + { + $config = new ConfigModel(); + $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'OFFLINE_PAY_CONFIG' ] ]); + $res['data']['value'] = $this->handleConfigData($res['data']['value']); + return $res; + } + + /** + * 处理配置数据 + * @param $data + * @return mixed + */ + protected function handleConfigData($data) + { + $default_config = [ + 'pay_status' => 0,//支付状态 + 'bank' => [ + 'status' => 0,//是否开启 + 'bank_name' => '',//银行名称 + 'account_name' => '',//账户名称 + 'account_number' => '',//账号 + 'branch_name' => '',//支行名称 + ], + 'wechat' => [ + 'status' => 0,//是否开启 + 'account_name' => '',//账户名称 + 'payment_code' => '',//收款码 + ], + 'alipay' => [ + 'status' => 0,//是否开启 + 'account_name' => '',//账户名称 + 'payment_code' => '',//收款码 + ], + ]; + return assignData($data, $default_config); + } +} \ No newline at end of file diff --git a/src/addon/offlinepay/model/Pay.php b/src/addon/offlinepay/model/Pay.php new file mode 100644 index 000000000..2284f179a --- /dev/null +++ b/src/addon/offlinepay/model/Pay.php @@ -0,0 +1,489 @@ + self::STATUS_WAIT_AUDIT, + 'name' => '待审核', + 'const' => 'WAIT_AUDIT', + ], + [ + 'id' => self::STATUS_AUDIT_PASS, + 'name' => '审核通过', + 'const' => 'AUDIT_PASS', + ], + [ + 'id' => self::STATUS_AUDIT_REFUSE, + 'name' => '审核拒绝', + 'const' => 'AUDIT_REFUSE', + ], + [ + 'id' => self::STATUS_CLOSE, + 'name' => '已关闭', + 'const' => 'CLOSE', + ], + ]; + if(isset($arr[0][$key])){ + $arr = array_column($arr, null, $key); + } + return $arr; + } + + /** + * 支付操作 + * @param $param + * @return array + */ + public function pay($param) + { + $member_id = $param['member_id'] ?? 0; + $out_trade_no = $param['out_trade_no'] ?? ''; + $imgs = $param['imgs'] ?? ''; + $desc = $param['desc'] ?? ''; + + if(empty($member_id)) return $this->error(null, '用户id不可为空'); + if(empty($out_trade_no)) return $this->error(null, '外部交易号不可为空'); + if(empty($imgs)) return $this->error(null, '请上传支付凭证'); + + $pay_model = new PayModel(); + $pay_info = $pay_model->getPayInfo($out_trade_no)['data']; + if(empty($pay_info)) return $this->error(null, '支付信息有误'); + if(!in_array($pay_info['pay_status'], [PayModel::PAY_STATUS_NOT, PayModel::PAY_STATUS_IN_PROCESS])){ + return $this->error(null, '支付状态有误'); + } + + $offline_pay_info = model('pay_offline')->getInfo([['out_trade_no', '=', $out_trade_no], ['member_id', '=', $member_id]]); + if(!empty($offline_pay_info) && $offline_pay_info['status'] != self::STATUS_AUDIT_REFUSE){ + return $this->error(null, '当前状态不可修改'); + } + + //记录线下支付信息 + $data = [ + 'member_id' => $member_id, + 'out_trade_no' => $out_trade_no, + 'imgs' => $imgs, + 'desc' => $desc, + 'status' => self::STATUS_WAIT_AUDIT, + 'update_time' => time(), + ]; + + model('pay_offline')->startTrans(); + try{ + if(empty($offline_pay_info)){ + $data['create_time'] = time(); + model('pay_offline')->add($data); + //绑定支付数据 + $pay_model->bindMchPay($out_trade_no, [ + "pay_type" => 'offlinepay', + ]); + }else{ + model('pay_offline')->update($data, [['id', '=', $offline_pay_info['id']]]); + } + + //支付信息修改 + $update_data = ['pay_type' => self::PAY_TYPE, 'pay_status' => PayModel::PAY_STATUS_IN_PROCESS]; + $pay_model->edit($update_data, [['out_trade_no', '=', $out_trade_no]]); + $pay_info = array_merge($pay_info, $update_data); + + //具体业务处理 + event('OfflinePay', $pay_info); + + //发送消息 + $message_model = new Message(); + $message_model->sendMessage(['keywords' => 'OFFLINEPAY_WAIT_AUDIT', 'pay_info' => $pay_info, 'site_id' => $pay_info['site_id']]); + + model('pay_offline')->commit(); + return $this->success(); + }catch(\Exception $e){ + model('pay_offline')->rollback(); + return $this->error(['file' => $e->getFile(), 'line' => $e->getLine(), 'message' => $e->getMessage()], $e->getMessage()); + } + } + + /** + * 审核通过 + * @param $condition + * @return array|mixed|null + */ + public function auditPass($condition) + { + $offline_pay_info = $this->getInfo($condition)['data']; + if(empty($offline_pay_info)) return $this->error(null, '支付信息有误'); + if($offline_pay_info['status'] != self::STATUS_WAIT_AUDIT) return $this->error(null, '不是待审核状态'); + + model('pay_offline')->startTrans(); + try{ + model('pay_offline')->update([ + 'status' => self::STATUS_AUDIT_PASS, + 'update_time' => time(), + ], $condition); + + $pay_model = new PayModel(); + $pay_res = $pay_model->onlinePay($offline_pay_info['out_trade_no'], self::PAY_TYPE, $offline_pay_info['out_trade_no'], 'offlinepay'); + if($pay_res['code'] < 0){ + model('pay_offline')->rollback(); + return $pay_res; + } + + model('pay_offline')->commit(); + return $this->success(); + }catch(\Exception $e){ + model('pay_offline')->rollback(); + return $this->error(['file' => $e->getFile(), 'line' => $e->getLine(), 'message' => $e->getMessage()], $e->getMessage()); + } + } + + /** + * 审核拒绝 + * @param $condition + * @param $audit_remark + * @return array + */ + public function auditRefuse($condition, $audit_remark) + { + $offline_pay_info = $this->getInfo($condition)['data']; + if(empty($offline_pay_info)) return $this->error(null, '支付信息有误'); + if($offline_pay_info['status'] != self::STATUS_WAIT_AUDIT) return $this->error(null, '不是待审核状态'); + + $pay_model = new PayModel(); + $pay_info = $pay_model->getPayInfo($offline_pay_info['out_trade_no'])['data']; + if(empty($pay_info)) return $this->error(null, '支付信息有误'); + + model('pay_offline')->update([ + 'status' => self::STATUS_AUDIT_REFUSE, + 'audit_remark' => $audit_remark, + 'update_time' => time(), + ], $condition); + + //发送消息 + $message_model = new Message(); + $message_model->sendMessage(['keywords' => 'OFFLINEPAY_AUDIT_REFUSE', 'pay_info' => $pay_info, 'site_id' => $pay_info['site_id']]); + + return $this->success(); + } + + /** + * 订单关闭取消处理 + * @param $condition + * @return array + */ + public function close($condition) + { + $offline_pay_info = $this->getInfo($condition)['data']; + if(empty($offline_pay_info)) return $this->success(); + if($offline_pay_info['status'] == self::STATUS_AUDIT_PASS) return $this->error(null, '线下支付审核通过不可以关闭'); + if($offline_pay_info['status'] == self::STATUS_WAIT_AUDIT) return $this->error(null, '线下支付单据审核中不可以关闭'); + + model('pay_offline')->update([ + 'status' => self::STATUS_CLOSE, + 'update_time' => time(), + ], $condition); + + return $this->success(); + } + + /** + * 退款 + * @param $out_trade_no + * @param $refund_money + * @return array + */ + public function refund($out_trade_no, $refund_money) + { + $offline_pay_info = $this->getInfo([['out_trade_no', '=', $out_trade_no]])['data']; + if(empty($offline_pay_info)) return $this->error(null, '线下支付信息有误'); + + $pay_model = new PayModel(); + $pay_info = $pay_model->getPayInfo($out_trade_no)['data']; + if(empty($pay_info)) return $this->error(null, '支付信息有误'); + + $member_account_model = new MemberAccount(); + return $member_account_model->addMemberAccount($pay_info['site_id'], $offline_pay_info['member_id'], AccountDict::balance_money, $refund_money, 'refund', $pay_info['relate_id'], '订单退款返还!'); + } + + /** + * 获取信息 + * @param $condition + * @param $field + * @return array + */ + public function getInfo($condition, $field = '*') + { + $info = model('pay_offline')->getInfo($condition, $field); + $info = $this->handleInfo($info); + return $this->success($info); + } + + /** + * 获取分页列表 + * @param $condition + * @param $page + * @param $page_size + * @param $order + * @param $field + * @param $alias + * @param $join + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getPageList($condition = [], $page = 1, $page_size = PAGE_LIST_ROWS, $order = '', $field = '*', $alias = 'a', $join = []) + { + $res = model('pay_offline')->pageList($condition, $field, $order, $page, $page_size, $alias, $join); + foreach($res['list'] as $key=>$val){ + $res['list'][$key] = $this->handleInfo($val); + } + return $this->success($res); + } + + /** + * 获取列表 + * @param $condition + * @param $order + * @param $field + * @param $alias + * @param $join + * @param $group + * @return array + */ + public function getList($condition, $order = '', $field = '*', $alias = 'a', $join = [], $group = '') + { + $list = model('pay_offline')->getList($condition, $field, $order, $alias, $join, $group); + foreach($list as $key=>$val){ + $list[$key] = $this->handleInfo($val); + } + return $this->success($list); + } + + public function handleInfo($info) + { + if(isset($info['status'])){ + $status_list = self::getStatus('id'); + $info['status_info'] = $status_list[$info['status']] ?? null; + } + return $info; + } + + /** + * 处理用户订单信息 + * @param $order_info + * @return array + */ + public function handleMemberOrderInfo($order_info) + { + //字段检测 + $fields = ['order_status','pay_type','out_trade_no','action']; + foreach($fields as $field){ + if(!isset($order_info[$field])){ + return $order_info; + } + } + + if($order_info['order_status'] == OrderCommon::ORDER_CREATE && $order_info['pay_type'] == self::PAY_TYPE){ + $offline_pay_info = $this->getInfo([['out_trade_no', '=', $order_info['out_trade_no']]])['data']; + if(!empty($offline_pay_info)){ + $order_info['offline_pay_info'] = $offline_pay_info; + if(in_array($offline_pay_info['status'], [self::STATUS_WAIT_AUDIT, self::STATUS_AUDIT_REFUSE])){ + foreach($order_info['action'] as $key=>$val){ + if($val['action'] == 'orderPay'){ + unset($order_info['action'][$key]); + } + } + } + if($offline_pay_info['status'] == self::STATUS_WAIT_AUDIT){ + $order_info['order_status_name'] = '待审核'; + }else if($offline_pay_info['status'] == self::STATUS_AUDIT_REFUSE){ + $order_info['order_status_name'] = '审核拒绝'; + $order_info['action'][] = [ + 'action' => 'orderOfflinePay', + 'title' => '线下支付', + 'color' => '', + ]; + } + $order_info['action'] = array_values($order_info['action']); + } + } + return $order_info; + } + + /** + * 处理用户订单信息 + * @param $order_info + * @return array + */ + public function handleAdminOrderInfo($order_info) + { + //字段检测 + $fields = ['order_status','order_status_action','pay_type','out_trade_no']; + foreach($fields as $field){ + if(!isset($order_info[$field])){ + return $order_info; + } + } + + if($order_info['order_status'] == OrderCommon::ORDER_CREATE){ + $order_status_info = json_decode($order_info['order_status_action'], true); + if($order_info['pay_type'] == self::PAY_TYPE){ + $offline_pay_info = $this->getInfo([['out_trade_no', '=', $order_info['out_trade_no']]])['data']; + $order_info['offline_pay_info'] = $offline_pay_info; + if(!empty($offline_pay_info)){ + if($offline_pay_info['status'] == self::STATUS_WAIT_AUDIT){ + $order_info['order_status_name'] = '待审核'; + $order_status_info['action'][] = [ + 'action' => 'offlinePayAudit', + 'title' => '支付审核', + 'color' => '', + ]; + }else if($offline_pay_info['status'] == self::STATUS_AUDIT_REFUSE){ + $order_info['status_name'] = '审核拒绝'; + } + } + }else{ + $order_status_info['action'][] = [ + 'action' => 'offlinePay', + 'title' => '线下支付', + 'color' => '', + ]; + } + $order_info['order_status_action'] = json_encode($order_status_info); + } + return $order_info; + } + + /** + * 发送消息 + * @param $param + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function messageWaitAudit($param) + { + $pay_info = $param['pay_info']; + $sms_model = new Sms(); + $wechat_model = new WechatMessage(); + + $shop_accept_message_model = new ShopAcceptMessage(); + $list = $shop_accept_message_model->getShopAcceptMessageList([['site_id', '=', $pay_info['site_id']]])['data']; + if (!empty($list)) { + foreach ($list as $v) { + if(!empty($v['mobile'])){ + $message_data = [ + 'var_parse' => [ + 'order_name' => str_sub(replaceSpecialChar($pay_info[ 'pay_body' ]), 25), + 'pay_money' => $pay_info['pay_money'], + 'out_trade_no' => $pay_info['out_trade_no'], + ], + 'sms_account' => $v[ 'mobile' ], + ]; + $sms_model->sendMessage(array_merge($param, $message_data)); + } + + if (!empty($v[ 'wx_openid' ])) { + $message_data = [ + 'openid' => $v[ 'wx_openid' ], + 'template_data' => [ + 'thing10' => str_sub($pay_info['pay_body'], 20), + 'amount4' => $pay_info['pay_money'], + 'character_string1' => $pay_info['out_trade_no'], + ], + 'page' => '', + ]; + $wechat_model->sendMessage(array_merge($param, $message_data)); + } + } + } + } + + /** + * 发送消息 + * @param $param + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function messageAuditRefuse($param) + { + $pay_info = $param['pay_info']; + $sms_model = new Sms(); + $wechat_model = new WechatMessage(); + $weapp_model = new WeappMessage(); + + $member_model = new Member(); + $member_info = $member_model->getMemberInfo([ [ 'member_id', '=', $pay_info[ 'member_id' ] ] ])[ 'data' ]; + + if(!empty($member_info['mobile'])){ + $message_data = [ + 'var_parse' => [ + 'order_name' => str_sub(replaceSpecialChar($pay_info[ 'pay_body' ]), 25), + 'pay_money' => $pay_info['pay_money'], + 'out_trade_no' => $pay_info['out_trade_no'], + ], + 'sms_account' => $member_info[ 'mobile' ], + ]; + $sms_model->sendMessage(array_merge($param, $message_data)); + } + + if (!empty($member_info[ 'wx_openid' ])) { + $message_data = [ + 'openid' => $member_info[ 'wx_openid' ], + 'template_data' => [ + 'thing10' => str_sub($pay_info['pay_body'], 20), + 'amount6' => $pay_info['pay_money'], + 'character_string5' => $pay_info['out_trade_no'], + ], + 'page' => 'pages/order/detail?order_id='.$pay_info['relate_id'], + ]; + $wechat_model->sendMessage(array_merge($param, $message_data)); + } + + if (!empty($member_info[ 'weapp_openid' ])) { + $message_data = [ + 'openid' => $member_info[ 'weapp_openid' ], + 'template_data' => [ + 'thing2' => [ + 'value' => str_sub($pay_info['pay_body'], 20) + ], + 'amount3' => [ + 'value' => $pay_info['pay_money'] + ], + 'character_string1' => [ + 'value' => $pay_info['out_trade_no'] + ], + 'thing5' => [ + 'value' => '线下支付审核拒绝' + ], + ], + 'page' => 'pages/order/detail?order_id='.$pay_info['relate_id'], + ]; + $weapp_model->sendMessage(array_merge($param, $message_data)); + } + } +} diff --git a/src/addon/offlinepay/shop/controller/Pay.php b/src/addon/offlinepay/shop/controller/Pay.php new file mode 100644 index 000000000..d60932730 --- /dev/null +++ b/src/addon/offlinepay/shop/controller/Pay.php @@ -0,0 +1,172 @@ +isJson()) { + $data = [ + 'pay_status' => input('pay_status', 0),//支付状态 + 'bank' => [ + 'status' => input('bank_status', 0),//是否开启 + 'bank_name' => input('bank_bank_name', ''),//银行名称 + 'account_name' => input('bank_account_name', ''),//账户名称 + 'account_number' => input('bank_account_number', ''),//账号 + 'branch_name' => input('bank_branch_name', ''),//支行名称 + ], + 'wechat' => [ + 'status' => input('wechat_status', 0),//是否开启 + 'account_name' => input('wechat_account_name', ''),//账户名称 + 'payment_code' => input('wechat_payment_code', ''),//收款码 + ], + 'alipay' => [ + 'status' => input('alipay_status', 0),//是否开启 + 'account_name' => input('alipay_account_name', ''),//账户名称 + 'payment_code' => input('alipay_payment_code', ''),//收款码 + ], + ]; + $result = $config_model->setPayConfig($data, $this->site_id, $this->app_module); + return $result; + } else { + $config_info = $config_model->getPayConfig($this->site_id, $this->app_module)[ 'data' ][ 'value' ]; + $this->assign("config_info", $config_info); + return $this->fetch("pay/config"); + } + } + + public function lists() + { + if (request()->isJson()) { + $page_index = input('page', 1); + $page_size = input('page_size', PAGE_LIST_ROWS); + $status = input('status', 0); + $search_field = input('search_field', ''); + $search_field_value = input('search_field_value', ''); + $out_trade_no = input('out_trade_no', ''); + + $alias = 'po'; + $join = [ + ['member m', 'm.member_id = po.member_id', 'left'], + ['pay p', 'p.out_trade_no = po.out_trade_no', 'left'], + ]; + $field = [ + 'po.*', + 'm.nickname,m.mobile', + 'p.pay_detail,p.pay_money,p.event,p.relate_id', + ]; + $condition = []; + if($status !== 'all'){ + $condition[] = ['po.status', '=', $status]; + } + if($search_field_value != ''){ + $condition[] = [$search_field, 'like', '%'.$search_field_value.'%']; + } + if($out_trade_no){ + $condition[] = ['po.out_trade_no', '=', $out_trade_no]; + } + $order = 'po.create_time desc'; + + + $pay_model = new PayModel(); + $res = $pay_model->getPageList($condition, $page_index, $page_size, $order, $field, $alias, $join); + + //各种状态统计 + foreach ($condition as $key=>$val){ if($val[0] == 'po.status') unset($condition[$key]); } + $condition = array_values($condition); + $status_num_list = $pay_model->getList($condition, '', 'count(*) as num, po.status', $alias, $join, 'po.status')['data']; + + $status_num_data = array_column($status_num_list, 'num', 'status'); + $res['data']['status_num_data'] = $status_num_data; +$res['c'] = $condition; + return $res; + } else { + $status_list = PayModel::getStatus(); + $this->assign('status_list', $status_list); + + $out_trade_no = input('out_trade_no', ''); + $this->assign('out_trade_no', $out_trade_no); + + return $this->fetch('pay/lists'); + } + } + + public function auditPass() + { + if(request()->isJson()){ + $id = input('id', 0); + $pay_model = new PayModel(); + return $pay_model->auditPass([['id', '=', $id]]); + } + } + + public function auditRefuse() + { + if(request()->isJson()){ + $id = input('id', 0); + $audit_remark = input('audit_remark', ''); + $pay_model = new PayModel(); + return $pay_model->auditRefuse([['id', '=', $id]], $audit_remark); + } + } + + public function pay() + { + if(request()->isJson()){ + $imgs = input('imgs', ''); + $desc = input('desc', ''); + $out_trade_no = input('out_trade_no', ''); + $member_id = input('member_id', 0); + $pay_model = new PayModel(); + //支付 + $pay_res = $pay_model->pay([ + 'member_id' => $member_id, + 'out_trade_no' => $out_trade_no, + 'imgs' => $imgs, + 'desc' => $desc, + ]); + if($pay_res['code'] < 0) return $pay_res; + //审核 + $audit_res = $pay_model->auditPass([ + ['out_trade_no', '=', $out_trade_no], + ['member_id', '=', $member_id], + ]); + return $audit_res; + }else{ + $out_trade_no = input('out_trade_no', ''); + $this->assign("out_trade_no", $out_trade_no); + $member_id = input('member_id', 0); + $this->assign("member_id", $member_id); + return $this->fetch("pay/pay"); + } + } + + public function test() + { + $out_trade_no = '171997599310581711000'; + $member_id = 171; + $imgs = join(',', [ + 'http://b2cv4.com/upload/1/common/images/20240618/20240618105545171867934599817_BIG.jpg', + 'http://b2cv4.com/upload/1/common/goods_grab/images/20240527/20240527032610171679477043213_BIG.jpg', + 'http://b2cv4.com/upload/1/common/images/20240618/20240618105545171867934599817_BIG.jpg', + ]); + $desc = '支付了33333次'; + $pay_model = new PayModel(); + $res = $pay_model->pay([ + 'member_id' => $member_id, + 'out_trade_no' => $out_trade_no, + 'imgs' => $imgs, + 'desc' => $desc, + ]); + dd($res); + } +} \ No newline at end of file diff --git a/src/addon/offlinepay/shop/view/pay/config.html b/src/addon/offlinepay/shop/view/pay/config.html new file mode 100644 index 000000000..02f35d845 --- /dev/null +++ b/src/addon/offlinepay/shop/view/pay/config.html @@ -0,0 +1,193 @@ + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
请输入真实姓名,用于支付时确认收款人
+
+
+ +
+
+
请上传正方形的二维码,截图裁剪时尽量贴着二维码,不要有多余的空白
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
请输入真实姓名,用于支付时确认收款人
+
+
+ +
+
+
请上传正方形的二维码,截图裁剪时尽量贴着二维码,不要有多余的空白
+
+
+ +
+ + +
+
+ + \ No newline at end of file diff --git a/src/addon/offlinepay/shop/view/pay/lists.html b/src/addon/offlinepay/shop/view/pay/lists.html new file mode 100644 index 000000000..600552c9d --- /dev/null +++ b/src/addon/offlinepay/shop/view/pay/lists.html @@ -0,0 +1,356 @@ + + +
+
+
+
+
+ +
+ +
+
 
+
+ +
+
+
+ + + +
+ + +
+
+
+
+ +
+
    + {foreach $status_list as $key=>$status_info} +
  • {$status_info.name}(0)
  • + {/foreach} +
  • 全部
  • +
+
+
+
+
+ + + + + + + diff --git a/src/addon/offlinepay/shop/view/pay/pay.html b/src/addon/offlinepay/shop/view/pay/pay.html new file mode 100644 index 000000000..5a94c9d59 --- /dev/null +++ b/src/addon/offlinepay/shop/view/pay/pay.html @@ -0,0 +1,73 @@ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+ + +
+ +
+
+ + \ No newline at end of file diff --git a/src/app/common.php b/src/app/common.php index 322b37b33..163d98f4d 100644 --- a/src/app/common.php +++ b/src/app/common.php @@ -321,7 +321,6 @@ function date_to_time($date) /** * 获取唯一随机字符串 - * 创建时间:2018年8月7日15:54:16 */ function unique_random($len = 10) { @@ -375,6 +374,8 @@ function http($url, $timeout = 30, $header = array ()) curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727;)'); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 关闭 SSL 证书校验 + if (!empty($header)) { curl_setopt($ch, CURLOPT_HTTPHEADER, $header); } @@ -423,9 +424,8 @@ function replace_array_element($array, $replace) /** * 过滤特殊符号 - * 创建时间:2018年1月30日15:39:32 - * @param unknown $string - * @return mixed + * @param $string + * @return array|string|string[]|null */ function ihtmlspecialchars($string) { @@ -693,13 +693,13 @@ function success($code = 0, $message = '', $data = '') /** * 实例化Model - * - * @param string $name - * Model名称 + * @param string $table + * @param array $option + * @return \app\model\Model */ -function model($table = '') +function model($table = '', $option = []) { - return new \app\model\Model($table); + return new \app\model\Model($table, $option); } /** @@ -1164,12 +1164,27 @@ function string_split($string, $delimiter, $value) * $str为要进行截取的字符串,$length为截取长度(汉字算一个字,字母算半个字 * @param $str * @param int $length - * @param bool $is_need_apostrophe + * @param boolean $is_need_apostrophe 是否需要省略号 + * @param string $apostrophe_pos 省略号位置 end 结尾 center 中间 * @return string */ -function str_sub($str, $length = 10, $is_need_apostrophe = true) +function str_sub($str, $length = 10, $is_need_apostrophe = false, $apostrophe_pos = 'end') { - return mb_substr($str, 0, $length, 'UTF-8') . ( $is_need_apostrophe ? '...' : '' ); + $encoding = 'UTF-8'; + $res_str = $str; + if(mb_strlen($str, $encoding) > $length){ + if($is_need_apostrophe){ + if($apostrophe_pos == 'end'){ + $res_str = mb_substr($str, 0, $length, $encoding) . '...'; + }else{ + $half_length = floor($length / 2); + $res_str = mb_substr($str, 0, $half_length, $encoding) . '...' . mb_substr($str, mb_strlen($str, $encoding) - $half_length, $half_length, $encoding); + } + }else{ + $res_str = mb_substr($str, 0, $length, 'UTF-8'); + } + } + return $res_str; } /** @@ -1879,6 +1894,199 @@ function paramFilter($param) return preg_replace($filter_rule, '', $param); } +//最小公倍数 +function getLeastCommonMultiple($a, $b) { + return $a * $b / getGreatestCommonDivisor($a, $b); +} +//最大公约数 +function getGreatestCommonDivisor($a, $b) { + if ($b === 0) return $a; + return getGreatestCommonDivisor($b, $a % $b); +} + + +/** + * 关联数组变为索引数组 + * @param array $list + * @return array + */ +function keyArrToIndexArr($list, $child = 'children'){ + $list = array_values($list); + foreach($list as $key=>$val){ + if(isset($val[$child]) && !empty($val[$child])){ + $list[$key][$child] = keyArrToIndexArr($val[$child], $child); + } + } + return $list; +} + +/** + * 索引数组变为关联数组 + * @param $list + * @param $pk + * @param string $child + * @return array + */ +function indexArrToKeyArr($list, $pk, $child = 'children'){ + $new_list = []; + foreach($list as $val){ + if(isset($val[$child]) && !empty($val[$child])){ + $val[$child] = indexArrToKeyArr($val[$child], $pk, $child); + } + if(isset($val[$pk])){ + $new_list[$val[$pk]] = $val; + } + } + return $new_list; +} + +/** + * 获取树的末端节点 + * @param $list + * @param $pk + * @param $child + * @return array + */ +function getTreeLeaf($list, $pk = 'id', $child = 'child') +{ + $leaf_arr = []; + foreach($list as $val){ + if(empty($val[$child])){ + $leaf_arr[] = $val[$pk]; + }else{ + $leaf_arr = array_merge($leaf_arr, getTreeLeaf($val[$child], $pk, $child)); + } + } + return $leaf_arr; +} + +/** + * 覆盖数据 + * @param $source_data + * @param $target_data + * @return mixed + */ +function assignData($source_data, $target_data) +{ + if(is_array($target_data)){ + foreach($target_data as $key=>$val){ + if(isset($source_data[$key])){ + $target_data[$key] = assignData($source_data[$key], $val); + } + } + }else{ + $target_data = $source_data; + } + return $target_data; +} + +//使用htmlpurifier防范xss攻击 +function removeXss($string) +{ + //相对index.php入口文件,引入HTMLPurifier.auto.php核心文件 + //require_once './plugins/htmlpurifier/HTMLPurifier.auto.php'; + // 生成配置对象 + $cfg = HTMLPurifier_Config::createDefault(); + // 以下就是配置: + $cfg->set('Core.Encoding', 'UTF-8'); + // 设置允许使用的HTML标签 + $cfg->set('HTML.Allowed', 'div,b,strong,i,em,a[href|title],ul,ol,li,br,p[style],span[style],img[width|height|alt|src],table,tbody,tr[class],th,td[width|valign|style]'); + // 设置允许出现的CSS样式属性 + //$cfg->set('CSS.AllowedProperties', 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align'); + // 设置a标签上是否允许使用target="_blank" + $cfg->set('HTML.TargetBlank', TRUE); + // 使用配置生成过滤用的对象 + $obj = new HTMLPurifier($cfg); + // 过滤字符串 + $array = json_decode($string, true); + if(is_array($array)){ + $array = recursiveDealWithArrayString($array, function ($str) use($obj){ + return $obj->purify($str); + }); + $string = json_encode($array, JSON_UNESCAPED_UNICODE); + }else{ + $string = $obj->purify($string); + } + return $string; +} + +/** + * 递归处理数组中的字符串 + * @param $array + * @param $callback + * @return mixed + */ +function recursiveDealWithArrayString($array, $callback){ + foreach($array as $key=>$val){ + if(is_string($val)){ + $val = $callback($val); + }else if(is_array($val)){ + $val = recursiveDealWithArrayString($val, $callback); + } + $array[$key] = $val; + } + return $array; +} + +function exceptionData(\Exception $e){ + return [ + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'message' => $e->getMessage(), + ]; +} + +function getCertKey($str) +{ + return require "extend/cert/".$str.".php"; +} + +if(!function_exists('http_url')){ + function http_url($url,$data,$headers = [],$type = 'POST') { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $type); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_URL, $url); + $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.110 Safari/537.36'; + curl_setopt($ch, CURLOPT_USERAGENT, $user_agent); # 在HTTP请求中包含一个"User-Agent: "头的字符串,声明用什么浏览器来打开目标网页 + if(!empty($headers)) { + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + } + if (1 == strpos("$".$url, "https://")) + { + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + } + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_AUTOREFERER, true); + curl_setopt($ch, CURLOPT_ENCODING, ''); + if($type == 'POST') { + curl_setopt($ch, CURLOPT_POST, true); + if(is_array($data)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); + }else { + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + } + } + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // ssl 访问核心参数 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // ssl 访问核心参数 + $result = curl_exec($ch); + curl_close($ch); + return $result; + } + + function secondsToTime($seconds) { + $hours = floor($seconds / 3600); + $minutes = floor(($seconds % 3600) / 60); + $seconds = $seconds % 60; + + // 格式化为两位数 + return sprintf("%02d:%02d:%02d", $hours, $minutes, $seconds); + } +} + /** * 格式化数据为日志友好的字符串(保持中文可读性) *