/** * 发起充值,获取充值参数 */public function pay() { // step1 验证参数 if (!$config_id = $_POST['config_id']) { $this->json->E('缺少参数'); } $recharge_config = M('recharge_config'); $recharge_config_info = $recharge_config->where(['id'=>$config_id,'deleted'=>0,'is_show'=>1])->find(); if (!$recharge_config_info) { $this->json->E('充值项不存在'); } // step2 创建订单 $order_num = Func::createOrderNum(); $add_data = [ 'order_num' => $order_num, 'amount' => $recharge_config_info['recharge'], 'year' => $recharge_config_info['year'], 'company_uid' => $this->uid, 'status' => 1, // 未支付 'create_time' => time(), ]; $recharge_order = M('recharge_order'); $add_order_flag = $recharge_order->add($add_data); if (!$add_order_flag) { $this->json->E('创建支付订单失败,请重试'); } // step3 生成支付参数 $company_user = M('company_user'); $company_user_info = $company_user->where(['id'=>$this->uid])->find(); $openid = $company_user_info['openid']; $products_name = '会员充值'; $total_fee = $recharge_config_info['recharge'] * 100; $unifiedorder = WxPayService::unifiedOrder($openid,$order_num,$total_fee,$products_name,C('COMPANY_RECHARGE_NOTIFY_URL')); // step4 等待支付成功后,处理微信回调 $data = $unifiedorder; // 其他信息 $this->json->setAttr('data', $data); $this->json->Send(); $this->json->S();}
支付基类
<?php/** * User: Eden * Date: 2019/3/21 * 共有内容 */namespace CommonService;use ThinkException;use VendorFuncHttp;class WxPayService extends CommonService{ protected static $SSL_CERT_PATH = './apiclient_cert.pem'; //证书路径 protected static $SSL_KEY_PATH = './apiclient_key.pem'; //证书路径 public static function unifiedOrder($openid, $order_num, $total_fee, $products_name, $notify_url = '') { $trade_no = $order_num; $url = 'https://api.mch.weixin.qq.com/pay/unifiedorder'; $data = [ 'appid' => C('APP_ID'), 'mch_id' => C('MCHID'), 'nonce_str' => self::createNonceStr(), 'sign_type' => 'MD5', 'body' => $products_name, //商品名称组合 'attach' => C('APP_NAME') . '-附加信息', 'out_trade_no' => $trade_no, //订单号 'fee_type' => 'CNY', 'total_fee' => $total_fee, // 单位分 'spbill_create_ip' => $_SERVER['REMOTE_ADDR'], 'goods_tag' => C('APP_NAME') . '-商品标记', 'notify_url' => $notify_url ?: C('NOTIFY_URL'), 'trade_type' => 'JSAPI', 'openid' => $openid ]; setlog($data,[],'','pay.log'); $sign = self::MakeSign($data); $data['sign'] = $sign; $xml = self::ToXml($data); $result = self::FromXml(Http::postXmlCurl($url, $xml)); /** * array ( * 'return_code' => 'FAIL', * 'return_msg' => '签名错误', * ) */ /** *array ( *'return_code' => 'SUCCESS', *'return_msg' => 'OK', *'appid' => 'wx4f00a0a86b52c297', *'mch_id' => '1574476801', *'nonce_str' => '7w1tka0oQmzzUtl9', *'sign' => 'E6470B7A55841CC77E905BE3BDFF2B92', *'result_code' => 'SUCCESS', *'prepay_id' => 'wx19111924446300f7f5d9f7fb1295195400', *'trade_type' => 'JSAPI', *) */ setlog($result,[],'','pay.log'); // 加工数据 $data = [ 'appId' => $result['appid'] ?: C('APP_ID'), 'timeStamp' => time(), 'nonceStr' => self::createNonceStr(), 'package' => 'prepay_id=' . $result['prepay_id'], 'signType' => 'MD5' ]; $sign = self::MakeSign($data); $data['sign'] = $sign; return $data; } /** * 处理退款 * @param $out_trade_no * @param $total_fee * @param $refund_fee * @param $from 1余额 2未结算 * @return array * @throws Exception * 策略一:当天支付的钱,从未结算中退;非当天支付的钱,从余额中退(结算的钱到余额中有个缓冲期1-3天,结算到余额要收千分之一的手续费)。确保退款正常,需要在余额中留有备用金。 * 策略二:优先从未结算中退,未结算中余额不足,再从余额中退。(需要查询两次,比较消耗网络。好处就是可以节省被腾讯收取的千分之一的费用。) */ public static function refundOrder($out_trade_no, $total_fee, $refund_fee, $from = 1) { $refund_no = $out_trade_no . $total_fee; if ((int) $from === 1) { $refund_account = 'REFUND_SOURCE_RECHARGE_FUNDS'; } else { $refund_account = 'REFUND_SOURCE_UNSETTLED_FUNDS'; } // $refund_account = 'REFUND_SOURCE_UNSETTLED_FUNDS'; $param = array( 'appid' => C('APP_ID'), 'mch_id' => C('MCHID'), 'nonce_str' => self::createNonceStr(), 'out_refund_no' => $refund_no, //由后端生成的退款单号,需要保证唯一,因为多个同样的退款单号只会退款一次。 'out_trade_no' => $out_trade_no, //退款订单在支付时生成的订单号 'total_fee' => $total_fee, 'refund_fee' => $refund_fee, 'refund_account' => $refund_account, // REFUND_SOURCE_RECHARGE_FUNDS 从余额退,REFUND_SOURCE_UNSETTLED_FUNDS 从未结算退 'op_user_id' => C('MCHID'), //操作员 op_user_id .与商户号相同即可 ); $param['sign'] = self::MakeSign($param); $xml_data = self::ToXml($param); $xml_result = self::postXmlSSLCurl($xml_data, 'https://api.mch.weixin.qq.com/secapi/pay/refund'); $result = self::FromXml($xml_result); if (!$result) { $result_arr = [ 'num' => '0', 'desc' => '接口错误', ]; return $result_arr; } if ($result['result_code'] != 'SUCCESS') { $result_arr = [ 'num' => '-1', 'desc' => $result['err_code_des'], 'err_code' => $result['err_code'], // NOTENOUGH 余额不足 ]; } else { $result_arr = [ 'num' => '1', 'desc' => '退款成功', 'refund_id' => $result['refund_id'], 'refund_no' => $refund_no, ]; } return $result_arr; } /** * xml2array * @param $xml * @return mixed * @throws Exception */ public static function FromXml($xml) { if (!$xml) { throw new Exception("xml数据异常!"); } //将XML转为array //禁止引用外部xml实体 libxml_disable_entity_loader(true); $values = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true); return $values; } /** * array2xml * @param $array * @return string|void */ public static function ToXml($array) { if (!is_array($array) || count($array) <= 0) { return; } $xml = '<xml version="1.0">'; foreach ($array as $key => $val) { if (is_numeric($val)) { $xml .= "<" . $key . ">" . $val . "</" . $key . ">"; } else { $xml .= "<" . $key . "><![CDATA[" . $val . "]]></" . $key . ">"; } } $xml .= "</xml>"; return $xml; } /** * 创建随机字符串 * @param int $length * @return string */ public static function createNonceStr($length = 16) { $chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; $str = ''; for ( $i = 0; $i < $length; $i++ ) { $str .= substr($chars, mt_rand(0, strlen($chars)-1), 1); } return $str; } /** * 签名 * @param $data * @return string */ public static function MakeSign($data) { //签名步骤一:按字典序排序参数 ksort($data); $string = self::ToUrlParams($data); //签名步骤二:在string后加入KEY $string = $string . "&key=".C('WEIXIN_PAY_KEY'); //签名步骤三:MD5加密 $string = md5($string); //签名步骤四:所有字符转为大写 $result = strtoupper($string); return $result; } /** * url * @param $array * @return string */ public static function ToUrlParams($array) { $buff = ''; foreach ($array as $k => $v) { if($k != 'sign' && $v != '' && !is_array($v)){ $buff .= $k . '=' . $v . '&'; } } $buff = trim($buff, '&'); return $buff; } /** * 需要使用证书的请求 * @param $xml * @param $url * @param int $second * @return bool|string */ public static function postXmlSSLCurl($xml,$url,$second=30) { $ch = curl_init(); //超时时间 curl_setopt($ch, CURLOPT_TIMEOUT, $second); //这里设置代理,如果有的话 //curl_setopt($ch,CURLOPT_PROXY, '8.8.8.8'); //curl_setopt($ch,CURLOPT_PROXYPORT, 8080); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); //设置header curl_setopt($ch, CURLOPT_HEADER, FALSE); //要求结果为字符串且输出到屏幕上 curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); //设置证书 //使用证书:cert 与 key 分别属于两个.pem文件 //默认格式为PEM,可以注释 curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM'); curl_setopt($ch, CURLOPT_SSLCERT, self::$SSL_CERT_PATH); //默认格式为PEM,可以注释 curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM'); curl_setopt($ch, CURLOPT_SSLKEY, self::$SSL_KEY_PATH); //post提交方式 curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); $data = curl_exec($ch); //返回结果 if ($data) { curl_close($ch); return $data; } else { $error = curl_errno($ch); echo "curl出错,错误码:$error" . "<br>"; curl_close($ch); return false; } }}
tips:务必开通商户平台,并配置好商户号和支付秘钥