Wechatpay.class.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. <?php
  2. /**
  3. * 微信公众平台PHP-SDK, 旧版微信支付接口
  4. * @author dodge <dodgepudding@gmail.com>
  5. * @link https://github.com/dodgepudding/wechat-php-sdk
  6. * @version 1.2
  7. * 参考旧版文档 https://mp.weixin.qq.com/cgi-bin/readtemplate?t=business/course2_tmpl&lang=zh_CN
  8. * usage:
  9. * $options = array(
  10. * 'appid'=>'wxdk1234567890', //填写高级调用功能的app id
  11. * 'appsecret'=>'xxxxxxxxxxxxxxxxxxx', //填写高级调用功能的密钥
  12. * 'partnerid'=>'88888888', //财付通商户身份标识
  13. * 'partnerkey'=>'', //财付通商户权限密钥Key
  14. * 'paysignkey'=>'' //商户签名密钥Key
  15. * );
  16. * $payObj = new Wechatpay($options);
  17. * $package = $payObj->createPackage($out_trade_no,$body,$total_fee,$notify_url,$spbill_create_ip,$fee_type,$bank_type,$input_charset,$time_start,$time_expire,$transport_fee,$product_fee,$goods_tag,$attach);
  18. *
  19. */
  20. class Wechatpay
  21. {
  22. const API_URL_PREFIX = 'https://api.weixin.qq.com/cgi-bin';
  23. const AUTH_URL = '/token?grant_type=client_credential&';
  24. const API_BASE_URL_PREFIX = 'https://api.weixin.qq.com'; //以下API接口URL需要使用此前缀
  25. const PAY_DELIVERNOTIFY = '/pay/delivernotify?';
  26. const PAY_ORDERQUERY = '/pay/orderquery?';
  27. private $appid;
  28. private $appsecret;
  29. private $access_token;
  30. private $user_token;
  31. private $partnerid;
  32. private $partnerkey;
  33. private $paysignkey;
  34. public $debug = false;
  35. public $errCode = 40001;
  36. public $errMsg = "no access";
  37. private $_logcallback;
  38. public function __construct($options)
  39. {
  40. $this->appid = isset($options['appid'])?$options['appid']:'';
  41. $this->appsecret = isset($options['appsecret'])?$options['appsecret']:'';
  42. $this->partnerid = isset($options['partnerid'])?$options['partnerid']:'';
  43. $this->partnerkey = isset($options['partnerkey'])?$options['partnerkey']:'';
  44. $this->paysignkey = isset($options['paysignkey'])?$options['paysignkey']:'';
  45. $this->debug = isset($options['debug'])?$options['debug']:false;
  46. $this->_logcallback = isset($options['logcallback'])?$options['logcallback']:false;
  47. }
  48. private function log($log){
  49. if ($this->debug ) {
  50. if (function_exists($this->_logcallback)) {
  51. if (is_array($log)) $log = print_r($log,true);
  52. return call_user_func($this->_logcallback,$log);
  53. }elseif (class_exists('Log')) {
  54. Log::write('wechat:'.$log, Log::DEBUG);
  55. }
  56. }
  57. return false;
  58. }
  59. /**
  60. * GET 请求
  61. * @param string $url
  62. */
  63. private function http_get($url){
  64. $oCurl = curl_init();
  65. if(stripos($url,"https://")!==FALSE){
  66. curl_setopt($oCurl, CURLOPT_SSL_VERIFYPEER, FALSE);
  67. curl_setopt($oCurl, CURLOPT_SSL_VERIFYHOST, FALSE);
  68. curl_setopt($oCurl, CURLOPT_SSLVERSION, 1); //CURL_SSLVERSION_TLSv1
  69. }
  70. curl_setopt($oCurl, CURLOPT_URL, $url);
  71. curl_setopt($oCurl, CURLOPT_RETURNTRANSFER, 1 );
  72. $sContent = curl_exec($oCurl);
  73. $aStatus = curl_getinfo($oCurl);
  74. curl_close($oCurl);
  75. if(intval($aStatus["http_code"])==200){
  76. return $sContent;
  77. }else{
  78. return false;
  79. }
  80. }
  81. /**
  82. * POST 请求
  83. * @param string $url
  84. * @param array $param
  85. * @param boolean $post_file 是否文件上传
  86. * @return string content
  87. */
  88. private function http_post($url,$param,$post_file=false){
  89. $oCurl = curl_init();
  90. if(stripos($url,"https://")!==FALSE){
  91. curl_setopt($oCurl, CURLOPT_SSL_VERIFYPEER, FALSE);
  92. curl_setopt($oCurl, CURLOPT_SSL_VERIFYHOST, false);
  93. curl_setopt($oCurl, CURLOPT_SSLVERSION, 1); //CURL_SSLVERSION_TLSv1
  94. }
  95. if (is_string($param) || $post_file) {
  96. $strPOST = $param;
  97. } else {
  98. $aPOST = array();
  99. foreach($param as $key=>$val){
  100. $aPOST[] = $key."=".urlencode($val);
  101. }
  102. $strPOST = join("&", $aPOST);
  103. }
  104. curl_setopt($oCurl, CURLOPT_URL, $url);
  105. curl_setopt($oCurl, CURLOPT_RETURNTRANSFER, 1 );
  106. curl_setopt($oCurl, CURLOPT_POST,true);
  107. curl_setopt($oCurl, CURLOPT_POSTFIELDS,$strPOST);
  108. $sContent = curl_exec($oCurl);
  109. $aStatus = curl_getinfo($oCurl);
  110. curl_close($oCurl);
  111. if(intval($aStatus["http_code"])==200){
  112. return $sContent;
  113. }else{
  114. return false;
  115. }
  116. }
  117. /**
  118. * 通用auth验证方法,暂时仅用于菜单更新操作
  119. * @param string $appid
  120. * @param string $appsecret
  121. * @param string $token 手动指定access_token,非必要情况不建议用
  122. */
  123. public function checkAuth($appid='',$appsecret='',$token=''){
  124. if (!$appid || !$appsecret) {
  125. $appid = $this->appid;
  126. $appsecret = $this->appsecret;
  127. }
  128. $authname = 'wechat_access_token'.$appid;
  129. if ($token) { //手动指定token,优先使用
  130. $this->access_token=$token;
  131. return $this->access_token;
  132. }
  133. if ($rs = S($authname)) {
  134. $this->access_token = $rs;
  135. return $rs;
  136. }
  137. $result = $this->http_get(self::API_URL_PREFIX.self::AUTH_URL.'appid='.$appid.'&secret='.$appsecret);
  138. if ($result)
  139. {
  140. $json = json_decode($result,true);
  141. if (!$json || isset($json['errcode'])) {
  142. $this->errCode = $json['errcode'];
  143. $this->errMsg = $json['errmsg'];
  144. return false;
  145. }
  146. $this->access_token = $json['access_token'];
  147. $expire = $json['expires_in'] ? intval($json['expires_in'])-100 : 3600;
  148. S($authname,$this->access_token,$expire);
  149. return $this->access_token;
  150. }
  151. return false;
  152. }
  153. /**
  154. * 删除验证数据
  155. * @param string $appid
  156. */
  157. public function resetAuth($appid=''){
  158. if (!$appid) $appid = $this->appid;
  159. $this->access_token = '';
  160. $authname = 'wechat_access_token'.$appid;
  161. S($authname,null);
  162. return true;
  163. }
  164. /**
  165. * 微信api不支持中文转义的json结构
  166. * @param array $arr
  167. */
  168. static function json_encode($arr) {
  169. $parts = array ();
  170. $is_list = false;
  171. //Find out if the given array is a numerical array
  172. $keys = array_keys ( $arr );
  173. $max_length = count ( $arr ) - 1;
  174. if (($keys [0] === 0) && ($keys [$max_length] === $max_length )) { //See if the first key is 0 and last key is length - 1
  175. $is_list = true;
  176. for($i = 0; $i < count ( $keys ); $i ++) { //See if each key correspondes to its position
  177. if ($i != $keys [$i]) { //A key fails at position check.
  178. $is_list = false; //It is an associative array.
  179. break;
  180. }
  181. }
  182. }
  183. foreach ( $arr as $key => $value ) {
  184. if (is_array ( $value )) { //Custom handling for arrays
  185. if ($is_list)
  186. $parts [] = self::json_encode ( $value ); /* :RECURSION: */
  187. else
  188. $parts [] = '"' . $key . '":' . self::json_encode ( $value ); /* :RECURSION: */
  189. } else {
  190. $str = '';
  191. if (! $is_list)
  192. $str = '"' . $key . '":';
  193. //Custom handling for multiple data types
  194. if (!is_string ( $value ) && is_numeric ( $value ) && $value<2000000000)
  195. $str .= $value; //Numbers
  196. elseif ($value === false)
  197. $str .= 'false'; //The booleans
  198. elseif ($value === true)
  199. $str .= 'true';
  200. else
  201. $str .= '"' . addslashes ( $value ) . '"'; //All other things
  202. // :TODO: Is there any more datatype we should be in the lookout for? (Object?)
  203. $parts [] = $str;
  204. }
  205. }
  206. $json = implode ( ',', $parts );
  207. if ($is_list)
  208. return '[' . $json . ']'; //Return numerical JSON
  209. return '{' . $json . '}'; //Return associative JSON
  210. }
  211. /**
  212. * 获取签名
  213. * @param array $arrdata 签名数组
  214. * @param string $method 签名方法
  215. * @return boolean|string 签名值
  216. */
  217. public function getSignature($arrdata,$method="sha1") {
  218. if (!function_exists($method)) return false;
  219. ksort($arrdata);
  220. $paramstring = "";
  221. foreach($arrdata as $key => $value)
  222. {
  223. if(strlen($paramstring) == 0)
  224. $paramstring .= $key . "=" . $value;
  225. else
  226. $paramstring .= "&" . $key . "=" . $value;
  227. }
  228. $paySign = $method($paramstring);
  229. return $paySign;
  230. }
  231. /**
  232. * 生成随机字串
  233. * @param number $length 长度,默认为16,最长为32字节
  234. * @return string
  235. */
  236. public function generateNonceStr($length=16){
  237. // 密码字符集,可任意添加你需要的字符
  238. $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
  239. $str = "";
  240. for($i = 0; $i < $length; $i++)
  241. {
  242. $str .= $chars[mt_rand(0, strlen($chars) - 1)];
  243. }
  244. return $str;
  245. }
  246. /**
  247. * 生成原生支付url
  248. * @param number $productid 商品编号,最长为32字节
  249. * @return string
  250. */
  251. public function createNativeUrl($productid){
  252. $nativeObj["appid"] = $this->appid;
  253. $nativeObj["appkey"] = $this->paysignkey;
  254. $nativeObj["productid"] = urlencode($productid);
  255. $nativeObj["timestamp"] = time();
  256. $nativeObj["noncestr"] = $this->generateNonceStr();
  257. $nativeObj["sign"] = $this->getSignature($nativeObj);
  258. unset($nativeObj["appkey"]);
  259. $bizString = "";
  260. foreach($nativeObj as $key => $value)
  261. {
  262. if(strlen($bizString) == 0)
  263. $bizString .= $key . "=" . $value;
  264. else
  265. $bizString .= "&" . $key . "=" . $value;
  266. }
  267. return "weixin://wxpay/bizpayurl?".$bizString;
  268. //weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXXX&productid=XXXXXX&timestamp=XXXXXX&noncestr=XXXXXX
  269. }
  270. /**
  271. * 生成订单package字符串
  272. * @param string $out_trade_no 必填,商户系统内部的订单号,32个字符内,确保在商户系统唯一
  273. * @param string $body 必填,商品描述,128 字节以下
  274. * @param int $total_fee 必填,订单总金额,单位为分
  275. * @param string $notify_url 必填,支付完成通知回调接口,255 字节以内
  276. * @param string $spbill_create_ip 必填,用户终端IP,IPV4字串,15字节内
  277. * @param int $fee_type 必填,现金支付币种,默认1:人民币
  278. * @param string $bank_type 必填,银行通道类型,默认WX
  279. * @param string $input_charset 必填,传入参数字符编码,默认UTF-8,取值有UTF-8和GBK
  280. * @param string $time_start 交易起始时间,订单生成时间,格式yyyyMMddHHmmss
  281. * @param string $time_expire 交易结束时间,也是订单失效时间
  282. * @param int $transport_fee 物流费用,单位为分
  283. * @param int $product_fee 商品费用,单位为分,必须保证 transport_fee + product_fee=total_fee
  284. * @param string $goods_tag 商品标记,优惠券时可能用到
  285. * @param string $attach 附加数据,notify接口原样返回
  286. * @return string
  287. */
  288. public function createPackage($out_trade_no,$body,$total_fee,$notify_url,$spbill_create_ip,$fee_type=1,$bank_type="WX",$input_charset="UTF-8",$time_start="",$time_expire="",$transport_fee="",$product_fee="",$goods_tag="",$attach=""){
  289. $arrdata = array("bank_type" => $bank_type, "body" => $body, "partner" => $this->partnerid, "out_trade_no" => $out_trade_no, "total_fee" => $total_fee, "fee_type" => $fee_type, "notify_url" => $notify_url, "spbill_create_ip" => $spbill_create_ip, "input_charset" => $input_charset);
  290. if ($time_start) $arrdata['time_start'] = $time_start;
  291. if ($time_expire) $arrdata['time_expire'] = $time_expire;
  292. if ($transport_fee) $arrdata['transport_fee'] = $transport_fee;
  293. if ($product_fee) $arrdata['product_fee'] = $product_fee;
  294. if ($goods_tag) $arrdata['goods_tag'] = $goods_tag;
  295. if ($attach) $arrdata['attach'] = $attach;
  296. ksort($arrdata);
  297. $paramstring = "";
  298. foreach($arrdata as $key => $value)
  299. {
  300. if(strlen($paramstring) == 0)
  301. $paramstring .= $key . "=" . $value;
  302. else
  303. $paramstring .= "&" . $key . "=" . $value;
  304. }
  305. $stringSignTemp = $paramstring . "&key=" . $this->partnerkey;
  306. $signValue = strtoupper(md5($stringSignTemp));
  307. $package = http_build_query($arrdata) . "&sign=" . $signValue;
  308. return $package;
  309. }
  310. /**
  311. * 支付签名(paySign)生成方法
  312. * @param string $package 订单详情字串
  313. * @param string $timeStamp 当前时间戳(需与JS输出的一致)
  314. * @param string $nonceStr 随机串(需与JS输出的一致)
  315. * @return string 返回签名字串
  316. */
  317. public function getPaySign($package, $timeStamp, $nonceStr){
  318. $arrdata = array("appid" => $this->appid, "timestamp" => $timeStamp, "noncestr" => $nonceStr, "package" => $package, "appkey" => $this->paysignkey);
  319. $paySign = $this->getSignature($arrdata);
  320. return $paySign;
  321. }
  322. /**
  323. * 回调通知签名验证
  324. * @param array $orderxml 返回的orderXml的数组表示,留空则自动从post数据获取
  325. * @return boolean
  326. */
  327. public function checkOrderSignature($orderxml=''){
  328. if (!$orderxml) {
  329. $postStr = file_get_contents("php://input");
  330. if (!empty($postStr)) {
  331. $orderxml = (array)simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
  332. } else return false;
  333. }
  334. $arrdata = array('appid'=>$orderxml['AppId'],'appkey'=>$this->paysignkey,'timestamp'=>$orderxml['TimeStamp'],'noncestr'=>$orderxml['NonceStr'],'openid'=>$orderxml['OpenId'],'issubscribe'=>$orderxml['IsSubscribe']);
  335. $paySign = $this->getSignature($arrdata);
  336. if ($paySign!=$orderxml['AppSignature']) return false;
  337. return true;
  338. }
  339. /**
  340. * 发货通知
  341. * @param string $openid 用户open_id
  342. * @param string $transid 交易单号
  343. * @param string $out_trade_no 第三方订单号
  344. * @param int $status 0:发货失败;1:已发货
  345. * @param string $msg 失败原因
  346. * @return boolean|array
  347. */
  348. public function sendPayDeliverNotify($openid,$transid,$out_trade_no,$status=1,$msg='ok'){
  349. if (!$this->access_token && !$this->checkAuth()) return false;
  350. $postdata = array(
  351. "appid"=>$this->appid,
  352. "appkey"=>$this->paysignkey,
  353. "openid"=>$openid,
  354. "transid"=>strval($transid),
  355. "out_trade_no"=>strval($out_trade_no),
  356. "deliver_timestamp"=>strval(time()),
  357. "deliver_status"=>strval($status),
  358. "deliver_msg"=>$msg,
  359. );
  360. $postdata['app_signature'] = $this->getSignature($postdata);
  361. $postdata['sign_method'] = 'sha1';
  362. unset($postdata['appkey']);
  363. $result = $this->http_post(self::API_BASE_URL_PREFIX.self::PAY_DELIVERNOTIFY.'access_token='.$this->access_token,self::json_encode($postdata));
  364. if ($result)
  365. {
  366. $json = json_decode($result,true);
  367. if (!$json || !empty($json['errcode'])) {
  368. $this->errCode = $json['errcode'];
  369. $this->errMsg = $json['errmsg'];
  370. return false;
  371. }
  372. return $json;
  373. }
  374. return false;
  375. }
  376. /**
  377. * 查询订单信息
  378. * @param string $out_trade_no 订单号
  379. * @return boolean|array
  380. */
  381. public function getPayOrder($out_trade_no) {
  382. if (!$this->access_token && !$this->checkAuth()) return false;
  383. $sign = strtoupper(md5("out_trade_no=$out_trade_no&partner={$this->partnerid}&key={$this->partnerkey}"));
  384. $postdata = array(
  385. "appid"=>$this->appid,
  386. "appkey"=>$this->paysignkey,
  387. "package"=>"out_trade_no=$out_trade_no&partner={$this->partnerid}&sign=$sign",
  388. "timestamp"=>strval(time()),
  389. );
  390. $postdata['app_signature'] = $this->getSignature($postdata);
  391. $postdata['sign_method'] = 'sha1';
  392. unset($postdata['appkey']);
  393. $result = $this->http_post(self::API_BASE_URL_PREFIX.self::PAY_ORDERQUERY.'access_token='.$this->access_token,self::json_encode($postdata));
  394. if ($result)
  395. {
  396. $json = json_decode($result,true);
  397. if (!$json || !empty($json['errcode'])) {
  398. $this->errCode = $json['errcode'];
  399. $this->errMsg = $json['errmsg'].json_encode($postdata);
  400. return false;
  401. }
  402. return $json["order_info"];
  403. }
  404. return false;
  405. }
  406. /**
  407. * 设置用户授权密钥
  408. * @param string $user_token
  409. * @return string
  410. */
  411. public function setUserToken($user_token) {
  412. return $this->user_token = $user_token;
  413. }
  414. /**
  415. * 设置用户授权密钥
  416. * @param string $user_token
  417. * @return string
  418. */
  419. public function setUserToken($user_token) {
  420. return $this->user_token = $user_token;
  421. }
  422. /**
  423. * 获取收货地址JS的签名
  424. * @tutorial 参考weixin.js脚本的WeixinJS.editAddress方法调用
  425. * @param string $appId
  426. * @param string $url
  427. * @param int $timeStamp
  428. * @param string $nonceStr
  429. * @param string $user_token
  430. * @return Ambigous <boolean, string>
  431. */
  432. public function getAddrSign($url, $timeStamp, $nonceStr, $user_token=''){
  433. if (!$user_token) $user_token = $this->user_token;
  434. if (!$user_token) {
  435. $this->errMsg = 'no user access token found!';
  436. return false;
  437. }
  438. $url = htmlspecialchars_decode($url);
  439. $arrdata = array(
  440. 'appid'=>$this->appid,
  441. 'url'=>$url,
  442. 'timestamp'=>strval($timeStamp),
  443. 'noncestr'=>$nonceStr,
  444. 'accesstoken'=>$user_token
  445. );
  446. return $this->getSignature($arrdata);
  447. }
  448. }