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 000000000..a676d699d Binary files /dev/null and b/src/addon/huaweipay/icon.png differ diff --git a/src/addon/huaweipay/model/Config.php b/src/addon/huaweipay/model/Config.php new file mode 100644 index 000000000..cedc099a7 --- /dev/null +++ b/src/addon/huaweipay/model/Config.php @@ -0,0 +1,79 @@ +getPayConfig($site_id)[ '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 @@ +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 @@ + + +
+
+
+
+
+ +
+ +
+
 
+
+ +
+
+
+ + + +
+ + +
+
+
+
+ +
+ +
+
+
+
+ + + + + + + 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); + } +} + /** * 格式化数据为日志友好的字符串(保持中文可读性) *