/**
* 发起充值,获取充值参数
*/
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;
}
}
}
支付回调
//微信支付回调
public function order_notice()
{
$xml = $GLOBALS['HTTP_RAW_POST_DATA'];
$data = WxPayService::FromXml($xml);
$data_sign = $data['sign'];
unset($data['sign']);
$sign = WxPayService::MakeSign($data);
if (($sign === $data_sign) && ($data['return_code'] == 'SUCCESS') && ($data['result_code'] == 'SUCCESS')) {
$order_num = $data['out_trade_no']; //订单单号
$openid = $data['openid']; //付款人openID
$total_fee = $data['total_fee']; //付款金额
$transaction_id = $data['transaction_id']; //微信支付流水号
$result = $this->order_notice_datadeal($order_num, $openid, $total_fee, $transaction_id);
} else {
$result = false;
}
if ($result) {
$str = '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
} else {
$str = '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名失败]]></return_msg></xml>';
}
echo $str;
return $result;
}
// 支付成功后回调数据处理;
// @param $order_num 订单单号
// @param $openid 付款人openID
// @param $total_fee ,实际支付的付款金额,单位分
// @param string $transaction_id ,微信支付流水号
private function order_notice_datadeal($order_num, $openid, $total_fee, $transaction_id = '')
{
$recharge_order = M('recharge_order');
$order_info = $recharge_order->where(['order_num' => $order_num])->find();
if ((int) $order_info['status'] === 2) {
return true;
}
// 订单状态处理
$save_data = [
'total_payed_price' => MathUtil::div($total_fee, 100),
'transaction_id' => $transaction_id,
'pay_time' => time(),
'status' => 2 //1.未支付;2.已支付;
];
M()->startTrans();
$order_save_flag = $recharge_order->where(array('order_num' => $order_num))->save($save_data);
if ($order_save_flag === false) {
M()->rollback();
return false;
}
$company_user = M('company_user');
$user_info = $company_user->where(array('id' => $order_info['company_uid']))->find();
if ((int) $user_info['vip_end_time'] < time()) { // 已过期,新开会员
$save_data = [
'is_vip' => 1,
'vip_end_time' => time() + (int)$order_info['year'] * 31536000,
];
} else { // 续费
$save_data = [
'is_vip' => 1,
'vip_end_time' => (int) $user_info['vip_end_time'] + (int)$order_info['year'] * 31536000,
];
}
// 处理vip
$user_save_flag = $company_user->where(array('id' => $order_info['company_uid']))->save($save_data);
if ($user_save_flag === false) {
M()->rollback();
return false;
}
M()->commit();
return true;
}
tips:务必开通商户平台,并配置好商户号和支付秘钥