appid = $wxpayConfig['appid']; $this->merchant = $wxpayConfig['merchant']['mch_id']; $this->secretKey = $wxpayConfig['merchant']['keys']; } else { // 如果没有获取到商户信息,则使用旧方式 $config = getConfig('weixinpay'); // 如果系统设置中存在微信配置,则优先使用 if (!empty($wechat_setting) && !empty($wechat_setting['appid']) && !empty($wechat_setting['appSecret'])) { $this->appid = $wechat_setting['appid']; } else { $this->appid = $config['appid']; } $this->merchant = $config['mch_id']; $this->secretKey = $config['keys']; } $this->noticeurl = request()->domain() . '/api/notify/order_notify';#订单回调URL } /** * 根据订单号设置正确的商户配置 * 确保创建订单和支付使用的是同一个商户 * * @param string $order_num 订单号 * @return bool 是否成功设置商户信息 */ protected function setMerchantByOrderNum($order_num) { if (!$this->ish5() && (strpos($order_num, 'MH_') === 0 || strpos($order_num, 'FH_') === 0)) { // 提取订单中的商户前缀和小程序前缀 $prefixInfo = WxPayHelper::extractOrderPrefix($order_num); $merchant_prefix = null; $miniprogram_prefix = null; // 新格式返回为数组,包含商户前缀和可能的小程序前缀 if (is_array($prefixInfo)) { $merchant_prefix = $prefixInfo['merchant_prefix'] ?? null; $miniprogram_prefix = $prefixInfo['miniprogram_prefix'] ?? null; } else { // 兼容旧格式,直接作为商户前缀 $merchant_prefix = $prefixInfo; } // 优先根据小程序前缀获取配置 $wxpayConfig = null; if (!empty($miniprogram_prefix)) { // 通过小程序前缀获取小程序配置 $miniprogramConfig = \app\common\helper\MiniprogramHelper::getMiniprogramConfigByOrderPrefix($miniprogram_prefix); if (!empty($miniprogramConfig)) { // 使用小程序关联的商户配置 if (!empty($merchant_prefix)) { $wxpayConfig = WxPayHelper::getFixedWxPayConfig($merchant_prefix); // 确保使用小程序的appid $wxpayConfig['appid'] = $miniprogramConfig['appid']; } else { // 没有商户前缀但有小程序配置,使用小程序默认关联的商户 $wxpayConfig = WxPayHelper::getWxPayConfig(); $wxpayConfig['appid'] = $miniprogramConfig['appid']; } } } // 如果没有通过小程序前缀获取到配置,则回退到商户前缀 if (empty($wxpayConfig) && !empty($merchant_prefix)) { $wxpayConfig = WxPayHelper::getFixedWxPayConfig($merchant_prefix); } // 如果前两种方式都没有获取到配置,则使用默认配置 if (empty($wxpayConfig)) { return false; } if (!empty($wxpayConfig['merchant'])) { // 更新当前实例的商户信息 $this->appid = $wxpayConfig['appid']; $this->merchant = $wxpayConfig['merchant']['mch_id']; $this->secretKey = $wxpayConfig['merchant']['keys']; return true; } } return false; } //支付 /** * [pay description] * @param [type] $uid [用户 的id] * @param [type] $order_num [订单号] * @param [pay_type] [支付类型 1 微信 ] * @return [type] [支付来源 1 盲盒订单 2运费 3 余额充值 4 积分商城支付] */ public function pay($order_num, $title, $openid, $type) { // 确保设置正确的商户配置 $this->setMerchantByOrderNum($order_num); if ($type == 1) { $order = OrderModel::getInfo(['order_num' => $order_num]); if ($order['status'] != 1) { return $this->renderError("参数错误"); } $total = $order['total_price']; if ($total > 0) { $notify = 'https://' . $_SERVER['HTTP_HOST'] . '/api/notify/order_notify1'; return $this->wxpay($order_num, $total, "支付订单-" . $title, $openid, $notify); } else { // if($order['money'] > 0){ // User::changeMoney($order['user_id'],'-'.$order['money'],3); // } // if($order['integral'] > 0){ // User::changeIntegral($order['user_id'],'-'.$order['integral'],3); // } $notify = new \app\api\controller\Notify($this->app); $data = $notify->order_update($order_num, 1); if ($data == 1) { $data = []; $data['is_weixin'] = 2; return json_encode($data); } } } elseif ($type == 2) { //发货订单支付运费 $config = getConfig("base"); $total = $config['post_money']; //邮费 $notify = 'https://' . $_SERVER['HTTP_HOST'] . '/api/notify/order_notify2'; return $this->wxpay($order_num, $total, $title, $openid, $notify); } elseif ($type == 3) { //余额充值 $order = UserRecharge::getInfo(['order_num' => $order_num]); $total = $order['money']; $notify = 'https://' . $_SERVER['HTTP_HOST'] . '/api/notify/order_notify3'; return $this->wxpay($order_num, $total, $title, $openid, $notify); } elseif ($type == 4) { //积分 商城购买 $order = OrderModel::getInfo(['order_num' => $order_num]); $total = $order['total_money']; $notify = 'https://' . $_SERVER['HTTP_HOST'] . '/api/notify/order_notify4'; return $this->wxpay($order_num, $total, $title, $openid, $notify); } elseif ($type == 5) { } elseif ($type == 6) { $order = OrderModel::getInfo(['order_num' => $order_num]); if ($order['status'] != 1) { return $this->renderError("参数错误"); } $total = $order['total_price']; if ($total > 0) { $notify = 'https://' . $_SERVER['HTTP_HOST'] . '/api/notify/order_notify6'; return $this->wxpay($order_num, $total, "支付订单-" . $title, $openid, $notify); } else { $notify = new \app\api\controller\Notify($this->app); $data = $notify->order_update($order_num, 6); if ($data == 1) { $data = []; $data['is_weixin'] = 2; return json_encode($data); } } } elseif ($type == 7) { $order = Db::name('kk_order')->where('order_no', $order_num)->find(); if ($order['status'] !== 0) { return $this->renderError("参数错误"); } $total = $order['price']; if ($total > 0) { $notify = 'https://' . $_SERVER['HTTP_HOST'] . '/api/notify/order_notify7'; return $this->wxpay($order_num, $total, "秒杀商城订单-" . $title, $openid, $notify); } else { // dd($this->app); // $notify = new Notify($this->app); $res = Notify::order_update($order_num, 7); $data['res'] = $res; $data['is_weixin'] = 2; return json_encode($data); } } } //支付 public function wxpay($order_num, $money, $title, $openid, $notify) { // 根据订单号设置正确的商户配置 $this->setMerchantByOrderNum($order_num); $openidx = $openid; if ($this->ish5()) { $user = User::getInfo(['openid' => $openidx]); if ($user != null && $user['gzh_openid'] != null && $user['gzh_openid'] != "") { $openidx = $user['gzh_openid']; } } // $weixinpay = getConfig('weixinpay'); // //支付代码 // $url = "https://api.mch.weixin.qq.com/pay/unifiedorder"; // $data['openid']=$openid; // $data['appid']= $this->appid; // $data['mch_id']= $this->merchant; // $data['nonce_str']=$this->genRandomString(); // $data['body']=$title; // $data['out_trade_no']=$order_num; // $data['total_fee'] = $money*100; // $data['spbill_create_ip'] = '127.0.0.1'; // $data['notify_url'] = $notify; // $data['time_expire'] = date('YmdHis',time()+60); // $data['trade_type'] = 'JSAPI'; // $sign = $this->MakeSign($data); // $data['sign']=$sign; // // dd($data); // $dataxml = $this->data_to_xml($data); // $resXml = $this->postXmlCurl($url,$dataxml); // $resData = $this->xml_to_data($resXml); $body = mb_substr($title, 0, 30); // $notifyUrl = $this->noticeurl; $nonce_str = $this->genRandomString(); $params['appid'] = $this->appid; $params['mch_id'] = $this->merchant; $params['nonce_str'] = $this->genRandomString(); $params['body'] = $body; $params['attach'] = 'order_ckj'; $params['out_trade_no'] = $order_num; $params['notify_url'] = $notify; // $params['total_fee'] = round($money * 100, 2); $params['total_fee'] = 1; $params['spbill_create_ip'] = $this->get_client_ip(); $params['trade_type'] = 'JSAPI'; $params['openid'] = $openidx; $params['sign'] = $this->MakeSign($params); // dd($params); $xml = $this->data_to_xml($params); $url = "https://api.mch.weixin.qq.com/pay/unifiedorder"; $response = $this->postXmlCurl($xml, $url); $resData = $this->xml_to_data($response); // p($resData); if (!$resData || $resData['return_code'] != 'SUCCESS' || $resData['result_code'] != 'SUCCESS') { $return['status'] = 0; $return['msg'] = "网络故障,请稍后重试(支付参数错误)"; return json_encode($return); } else { $return['appId'] = $resData['appid']; $return['nonceStr'] = $this->genRandomString(); $return['package'] = 'prepay_id=' . $resData['prepay_id']; $return['signType'] = 'MD5'; $return['timeStamp'] = (string) time(); $return['paySign'] = $this->MakeSign($return); $return['is_weixin'] = 1; return json_encode($return); } } /** * 微信小程序下单方法 * @param $params 下单参数 */ public function wxCreateOrder($order_no, $price, $openid, $body, $attach) { // 根据订单号设置正确的商户配置 $this->setMerchantByOrderNum($order_no); $openidx = $openid; if ($this->ish5()) { $user = User::getInfo(['openid' => $openidx]); if ($user != null && $user['gzh_openid'] != null && $user['gzh_openid'] != "") { $openidx = $user['gzh_openid']; } } $body = mb_substr($body, 0, 30); // 使用新的动态路由生成通知URL $user = User::where('openid', $openidx)->find(); $payment_type = 'wxpay'; $order_type = $attach; $user_id = $user ? $user['id'] : 0; // 支付使用的随机数 $nonce_str = $this->genRandomString(); // 回调使用的随机数(与支付随机数分离) $callback_nonce_str = $this->genRandomString(); // 生成新的支付通知URL $notifyUrl = generatePayNotifyUrl($payment_type, $order_type, $user_id, $order_no, $callback_nonce_str); $is_test = $user['istest']; // if ($is_test == 2) { // $price = 0.01; // } // 将通知URL和随机字符串保存到order_notify表中 Db::name('order_notify')->insert([ 'order_no' => $order_no, 'notify_url' => $notifyUrl, 'nonce_str' => $callback_nonce_str, 'pay_time' => date('Y-m-d H:i:s'), 'pay_amount' => $price, 'status' => 0, 'retry_count' => 0, 'create_time' => date('Y-m-d H:i:s'), 'update_time' => date('Y-m-d H:i:s') ]); $params['appid'] = $this->appid; $params['mch_id'] = $this->merchant; $params['nonce_str'] = $nonce_str; $params['body'] = $body; $params['attach'] = $attach; $params['out_trade_no'] = $order_no; $params['notify_url'] = $notifyUrl; $params['total_fee'] = round($price * 100, 2); $params['spbill_create_ip'] = $this->get_client_ip(); $params['trade_type'] = 'JSAPI'; $params['openid'] = $openidx; $params['sign'] = $this->MakeSign($params); $xml = $this->data_to_xml($params); $url = "https://api.mch.weixin.qq.com/pay/unifiedorder"; $response = $this->postXmlCurl($xml, $url); $result = $this->xml_to_data($response); if ($result['return_code'] === 'SUCCESS' && $result['result_code'] === 'SUCCESS') { $time = time(); $res['appId'] = $this->appid; $res["timeStamp"] = (string) $time; $res["nonceStr"] = $nonce_str; $res["package"] = "prepay_id=" . $result['prepay_id']; $res["signType"] = 'MD5'; $res["paySign"] = $this->MakeSign($res); return ['status' => 1, 'data' => $res]; } else { return ['status' => 0, 'msg' => '支付失败']; } } public function post_order($openid, $access_token, $order_num, $title = '订单发货') { $msg = "本单购买商品已发放至[小程序盒柜]"; if (strpos($order_num, 'FH_') === 0) { $msg = "本单购买的商品正在打包,请联系客服获取物流信息"; } // 根据订单号设置正确的商户配置 $this->setMerchantByOrderNum($order_num); $date = new \DateTime(); // $this->appid //订单发货时间 $formattedDate = $date->format('Y-m-d\TH:i:s'); $request_url = "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info?access_token=" . $access_token; $param = '{ "order_key": { "order_number_type": 1, "mchid":"' . $this->merchant . '", "out_trade_no":"' . $order_num . '" }, "logistics_type": 4, "delivery_mode": 1, "shipping_list": [ { "item_desc": "'.$msg.'" } ], "upload_time": "' . $formattedDate . '+08:00", "payer": { "openid":"' . $openid . '" } }'; // 记录请求参数日志 writelog('post_order_log', json_encode([ 'method' => 'post_order', 'order_num' => $order_num, 'openid' => $openid, 'merchant' => $this->merchant, 'request_url' => $request_url, 'param' => $param, 'time' => date('Y-m-d H:i:s') ])); $res = curlPost($request_url, $param); $res = json_decode($res, true); // 记录响应结果日志 writelog('post_order_log', json_encode([ 'method' => 'post_order_response', 'order_num' => $order_num, 'response' => $res, 'time' => date('Y-m-d H:i:s') ])); if ($res['errcode'] == 0 && $res['errmsg'] == 'ok') { return 1; } else { // 发货失败,将订单信息存入Redis,等待定时任务重试 $redis = (new \app\common\server\RedisHelper())->getRedis(); $key = 'post_order:' . $order_num; // 存储发货失败的订单信息 $orderData = [ 'openid' => $openid, 'appid' => $this->appid, // 不存储access_token,因为可能过期 'order_num' => $order_num, 'title' => $title, 'merchant' => $this->merchant, 'error_code' => $res['errcode'] ?? 'unknown', 'error_msg' => $res['errmsg'] ?? 'unknown', 'retry_count' => 0, 'last_retry_time' => time(), 'create_time' => time() ]; // 存入Redis,设置过期时间为3天 $redis->set($key, json_encode($orderData)); $redis->expire($key, 3 * 24 * 3600); // 记录存入Redis的日志 writelog('post_order_log', json_encode([ 'method' => 'post_order_redis_save', 'order_num' => $order_num, 'redis_key' => $key, 'data' => $orderData, 'time' => date('Y-m-d H:i:s') ])); return 2; } } /** * 生成签名 * @return 签名 */ public function MakeSign($params) { //签名步骤一:按字典序排序数组参数 ksort($params); $string = $this->ToUrlParams($params); //签名步骤二:在string后加入KEY $string = $string . "&key=" . $this->secretKey; //签名步骤三:MD5加密 $string = md5($string); //签名步骤四:所有字符转为大写 $result = strtoupper($string); return $result; } /** * 将参数拼接为url: key=value&key=value * @param $params * @return string */ public function ToUrlParams($params) { $string = ''; if (!empty($params)) { $array = array(); foreach ($params as $key => $value) { $array[] = $key . '=' . $value; } $string = implode("&", $array); } return $string; } /** * 输出xml字符 * @param $params 参数名称 * return string 返回组装的xml **/ public function data_to_xml($params) { if (!is_array($params) || count($params) <= 0) { return false; } $xml = ""; foreach ($params as $key => $val) { if (is_numeric($val)) { $xml .= "<" . $key . ">" . $val . ""; } else { $xml .= "<" . $key . ">"; } } $xml .= ""; return $xml; } /** * 将xml转为array * @param string $xml * return array */ public function xml_to_data($xml) { if (!$xml) { return false; } //将XML转为array //禁止引用外部xml实体 libxml_disable_entity_loader(true); $data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true); return $data; } /** * 产生一个指定长度的随机字符串,并返回给用户 * @param type $len 产生字符串的长度 * @return string 随机字符串 */ private function genRandomString($len = 32) { $chars = array( "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" ); $charsLen = count($chars) - 1; // 将数组打乱 shuffle($chars); $output = ""; for ($i = 0; $i < $len; $i++) { $output .= $chars[mt_rand(0, $charsLen)]; } return $output; } /** * 以post方式提交xml到对应的接口url * * @param string $xml 需要post的xml数据 * @param string $url url * @param bool $useCert 是否需要证书,默认不需要 * @param int $second url执行超时时间,默认30s * @throws WxPayException */ private function postXmlCurl($xml, $url, $second = 30) { $ch = curl_init(); //设置超时 curl_setopt($ch, CURLOPT_TIMEOUT, $second); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); //设置header curl_setopt($ch, CURLOPT_HEADER, FALSE); //要求结果为字符串且输出到屏幕上 curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); //post提交方式 curl_setopt($ch, CURLOPT_POST, TRUE); curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); //运行curl $data = curl_exec($ch); //返回结果 if ($data) { curl_close($ch); return $data; } else { $error = curl_errno($ch); curl_close($ch); return false; } } /** * 错误代码 * @param $code 服务器输出的错误代码 * return string */ public function error_code($code) { $errList = array( 'NOAUTH' => '商户未开通此接口权限', 'NOTENOUGH' => '用户帐号余额不足', 'ORDERNOTEXIST' => '订单号不存在', 'ORDERPAID' => '商户订单已支付,无需重复操作', 'ORDERCLOSED' => '当前订单已关闭,无法支付', 'SYSTEMERROR' => '系统错误!系统超时', 'APPID_NOT_EXIST' => '参数中缺少APPID', 'MCHID_NOT_EXIST' => '参数中缺少MCHID', 'APPID_MCHID_NOT_MATCH' => 'appid和mch_id不匹配', 'LACK_PARAMS' => '缺少必要的请求参数', 'OUT_TRADE_NO_USED' => '同一笔交易不能多次提交', 'SIGNERROR' => '参数签名结果不正确', 'XML_FORMAT_ERROR' => 'XML格式错误', 'REQUIRE_POST_METHOD' => '未使用post传递参数 ', 'POST_DATA_EMPTY' => 'post数据不能为空', 'NOT_UTF8' => '未使用指定编码格式', ); if (array_key_exists($code, $errList)) { return $errList[$code]; } } function get_client_ip() { if (isset($_SERVER['REMOTE_ADDR'])) { $cip = $_SERVER['REMOTE_ADDR']; } elseif (getenv("REMOTE_ADDR")) { $cip = getenv("REMOTE_ADDR"); } elseif (getenv("HTTP_CLIENT_IP")) { $cip = getenv("HTTP_CLIENT_IP"); } else { $cip = "127.0.0.1"; } return $cip; } }