GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Pay::refund()   B
last analyzed

Complexity

Conditions 6
Paths 6

Size

Total Lines 41
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 32
c 1
b 0
f 0
nc 6
nop 0
dl 0
loc 41
rs 8.7857
1
<?php
2
/**
3
 * Class Pay
4
 *
5
 * @link https://www.icy2003.com/
6
 * @author icy2003 <[email protected]>
7
 * @copyright Copyright (c) 2017, icy2003
8
 */
9
namespace icy2003\php\iapis\wechat;
10
11
use Exception;
12
use icy2003\php\C;
13
use icy2003\php\I;
14
use icy2003\php\iapis\Api;
15
use icy2003\php\ihelpers\Arrays;
16
use icy2003\php\ihelpers\Header;
17
use icy2003\php\ihelpers\Http;
18
use icy2003\php\ihelpers\Request;
19
use icy2003\php\ihelpers\Strings;
20
use icy2003\php\ihelpers\Url;
21
use icy2003\php\ihelpers\Xml;
22
23
/**
24
 * Pay 支付
25
 *
26
 * - 参看[微信支付开发文档](https://pay.weixin.qq.com/wiki/doc/api/index.html)
27
 * - 由于涉及参数过多,为了保持方法的整洁,请结合文档和本类的 set 方法添加参数,如遇到必要参数没设置的情况,会抛错。
28
 * - 可用方法名对应微信文档里 API 路由,例如[统一下单](https://api.mch.weixin.qq.com/pay/unifiedorder),对应方法名为 unifiedOrder
29
 */
30
class Pay extends Api
31
{
32
33
    use PaySetterTrait;
34
35
    /**
36
     * 初始化
37
     *
38
     * @param string $mchid 商户号。微信支付分配的商户号
39
     * @param string $appid 应用ID。微信开放平台审核通过的应用APPID(请登录open.weixin.qq.com查看,注意与公众号的APPID不同)
40
     * @param string $apiKey 密钥。key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置
41
     */
42
    public function __construct($mchid, $appid, $apiKey)
43
    {
44
        $this->_mchId = $mchid;
45
        if (null === $this->_mchId) {
46
            throw new Exception("缺少商户号");
47
        }
48
        $this->_appId = $appid;
49
        if (null === $this->_appId) {
50
            throw new Exception("缺少应用 ID");
51
        }
52
        $this->_apiKey = $apiKey;
53
        if (null === $this->_apiKey) {
54
            throw new Exception("缺少密钥");
55
        }
56
    }
57
58
    /**
59
     * 获取终端 IP
60
     *
61
     * @return string
62
     */
63
    public function getIp()
64
    {
65
        return (new Request())->getRemoteIP();
66
    }
67
68
    /**
69
     * 生成签名
70
     *
71
     * @param array $params 签名参数
72
     *
73
     * @return string
74
     */
75
    public function getSign($params)
76
    {
77
        ksort($params);
78
        $arr = [];
79
        foreach ($params as $key => $value) {
80
            if ($key != 'sign' && !empty($value)) {
81
                $arr[] = $key . '=' . $value;
82
            }
83
        }
84
        $arr[] = 'key=' . $this->_apiKey;
85
        $string = implode('&', $arr);
86
        $method = I::get($params, 'sign_type', 'MD5');
87
        if ('MD5' === $method) {
88
            $string = md5($string);
89
        } elseif ('HMAC-SHA256' === $method) {
90
            $string = hash_hmac("sha256", $string, $this->_apiKey);
91
        } else {
92
            throw new Exception("签名类型不支持!");
93
        }
94
        return strtoupper($string);
95
    }
96
97
    /**
98
     * 支付类型:APP
99
     */
100
    const TRADE_TYPE_APP = 'APP';
101
    /**
102
     * 支付类型:JSAPI
103
     */
104
    const TRADE_TYPE_JSAPI = 'JSAPI';
105
    /**
106
     * 支付类型:Native
107
     */
108
    const TRADE_TYPE_NATIVE = 'NATIVE';
109
    /**
110
     * 支付类型:H5
111
     */
112
    const TRADE_TYPE_H5 = 'MWEB';
113
    /**
114
     * 支付类型:付款码
115
     */
116
    const TRADE_TYPE_MICROPAY = 'MICROPAY';
117
118
    /**
119
     * 统一下单
120
     *
121
     * - 商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再在APP里面调起支付
122
     *
123
     * @return static
124
     */
125
    public function unifiedOrder()
126
    {
127
        if (null === ($body = I::get($this->_options, 'body'))) {
128
            throw new Exception('请使用 setBody() 设置商品描述 body');
129
        }
130
        if (null === ($outTradeNo = I::get($this->_options, 'out_trade_no'))) {
131
            throw new Exception('请使用 setOutTradeNo() 设置订单号 out_trade_no');
132
        }
133
        if (null === ($totalFee = I::get($this->_options, 'total_fee'))) {
134
            throw new Exception('请使用 setTotalFee() 设置商品金额 total_fee');
135
        }
136
        if (null === ($notifyUrl = I::get($this->_options, 'notify_url'))) {
137
            throw new Exception('请使用 setNotifyUrl() 设置通知地址 notify_url');
138
        }
139
        if (null === ($tradeType = I::get($this->_options, 'trade_type'))) {
140
            throw new Exception('请使用 setTradeType() 设置交易类型 trade_type');
141
        }
142
        $values = array_filter([
143
            'appid' => $this->_appId,
144
            'mch_id' => $this->_mchId,
145
            'device_info' => I::get($this->_options, 'device_info'),
146
            'nonce_str' => Strings::random(),
147
            'sign_type' => I::get($this->_options, 'sign_type'),
148
            'body' => $body,
149
            'detail' => I::get($this->_options, 'detail'),
150
            'attach' => I::get($this->_options, 'attach'),
151
            'out_trade_no' => $outTradeNo,
152
            'fee_type' => I::get($this->_options, 'fee_type'),
153
            'total_fee' => $totalFee,
154
            'spbill_create_ip' => $this->getIp(),
155
            'time_start' => I::get($this->_options, 'time_start'),
156
            'time_expire' => I::get($this->_options, 'time_expire'),
157
            'goods_tag' => I::get($this->_options, 'goods_tag'),
158
            'notify_url' => $notifyUrl,
159
            'trade_type' => $tradeType,
160
            'limit_pay' => I::get($this->_options, 'limit_pay'),
161
            'receipt' => I::get($this->_options, 'receipt'),
162
        ]);
163
164
        if ('NATIVE' === $tradeType) {
165
            if (null === ($productId = I::get($this->_options, 'product_id'))) {
166
                throw new Exception('请使用 setProductId() 设置商品描述 product_id');
167
            } else {
168
                $values['product_id'] = $productId;
169
            }
170
        } elseif ('JSAPI' === $tradeType) {
171
            if (null === ($openId = I::get($this->_options, 'openid'))) {
172
                throw new Exception('请使用 setOpenId() 设置 OpenID openid');
173
            } else {
174
                $values['openid'] = $openId;
175
            }
176
        } elseif ('MWEB' === $tradeType) {
177
            if (null === ($sceneInfo = I::get($this->_options, 'scene_info'))) {
178
                throw new Exception('请使用 setSceneInfo() 设置场景信息 scene_info');
179
            } else {
180
                $values['scene_info'] = $sceneInfo;
181
            }
182
        }
183
        $values['sign'] = $this->getSign($values);
184
        $responseXml = Http::body('https://api.mch.weixin.qq.com/pay/unifiedorder', Xml::fromArray($values));
185
        $this->_result = Xml::toArray($responseXml);
186
        return $this;
187
    }
188
189
    /**
190
     * 交易成功!
191
     *
192
     * - 只有交易成功有意义
193
     *
194
     * @return boolean
195
     */
196
    public function isSuccess()
197
    {
198
        return 'SUCCESS' === I::get($this->_result, 'return_code');
199
    }
200
201
    /**
202
     * 返回用于拉起微信支付用的前端参数
203
     *
204
     * @return array
205
     */
206
    public function getCallArray()
207
    {
208
        C::assertTrue($this->isSuccess(), (string) I::get($this->_result, 'return_msg'));
209
        $array = [];
210
        if (self::TRADE_TYPE_APP === I::get($this->_options, 'trade_type')) {
211
            $array = [
212
                'appid' => $this->_appId,
213
                'partnerid' => $this->_mchId,
214
                'prepayid' => I::get($this->_result, 'prepay_id'),
215
                'package' => 'Sign=WXPay',
216
                'noncestr' => Strings::random(),
217
                'timestamp' => time(),
218
            ];
219
            $array['sign'] = $this->getSign($array);
220
        }
221
        if (self::TRADE_TYPE_H5 === I::get($this->_options, 'trade_type')) {
222
            $array = [
223
                'mweb_url' => I::get($this->_result, 'mweb_url'),
224
            ];
225
        }
226
        if (self::TRADE_TYPE_JSAPI == I::get($this->_options, 'trade_type')) {
227
            $array = [
228
                'appId' => $this->_appId,
229
                'nonceStr' => Strings::random(),
230
                'package' => 'prepay_id=' . I::get($this->_result, 'prepay_id'),
231
                'signType' => I::get($this->_options, 'sign_type', 'MD5'),
232
                'timeStamp' => (string) time(),
233
            ];
234
            $array['paySign'] = $this->getSign($array);
235
        }
236
        return $array;
237
    }
238
239
    /**
240
     * 支付结果通知以及退款结果通知的数据处理
241
     *
242
     * - 如果交易成功,并且签名校验成功,返回数据
243
     *
244
     * @return array
245
     */
246
    public function getNotifyArray()
247
    {
248
        $xml = (new Request())->getRawBody();
249
        $array = Xml::toArray($xml);
250
        C::assertTrue('SUCCESS' === I::get($array, 'return_code') && 'SUCCESS' === I::get($array, 'result_code'), (string) I::get($array, 'return_msg'));
251
        $temp = $array;
252
        $sign = $temp['sign'];
253
        unset($temp['sign']);
254
        if ($this->getSign($temp) == $sign) {
255
            return $array;
256
        }
257
        return [];
258
    }
259
260
    /**
261
     * 返回通知成功时发送给微信的 XML
262
     *
263
     * @return string
264
     */
265
    public function getNotifyReturn()
266
    {
267
        return '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
268
    }
269
270
    /**
271
     * self::getNotifyArray 和 self::getNotifyReturn 的结合:通知为交易成功时,$callback 为 true,则输出成功给微信
272
     *
273
     * @param callback $callback 回调函数,true 或设置回调则输出成功,回调函数提供了微信给的通知数组 $array
274
     *
275
     * @return void
276
     * @info 此函数之后不得有任何输出
277
     */
278
    public function notify($callback = null)
279
    {
280
        $array = $this->getNotifyArray();
281
        if (!empty($array)) {
282
            if (null === $callback || true === I::call($callback, [$array])) {
283
                Header::xml();
284
                echo $this->getNotifyReturn();
285
            }
286
        }
287
    }
288
289
    /**
290
     * 该接口提供所有微信支付订单的查询,商户可以通过该接口主动查询订单状态
291
     *
292
     * @return static
293
     */
294
    public function orderQuery()
295
    {
296
        $values = array_filter([
297
            'appid' => $this->_appId,
298
            'mch_id' => $this->_mchId,
299
            'nonce_str' => Strings::random(),
300
            'transaction_id' => I::get($this->_options, 'transaction_id'),
301
            'out_trade_no' => I::get($this->_options, 'out_trade_no'),
302
        ]);
303
        if (false === Arrays::keyExistsSome(['transaction_id', 'out_trade_no'], $values)) {
304
            throw new Exception('transaction_id 和 out_trade_no 必须二选一');
305
        }
306
        $values['sign'] = $this->getSign($values);
307
        $responseBody = Http::body('https://api.mch.weixin.qq.com/pay/orderquery', Xml::fromArray($values));
308
        $this->_result = Xml::toArray($responseBody);
309
        return $this;
310
    }
311
312
    /**
313
     * 关闭订单
314
     *
315
     * @return static
316
     */
317
    public function closeOrder()
318
    {
319
        if (null === ($outTradeNo = I::get($this->_options, 'out_trade_no'))) {
320
            throw new Exception('请使用 setOutTradeNo() 设置订单号 out_trade_no');
321
        }
322
        $values = [
323
            'appid' => $this->_appId,
324
            'mch_id' => $this->_mchId,
325
            'out_trade_no' => $outTradeNo,
326
            'nonce_str' => Strings::random(),
327
        ];
328
        $values['sign'] = $this->getSign($values);
329
        $responseBody = Http::body('https://api.mch.weixin.qq.com/pay/closeorder', Xml::fromArray($values));
330
        $this->_result = Xml::toArray($responseBody);
331
        return $this;
332
    }
333
334
    /**
335
     * 申请退款
336
     *
337
     * @return static
338
     */
339
    public function refund()
340
    {
341
        if (null === $this->_certPath) {
342
            throw new Exception('请使用 setCertPath() 提供证书路径,下载的证书名为:apiclient_cert.pem');
343
        }
344
        if (null === $this->_certKeyPath) {
345
            throw new Exception('请使用 setCertKeyPath() 提供证书密钥路径,下载的密钥名为:apiclient_key.pem');
346
        }
347
        $this->setOutRefundNo();
348
        if (null === I::get($this->_options, 'out_refund_no')) {
349
            throw new Exception('请使用 setOutRefundNo() 设置退款单号 out_refund_no,若不填,则和 out_trade_no 一致');
350
        }
351
        $this->setRefundFee();
352
        if (null === I::get($this->_options, 'refund_fee')) {
353
            throw new Exception('请使用 setRefundFee() 设置退款金额 refund_fee,若不填,则和 total_fee 一致');
354
        }
355
        $values = array_filter([
356
            'appid' => $this->_appId,
357
            'mch_id' => $this->_mchId,
358
            'nonce_str' => Strings::random(),
359
            'sign_type' => I::get($this->_options, 'sign_type'),
360
            'transaction_id' => I::get($this->_options, 'transaction_id'),
361
            'out_trade_no' => I::get($this->_options, 'out_trade_no'),
362
            'out_refund_no' => I::get($this->_options, 'out_refund_no'),
363
            'total_fee' => I::get($this->_options, 'total_fee'),
364
            'refund_fee' => I::get($this->_options, 'refund_fee'),
365
            'refund_fee_type' => I::get($this->_options, 'refund_fee_type'),
366
            'refund_desc' => I::get($this->_options, 'refund_desc'),
367
            'refund_account' => I::get($this->_options, 'refund_account'),
368
            'notify_url' => I::get($this->_options, 'notify_url'),
369
        ]);
370
        if (false === Arrays::keyExistsSome(['transaction_id', 'out_trade_no'], $values)) {
371
            throw new Exception('transaction_id 和 out_trade_no 必须二选一');
372
        }
373
        $values['sign'] = $this->getSign($values);
374
        $responseBody = Http::body('https://api.mch.weixin.qq.com/secapi/pay/refund', Xml::fromArray($values), [], [
375
            'cert' => $this->_certPath,
376
            'ssl_key' => $this->_certKeyPath,
377
        ]);
378
        $this->_result = Xml::toArray($responseBody);
379
        return $this;
380
    }
381
382
    /**
383
     * 查询退款
384
     *
385
     * @return static
386
     */
387
    public function refundQuery()
388
    {
389
        $values = array_filter([
390
            'appid' => $this->_appId,
391
            'mch_id' => $this->_mchId,
392
            'nonce_str' => Strings::random(),
393
            'sign_type' => I::get($this->_options, 'sign_type'),
394
            'transaction_id' => I::get($this->_options, 'transaction_id'),
395
            'out_trade_no' => I::get($this->_options, 'out_trade_no'),
396
            'out_refund_no' => I::get($this->_options, 'out_refund_no'),
397
            'refund_id' => I::get($this->_options, 'refund_id'),
398
            'offset' => I::get($this->_options, 'offset'),
399
        ]);
400
        if (false === Arrays::keyExistsSome(['transaction_id', 'out_trade_no', 'out_refund_no', 'refund_id'], $values)) {
401
            throw new Exception('transaction_id、out_trade_no、out_refund_no 和 refund_id 必须四选一');
402
        }
403
        $values['sign'] = $this->getSign($values);
404
        $responseBody = Http::body('https://api.mch.weixin.qq.com/pay/refundquery', Xml::fromArray($values));
405
        $this->_result = Xml::toArray($responseBody);
406
        return $this;
407
    }
408
409
    /**
410
     * 下载对账单
411
     *
412
     * @return static
413
     */
414
    public function downloadBill()
415
    {
416
        if (null === ($billDate = I::get($this->_options, 'bill_date'))) {
417
            throw new Exception('请使用 setBillDate() 设置对账单日期 bill_date,格式:20140603');
418
        }
419
        $values = array_filter([
420
            'appid' => $this->_appId,
421
            'mch_id' => $this->_mchId,
422
            'nonce_str' => Strings::random(),
423
            'bill_date' => $billDate,
424
            'bill_type' => I::get($this->_options, 'bill_type'),
425
            'tar_type' => I::get($this->_options, 'tar_type'),
426
        ]);
427
        $values['sign'] = $this->getSign($values);
428
        $responseBody = Http::body('https://api.mch.weixin.qq.com/pay/downloadbill', Xml::fromArray($values));
429
        $this->_result = Xml::toArray($responseBody);
430
        return $this;
431
    }
432
433
    /**
434
     * 下载资金账单
435
     *
436
     * @return static
437
     */
438
    public function downloadFundFlow()
439
    {
440
        if (null === $this->_certPath) {
441
            throw new Exception('请使用 setCertPath() 提供证书路径,下载的证书名为:apiclient_cert.pem');
442
        }
443
        if (null === $this->_certKeyPath) {
444
            throw new Exception('请使用 setCertKeyPath() 提供证书密钥路径,下载的密钥名为:apiclient_key.pem');
445
        }
446
        if (null === ($accoutType = I::get($this->_options, 'account_type'))) {
447
            throw new Exception('请使用 setAccountType() 设置资金账户类型 account_type');
448
        }
449
        if (null === ($billDate = I::get($this->_options, 'bill_date'))) {
450
            throw new Exception('请使用 setBillDate() 设置资金账单日期 bill_date');
451
        }
452
        $values = array_filter([
453
            'appid' => $this->_appId,
454
            'mch_id' => $this->_mchId,
455
            'nonce_str' => Strings::random(),
456
            'sign_type' => 'HMAC-SHA256',
457
            'bill_date' => $billDate,
458
            'account_type' => $accoutType,
459
            'tar_type' => I::get($this->_options, 'tar_type'),
460
        ]);
461
        $values['sign'] = $this->getSign($values);
462
        $responseBody = Http::body('https://api.mch.weixin.qq.com/pay/downloadfundflow', Xml::fromArray($values), [], [
463
            'cert' => $this->_certPath,
464
            'ssl_key' => $this->_certKeyPath,
465
        ]);
466
        $this->_result = Xml::toArray($responseBody);
467
        return $this;
468
    }
469
470
    /**
471
     * 交易保障
472
     *
473
     * @todo 我不知道这货干嘛用的
474
     *
475
     * @return false
476
     */
477
    public function report()
478
    {
479
        return false;
480
    }
481
482
    /**
483
     * 转换短链接
484
     *
485
     * @todo 测试没通过
486
     *
487
     * @return static
488
     */
489
    public function shortUrl()
490
    {
491
        C::assertTrue(null !== ($longUrl = (string) I::get($this->_options, 'long_url')), '缺少 long_url 参数!');
492
        $values = array_filter([
493
            'appid' => $this->_appId,
494
            'mch_id' => $this->_mchId,
495
            'long_url' => $longUrl,
496
            'nonce_str' => Strings::random(),
497
            'sign_type' => I::get($this->_options, 'sign_type'),
498
        ]);
499
        $temp = $values;
500
        $temp['long_url'] = Url::encode($longUrl);
501
        $values['sign'] = $this->getSign($values);
502
        $responseBody = Http::body('https://api.mch.weixin.qq.com/tools/shorturl', Xml::fromArray($temp));
503
        $this->_result = Xml::toArray($responseBody);
504
        return $this;
505
    }
506
507
    /**
508
     * 拼接二维码地址
509
     *
510
     * - 详见[模式一](https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_4)
511
     *
512
     * @return string
513
     */
514
    public function getQrcodeUrl()
515
    {
516
        C::assertTrue(null !== ($productId = (string) I::get($this->_options, 'product_id')), '缺少 product_id 参数!');
517
        $values = [
518
            'appid' => $this->_appId,
519
            'mch_id' => $this->_mchId,
520
            'time_stamp' => time(),
521
            'nonce_str' => Strings::random(),
522
            'product_id' => $productId,
523
        ];
524
        $values['sign'] = $this->getSign($values);
525
        return 'weixin://wxpay/bizpayurl?sign=' . $values['sign'] .
526
            '&appid=' . $values['appid'] .
527
            '&mch_id=' . $values['mch_id'] .
528
            '&product_id=' . $values['product_id'] .
529
            '&time_stamp=' . $values['time_stamp'] .
530
            '&nonce_str=' . $values['nonce_str'];
531
    }
532
533
    /**
534
     * 在统一下单之后,输出此 XML,可让扫码拉起支付
535
     *
536
     * - 详见[模式一](https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_4)
537
     * - 模式一主要流程:
538
     *      1. 商户平台设置扫码回调
539
     *      2. 调用 self::getQrcodeUrl 获取二维码地址,生成二维码给用户扫描支付,微信会发消息到回调地址
540
     *      3. 回调接收到微信消息,获取 product_id 和 openid,调用统一下单接口
541
     *      4. 设置 prepay_id 后调用此函数,返回给微信,即可实现微信扫码支付
542
     *
543
     * @return string
544
     */
545
    public function getQrcodeCallXml()
546
    {
547
        if (null === ($prepayId = I::get($this->_options, 'prepay_id'))) {
548
            throw new Exception('缺少 prepay_id 参数!');
549
        }
550
        $values = [
551
            'return_code' => 'SUCCESS',
552
            'appid' => $this->_appId,
553
            'mch_id' => $this->_mchId,
554
            'nonce_str' => Strings::random(),
555
            'prepay_id' => $prepayId,
556
            'result_code' => 'SUCCESS',
557
        ];
558
        $values['sign'] = $this->getSign($values);
559
        return Xml::fromArray($values);
560
    }
561
}
562