WechatPay.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. <?php
  2. namespace Wechat;
  3. use Wechat\Lib\Tools;
  4. /**
  5. * 微信支付SDK
  6. * @author zoujingli <zoujingli@qq.com>
  7. * @date 2015/05/13 12:12:00
  8. */
  9. class WechatPay {
  10. /** 支付接口基础地址 */
  11. const MCH_BASE_URL = 'https://api.mch.weixin.qq.com';
  12. /** 公众号appid */
  13. public $appid;
  14. /** 商户身份ID */
  15. public $mch_id;
  16. /** 商户支付密钥Key */
  17. public $partnerKey;
  18. /** 证书路径 */
  19. public $ssl_cer;
  20. public $ssl_key;
  21. /** 执行错误消息及代码 */
  22. public $errMsg;
  23. public $errCode;
  24. /**
  25. * WechatPay constructor.
  26. * @param array $options
  27. */
  28. public function __construct($options = array()) {
  29. $config = Loader::config($options);
  30. $this->appid = isset($config['appid']) ? $config['appid'] : '';
  31. $this->mch_id = isset($config['mch_id']) ? $config['mch_id'] : '';
  32. $this->partnerKey = isset($config['partnerkey']) ? $config['partnerkey'] : '';
  33. $this->ssl_cer = isset($config['ssl_cer']) ? $config['ssl_cer'] : '';
  34. $this->ssl_key = isset($config['ssl_key']) ? $config['ssl_key'] : '';
  35. }
  36. /**
  37. * 设置标配的请求参数,生成签名,生成接口参数xml
  38. * @param array $data
  39. * @return string
  40. */
  41. protected function createXml($data) {
  42. if (!isset($data['wxappid']) && !isset($data['mch_appid']) && !isset($data['appid'])) {
  43. $data['appid'] = $this->appid;
  44. }
  45. if (!isset($data['mchid']) && !isset($data['mch_id'])) {
  46. $data['mch_id'] = $this->mch_id;
  47. }
  48. isset($data['nonce_str']) || $data['nonce_str'] = Tools::createNoncestr();
  49. $data["sign"] = Tools::getPaySign($data, $this->partnerKey);
  50. return Tools::arr2xml($data);
  51. }
  52. /**
  53. * POST提交XML
  54. * @param array $data
  55. * @param string $url
  56. * @return mixed
  57. */
  58. public function postXml($data, $url) {
  59. return Tools::httpPost($url, $this->createXml($data));
  60. }
  61. /**
  62. * 使用证书post请求XML
  63. * @param array $data
  64. * @param string $url
  65. * @return mixed
  66. */
  67. function postXmlSSL($data, $url) {
  68. return Tools::httpsPost($url, $this->createXml($data), $this->ssl_cer, $this->ssl_key);
  69. }
  70. /**
  71. * POST提交获取Array结果
  72. * @param array $data 需要提交的数据
  73. * @param string $url
  74. * @param string $method
  75. * @return array
  76. */
  77. public function getArrayResult($data, $url, $method = 'postXml') {
  78. return Tools::xml2arr($this->$method($data, $url));
  79. }
  80. /**
  81. * 解析返回的结果
  82. * @param array $result
  83. * @return bool|array
  84. */
  85. protected function _parseResult($result) {
  86. if (empty($result)) {
  87. $this->errCode = 'result error';
  88. $this->errMsg = '解析返回结果失败';
  89. return false;
  90. }
  91. if ($result['return_code'] !== 'SUCCESS') {
  92. $this->errCode = $result['return_code'];
  93. $this->errMsg = $result['return_msg'];
  94. return false;
  95. }
  96. if (isset($result['err_code']) && $result['err_code'] !== 'SUCCESS') {
  97. $this->errMsg = $result['err_code_des'];
  98. $this->errCode = $result['err_code'];
  99. return false;
  100. }
  101. return $result;
  102. }
  103. /**
  104. * 支付通知验证处理
  105. * @return bool|array
  106. */
  107. public function getNotify() {
  108. $notifyInfo = (array)simplexml_load_string(file_get_contents("php://input"), 'SimpleXMLElement', LIBXML_NOCDATA);
  109. if (empty($notifyInfo)) {
  110. Tools::log('Payment notification forbidden access.', 'ERR');
  111. $this->errCode = '404';
  112. $this->errMsg = 'Payment notification forbidden access.';
  113. return false;
  114. }
  115. if (empty($notifyInfo['sign'])) {
  116. Tools::log('Payment notification signature is missing.' . var_export($notifyInfo, true), 'ERR');
  117. $this->errCode = '403';
  118. $this->errMsg = 'Payment notification signature is missing.';
  119. return false;
  120. }
  121. $data = $notifyInfo;
  122. unset($data['sign']);
  123. if ($notifyInfo['sign'] !== Tools::getPaySign($data, $this->partnerKey)) {
  124. Tools::log('Payment notification signature verification failed.' . var_export($notifyInfo, true), 'ERR');
  125. $this->errCode = '403';
  126. $this->errMsg = 'Payment signature verification failed.';
  127. return false;
  128. }
  129. Tools::log('Payment notification signature verification success.' . var_export($notifyInfo, true), 'MSG');
  130. $this->errCode = '0';
  131. $this->errMsg = '';
  132. return $notifyInfo;
  133. }
  134. /**
  135. * 支付XML统一回复
  136. * @param array $data 需要回复的XML内容数组
  137. * @param bool $isReturn 是否返回XML内容,默认不返回
  138. * @return string
  139. */
  140. public function replyXml(array $data, $isReturn = false) {
  141. $xml = Tools::arr2xml($data);
  142. if ($isReturn) {
  143. return $xml;
  144. }
  145. ob_clean();
  146. exit($xml);
  147. }
  148. /**
  149. * 获取预支付ID
  150. * @param string $openid 用户openid,JSAPI必填
  151. * @param string $body 商品标题
  152. * @param string $out_trade_no 第三方订单号
  153. * @param int $total_fee 订单总价
  154. * @param string $notify_url 支付成功回调地址
  155. * @param string $trade_type 支付类型JSAPI|NATIVE|APP
  156. * @param string $goods_tag 商品标记,代金券或立减优惠功能的参数
  157. * @return bool|string
  158. */
  159. public function getPrepayId($openid, $body, $out_trade_no, $total_fee, $notify_url, $trade_type = "JSAPI", $goods_tag = null) {
  160. $postdata = array(
  161. "body" => $body,
  162. "out_trade_no" => $out_trade_no,
  163. "total_fee" => $total_fee,
  164. "notify_url" => $notify_url,
  165. "trade_type" => $trade_type,
  166. "spbill_create_ip" => Tools::getAddress()
  167. );
  168. empty($goods_tag) || $postdata['goods_tag'] = $goods_tag;
  169. empty($openid) || $postdata['openid'] = $openid;
  170. $result = $this->getArrayResult($postdata, self::MCH_BASE_URL . '/pay/unifiedorder');
  171. if (false === $this->_parseResult($result)) {
  172. return false;
  173. }
  174. return in_array($trade_type, array('JSAPI', 'APP')) ? $result['prepay_id'] : $result['code_url'];
  175. }
  176. /**
  177. * 获取二维码预支付ID
  178. * @param string $openid 用户openid,JSAPI必填
  179. * @param string $body 商品标题
  180. * @param string $out_trade_no 第三方订单号
  181. * @param int $total_fee 订单总价
  182. * @param string $notify_url 支付成功回调地址
  183. * @param string $goods_tag 商品标记,代金券或立减优惠功能的参数
  184. * @return bool|string
  185. */
  186. public function getQrcPrepayId($openid, $body, $out_trade_no, $total_fee, $notify_url, $goods_tag = null) {
  187. $postdata = array(
  188. "body" => $body,
  189. "out_trade_no" => $out_trade_no,
  190. "total_fee" => $total_fee,
  191. "notify_url" => $notify_url,
  192. "trade_type" => 'NATIVE',
  193. "spbill_create_ip" => Tools::getAddress()
  194. );
  195. empty($goods_tag) || $postdata['goods_tag'] = $goods_tag;
  196. empty($openid) || $postdata['openid'] = $openid;
  197. $result = $this->getArrayResult($postdata, self::MCH_BASE_URL . '/pay/unifiedorder');
  198. if (false === $this->_parseResult($result) || empty($result['prepay_id'])) {
  199. return false;
  200. }
  201. return $result['prepay_id'];
  202. }
  203. /**
  204. * 获取支付规二维码
  205. * @param string $product_id 商户定义的商品id 或者订单号
  206. * @return string
  207. */
  208. public function getQrcPayUrl($product_id) {
  209. $data = array(
  210. 'appid' => $this->appid,
  211. 'mch_id' => $this->mch_id,
  212. 'time_stamp' => (string)time(),
  213. 'nonce_str' => Tools::createNoncestr(),
  214. 'product_id' => (string)$product_id,
  215. );
  216. $data['sign'] = Tools::getPaySign($data, $this->partnerKey);
  217. return "weixin://wxpay/bizpayurl?" . http_build_query($data);
  218. }
  219. /**
  220. * 创建JSAPI支付参数包
  221. * @param string $prepay_id
  222. * @return array
  223. */
  224. public function createMchPay($prepay_id) {
  225. $option = array();
  226. $option["appId"] = $this->appid;
  227. $option["timeStamp"] = (string)time();
  228. $option["nonceStr"] = Tools::createNoncestr();
  229. $option["package"] = "prepay_id={$prepay_id}";
  230. $option["signType"] = "MD5";
  231. $option["paySign"] = Tools::getPaySign($option, $this->partnerKey);
  232. $option['timestamp'] = $option['timeStamp'];
  233. return $option;
  234. }
  235. /**
  236. * 关闭订单
  237. * @param string $out_trade_no
  238. * @return bool
  239. */
  240. public function closeOrder($out_trade_no) {
  241. $data = array('out_trade_no' => $out_trade_no);
  242. $result = $this->getArrayResult($data, self::MCH_BASE_URL . '/pay/closeorder');
  243. if (false === $this->_parseResult($result)) {
  244. return false;
  245. }
  246. return ($result['return_code'] === 'SUCCESS');
  247. }
  248. /**
  249. * 查询订单详情
  250. * @param $out_trade_no
  251. * @return bool|array
  252. */
  253. public function queryOrder($out_trade_no) {
  254. $data = array('out_trade_no' => $out_trade_no);
  255. $result = $this->getArrayResult($data, self::MCH_BASE_URL . '/pay/orderquery');
  256. if (false === $this->_parseResult($result)) {
  257. return false;
  258. }
  259. return $result;
  260. }
  261. /**
  262. * 订单退款接口
  263. * @param string $out_trade_no 商户订单号
  264. * @param string $transaction_id 微信订单号
  265. * @param string $out_refund_no 商户退款订单号
  266. * @param int $total_fee 商户订单总金额
  267. * @param int $refund_fee 退款金额
  268. * @param int|null $op_user_id 操作员ID,默认商户ID
  269. * @param string $refund_account 退款资金来源
  270. * 仅针对老资金流商户使用
  271. * REFUND_SOURCE_UNSETTLED_FUNDS --- 未结算资金退款(默认使用未结算资金退款)
  272. * REFUND_SOURCE_RECHARGE_FUNDS --- 可用余额退款
  273. * @return bool
  274. */
  275. public function refund($out_trade_no, $transaction_id, $out_refund_no, $total_fee, $refund_fee, $op_user_id = null, $refund_account = '') {
  276. $data = array();
  277. $data['out_trade_no'] = $out_trade_no;
  278. $data['transaction_id'] = $transaction_id;
  279. $data['out_refund_no'] = $out_refund_no;
  280. $data['total_fee'] = $total_fee;
  281. $data['refund_fee'] = $refund_fee;
  282. $data['op_user_id'] = empty($op_user_id) ? $this->mch_id : $op_user_id;
  283. !empty($refund_account) && $data['refund_account'] = $refund_account;
  284. $result = $this->getArrayResult($data, self::MCH_BASE_URL . '/secapi/pay/refund', 'postXmlSSL');
  285. if (false === $this->_parseResult($result)) {
  286. return false;
  287. }
  288. return ($result['return_code'] === 'SUCCESS');
  289. }
  290. /**
  291. * 退款查询接口
  292. * @param string $out_trade_no
  293. * @return bool|array
  294. */
  295. public function refundQuery($out_trade_no) {
  296. $data = array();
  297. $data['out_trade_no'] = $out_trade_no;
  298. $result = $this->getArrayResult($data, self::MCH_BASE_URL . '/pay/refundquery');
  299. if (false === $this->_parseResult($result)) {
  300. return false;
  301. }
  302. return $result;
  303. }
  304. /**
  305. * 获取对账单
  306. * @param string $bill_date 账单日期,如 20141110
  307. * @param string $bill_type ALL|SUCCESS|REFUND|REVOKED
  308. * @return bool|array
  309. */
  310. public function getBill($bill_date, $bill_type = 'ALL') {
  311. $data = array();
  312. $data['bill_date'] = $bill_date;
  313. $data['bill_type'] = $bill_type;
  314. $result = $this->postXml($data, self::MCH_BASE_URL . '/pay/downloadbill');
  315. $json = Tools::xml2arr($result);
  316. if (!empty($json) && false === $this->_parseResult($json)) {
  317. return false;
  318. }
  319. return $json;
  320. }
  321. /**
  322. * 发送现金红包
  323. * @param string $openid 红包接收者OPENID
  324. * @param int $total_amount 红包总金额
  325. * @param string $mch_billno 商户订单号
  326. * @param string $sendname 商户名称
  327. * @param string $wishing 红包祝福语
  328. * @param string $act_name 活动名称
  329. * @param string $remark 备注信息
  330. * @param null|int $total_num 红包发放总人数(大于1为裂变红包)
  331. * @param null|string $scene_id 场景id
  332. * @param string $risk_info 活动信息
  333. * @param null|string $consume_mch_id 资金授权商户号
  334. * @return array|bool
  335. * @link https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_5
  336. */
  337. public function sendRedPack($openid, $total_amount, $mch_billno, $sendname, $wishing, $act_name, $remark, $total_num = 1, $scene_id = null, $risk_info = '', $consume_mch_id = null) {
  338. $data = array();
  339. $data['mch_billno'] = $mch_billno; // 商户订单号 mch_id+yyyymmdd+10位一天内不能重复的数字
  340. $data['wxappid'] = $this->appid;
  341. $data['send_name'] = $sendname; //商户名称
  342. $data['re_openid'] = $openid; //红包接收者
  343. $data['total_amount'] = $total_amount; //红包总金额
  344. $data['total_num'] = '1'; //发放人数据
  345. $data['wishing'] = $wishing; //红包祝福语
  346. $data['client_ip'] = Tools::getAddress(); //调用接口的机器Ip地址
  347. $data['act_name'] = $act_name; //活动名称
  348. $data['remark'] = $remark; //备注信息
  349. $data['total_num'] = $total_num;
  350. !empty($scene_id) && $data['scene_id'] = $scene_id;
  351. !empty($risk_info) && $data['risk_info'] = $risk_info;
  352. !empty($consume_mch_id) && $data['consume_mch_id'] = $consume_mch_id;
  353. if ($total_num > 1) {
  354. $data['amt_type'] = 'ALL_RAND';
  355. $api = self::MCH_BASE_URL . '/mmpaymkttransfers/sendgroupredpack';
  356. } else {
  357. $api = self::MCH_BASE_URL . '/mmpaymkttransfers/sendredpack';
  358. }
  359. $result = $this->postXmlSSL($data, $api);
  360. $json = Tools::xml2arr($result);
  361. if (!empty($json) && false === $this->_parseResult($json)) {
  362. return false;
  363. }
  364. return $json;
  365. }
  366. /**
  367. * 现金红包状态查询
  368. * @param string $billno
  369. * @return bool|array
  370. * @link https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_7&index=6
  371. */
  372. public function queryRedPack($billno) {
  373. $data['mch_billno'] = $billno;
  374. $data['bill_type'] = 'MCHT';
  375. $result = $this->postXmlSSL($data, self::MCH_BASE_URL . '/mmpaymkttransfers/gethbinfo');
  376. $json = Tools::xml2arr($result);
  377. if (!empty($json) && false === $this->_parseResult($json)) {
  378. return false;
  379. }
  380. return $json;
  381. }
  382. /**
  383. * 企业付款
  384. * @param string $openid 红包接收者OPENID
  385. * @param int $amount 红包总金额
  386. * @param string $billno 商户订单号
  387. * @param string $desc 备注信息
  388. * @return bool|array
  389. * @link https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2
  390. */
  391. public function transfers($openid, $amount, $billno, $desc) {
  392. $data = array();
  393. $data['mchid'] = $this->mch_id;
  394. $data['mch_appid'] = $this->appid;
  395. $data['partner_trade_no'] = $billno;
  396. $data['openid'] = $openid;
  397. $data['amount'] = $amount;
  398. $data['check_name'] = 'NO_CHECK'; #不验证姓名
  399. $data['spbill_create_ip'] = Tools::getAddress(); //调用接口的机器Ip地址
  400. $data['desc'] = $desc; //备注信息
  401. $result = $this->postXmlSSL($data, self::MCH_BASE_URL . '/mmpaymkttransfers/promotion/transfers');
  402. $json = Tools::xml2arr($result);
  403. if (!empty($json) && false === $this->_parseResult($json)) {
  404. return false;
  405. }
  406. return $json;
  407. }
  408. /**
  409. * 企业付款查询
  410. * @param string $billno
  411. * @return bool|array
  412. * @link https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_3
  413. */
  414. public function queryTransfers($billno) {
  415. $data['appid'] = $this->appid;
  416. $data['mch_id'] = $this->mch_id;
  417. $data['partner_trade_no'] = $billno;
  418. $result = $this->postXmlSSL($data, self::MCH_BASE_URL . '/mmpaymkttransfers/gettransferinfo');
  419. $json = Tools::xml2arr($result);
  420. if (!empty($json) && false === $this->_parseResult($json)) {
  421. return false;
  422. }
  423. return $json;
  424. }
  425. /**
  426. * 二维码链接转成短链接
  427. * @param string $url 需要处理的长链接
  428. * @return bool|string
  429. */
  430. public function shortUrl($url) {
  431. $data = array();
  432. $data['long_url'] = $url;
  433. $result = $this->getArrayResult($data, self::MCH_BASE_URL . '/tools/shorturl');
  434. if (!$result || $result['return_code'] !== 'SUCCESS') {
  435. $this->errCode = $result['return_code'];
  436. $this->errMsg = $result['return_msg'];
  437. return false;
  438. }
  439. if (isset($result['err_code']) && $result['err_code'] !== 'SUCCESS') {
  440. $this->errMsg = $result['err_code_des'];
  441. $this->errCode = $result['err_code'];
  442. return false;
  443. }
  444. return $result['short_url'];
  445. }
  446. /**
  447. * 发放代金券
  448. * @param int $coupon_stock_id 代金券批次id
  449. * @param string $partner_trade_no 商户此次发放凭据号(格式:商户id+日期+流水号),商户侧需保持唯一性
  450. * @param string $openid Openid信息
  451. * @param string $op_user_id 操作员帐号, 默认为商户号 可在商户平台配置操作员对应的api权限
  452. * @return bool|array
  453. * @link https://pay.weixin.qq.com/wiki/doc/api/tools/sp_coupon.php?chapter=12_3
  454. */
  455. public function sendCoupon($coupon_stock_id, $partner_trade_no, $openid, $op_user_id = null) {
  456. $data = array();
  457. $data['appid'] = $this->appid;
  458. $data['coupon_stock_id'] = $coupon_stock_id;
  459. $data['openid_count'] = 1;
  460. $data['partner_trade_no'] = $partner_trade_no;
  461. $data['openid'] = $openid;
  462. $data['op_user_id'] = empty($op_user_id) ? $this->mch_id : $op_user_id;
  463. $result = $this->postXmlSSL($data, self::MCH_BASE_URL . '/mmpaymkttransfers/send_coupon');
  464. $json = Tools::xml2arr($result);
  465. if (!empty($json) && false === $this->_parseResult($json)) {
  466. return false;
  467. }
  468. return $json;
  469. }
  470. }