第十章 后端财务管理

订单支付状态查询

本小节,我们将来学习通过调用微信支付接口查询订单的支付状态,这里的支付状态与我们订单表中记录的订单状态是有一定的区别的。通常情况下,我们的订单表记录一开始是未支付状态,在用户进行了微信支付之后,APP后端的API接收到微信支付的支付成功通知回调后,经过一些逻辑判断,会把订单记录的支付状态改成已支付状态,这是最理想也是正常的情况。但是因为某些原因,比如APP后端API处理支付成功回调通知的过程中出现一些异常,又或者微信支付没有正确发送支付成功的回调等原因导致APP用户虽然支付成功了,但是订单状态并没有改变,因为支付到底成功没这个状态是支付平台去维护的,但是订单的状态是我们自己系统内维护的,难免会发生一些意外情况导致两个状态的信息不一致,这也是作者在实际工作中碰到过的问题。一开始的操作方式是复制一下订单号,然后打开登陆微信支付的商户后台,查询订单的状态看看是不是真的支付了再反馈给相关的工作人员,这个流程不是不可以,只是效率太低,既然我们有CMS这种东西,那理所当然的就应该把这个查询功能也集成到CMS中,就好像我们查询订单物流状态一样,直接在CMS查询而不是打开多一个网站。

在正式开始本小节的内容之前,有一个不幸的消息要告诉各位读者,那就是实现接口功能的一个前提就是你必须要有微信支付商户的相关资料(appid、秘钥、证书等),如果你是个人身份,不能拿到公司或者跟人借一个微信支付商户那么即便你实现了接口功能也无法进行测试(但是你可以看我怎么测试的)。那么对于没有微信支付商户的读者们,本小节的学习就到此结束直接进入下一小节了吗?很遗憾的是,下一小节、下下小节的内容依然需要微信支付商户的支持(措手不及.jpg)。这里作者的建议是照常学习即可,虽然最后不能像有微信支付商户的同学一样可以骄傲的联调测试,但是掌握如何调用微信支付的接口也是很关键的一环,读者可以先掌握了方法然后待日后做了老板或者工作上需要实现功能时可以回过头来翻一翻本小节的内容,具体实现过程绝对讲得比搜索引擎上大部分复制粘贴的内容要来得清晰易懂。

接下来就让我们怀着忐忑的心情开始我们本小节内容的学习,首先需要先了解下微信支付相关的接口,点击打开微信支付开发文档首页

这里我们选择小程序支付,跳转到对应支付类型的开发文档,其实各个支付类型的微信支付接口是一样的,文档内容还写得差不多,就是一些参数会有差异,调用上大同小异,可以说会一个就会其他的。

我们点击展开左边菜单栏中的API列表,可以看到微信支付为开发者提供好几个接口,这里面就有我们需要用到的订单查询接口,点击接口名称就可以看到关于该接口的调用方法介绍,但是这里作者并不打算带着大家阅读这开发文档,因为这文档写得真的是很不走心,乍一看你会给一堆的参数和签名劝退,对新手真的很不友好,这里文档的内容读者可以先大致浏览一下有个印象即可,但不是说这个文档就没用了,只是先放一放。这里我们选择利用微信支付提供给我们的SDK结合文档来上手微信支付接口的调用,通过利用微信支付提供的SDK,可以简化我们一些接口调用的实现过程。SDK点击下载:https://pay.weixin.qq.com/wiki/doc/api/download/WxpayAPI_php.zip

SDK即“软体开发工具包”,一般是一些被软件工程师用于为特定的软件包、软件框架、硬件平台、操作系统等建立应用软件的开发工具的集合。通俗点是指由第三方服务商提供的实现软件产品某项功能的工具包。——网络释义

下载完SDK包后会得到一个.zip的压缩包,解压后得到一个名为php_sdk_v3.0.10的文件夹,双击进入后会看到以下几个文件夹:

注意这里的版本号,如果当你下载的时候发现解压出来的版本号不一样然后又无法实现与本小节内容一样的效果可以联系作者索取该版本的压缩包

首先来大概认识下这几个目录都是干嘛的:

  • doc

这个目录下有一个README.doc的说明文档,虽然一样写得很不走心,但是也提供了一些有用的信息,推荐读者先阅读一下,对理解后面功能实现有帮助。

  • example

这个是SDK使用的示例代码,在某些情况下可以结合文档来搞明白这SDK到底怎么用,但记住这里只是示例代码,切勿直接用于线上生产环境。

  • lib

这个是微信支付提供给开发者的类库文件,里面包含了我们需要调用的接口和一系列方法,我们主要是需要这个目录里面的东西。双击打开这个目录,可以看到:

看名字能大概猜到是干什么的,看不懂也不要紧,首先复制这5个PHP文件,然后在lin-cms-tp5项目根目录下的extend目录下新建一个叫wx_pay的目录并把这5个PHP文件粘贴进去:

按照TP5的目录结构规范,extend目录用于存放非composer方式安装的第三方类库文件

SDK文件拷贝好之后,我们就要来开始动手写代码了,首先在控制器层下的Order控制器类下新增一个getOrderPayStatus()方法:

<?php


namespace app\api\controller\v1;

use app\api\model\Order as OrderModel;
use app\api\service\Order as OrderService;
use app\api\model\DeliverRecord as DeliverRecordModel;
use app\lib\exception\order\OrderException;
use think\facade\Request;

class Order
{
    /**分页查询所有订单*/
    public function getOrders(){...}

    /**订单发货*/
    public function deliverGoods($id){...}
    /**
     * @param $orderNo
     */
    public function getOrderPayStatus($orderNo)
    {
        
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

控制器方法定义好之后,问题来了,那SDK怎么用?直接看SDK的源码有点无从下手,我们先到刚刚提到的doc目录下的README.doc中看看有没有什么有用的信息,双击打开后重点看这段内容:

微信支付的PHPer们连word文档的格式都放弃掉了,不过好歹这里解释了之前lib目录下5个PHP文件的用途,WxPay.Api.phpWxPay.Config.Interface.php从文档的描述可以看出这两个文件应该是关键点所在了,接下来让我们带着疑问来尝试实现一下。这里我们把查询订单支付状态的功能按照分层的思想,把它定义成一个服务,在项目的服务层下,新建一个WxPay服务类,在类里面新增一个getWxOrderStatus()方法:

<?php


namespace app\api\service;


class WxPay
{
    private $orderNo;

    public function __construct($orderNo)
    {
        $this->orderNo = $orderNo;
    }

    public function getWxOrderStatus()
    {
       
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

doc文档里面说了,WxPay.Api.php包含了所有微信支付API接口的封装,我们先来看看这个文件里面有什么东西,双击打开WxPay.Api.php文件,这个文件里面实现了一个WxPayApi类,里面有很多静态方法对应不同的微信支付接口,好在这里的注释算是写到位了,不然这专栏写不下去了。我们可以大概浏览下这里面都有啥方法,大概在第85行处,有一个orderQuery()方法:

通过注释说明我们可以知道这个就是用来查询订单的方法,这个方法接收两个参数,$config是一个配置信息对象,$inputObj是一个查询参数对象,我们在调用这个查询方法的时候传入这两个对象就可以实现查询订单状态了,方法找到了,让我们回到服务层中的getWxOrderStatus()方法:

<?php


namespace app\api\service;

// 注意这里的引用路径是以TP5框架入口文件所在目录算起的,即项目根目录\public\index.php
require_once "../extend/wx_pay/WxPay.Api.php";

class WxPay
{
    private $orderNo;

    public function __construct($orderNo)
    {
        $this->orderNo = $orderNo;
    }

    public function getWxOrderStatus()
    {
       $payStatus = \WxPayApi::orderQuery();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

由于微信支付的SDK类库文件都没有使用命名空间,这里我们不能直接像之前写代码那样use xxxxx然后就可以使用,这里我们需要使用require_once 类库文件路径的方式来引入。前面我们看了orderQuery()方法的源码,他需要接收两个对象,一个配置信息对象,一个查询参数对象,那么这两个对象从哪来的呢?我们来逐一研究下,首先是配置信息对象,再次翻看doc文档,文档中已经给了我们提示:

这里告诉了我们,需要我们继承并实现WxPay.Config.Interface.php,我们继承之后的实现类在实例化后的对象就是用来传递给orderQuery()方法的。事不宜迟,我们在服务层下,再新建一个WxPayConfig服务类,然后继承WxPayConfigInterface:

<?php

namespace app\api\service;

require_once "../extend/wx_pay/WxPay.Config.Interface.php";

class WxPayConfig extends \WxPayConfigInterface
{

}
1
2
3
4
5
6
7
8
9
10

这里我们同样需要使用require_once引用一下WxPay.Config.Interface.php文件,由于WxPayConfigInterface类本身是一个抽象类,子类必须实现父类中所有的抽象方法,IDE也很智能的提示了我们:

这里说我们必须实现这一堆的方法,这里我们当然要实现,不然实现不了功能,在红线处按住键盘上的Alt+回车

,选择Add method stubs 接着再回车,IDE就会自动帮你填充了一堆的代码:

<?php
 
namespace app\api\service;

require_once "../extend/wx_pay/WxPay.Config.Interface.php";

class WxPayConfig extends \WxPayConfigInterface
{

    /**
     * TODO: 修改这里配置为您自己申请的商户信息
     * 微信公众号信息配置
     *
     * APPID:绑定支付的APPID(必须配置,开户邮件中可查看)
     *
     * MCHID:商户号(必须配置,开户邮件中可查看)
     *
     */
    public function GetAppId()
    {
        // TODO: Implement GetAppId() method.
    }

    public function GetMerchantId()
    {
        // TODO: Implement GetMerchantId() method.
    }

    /**
     * TODO:支付回调url
     * 签名和验证签名方式, 支持md5和sha256方式
     **/
    public function GetNotifyUrl()
    {
        // TODO: Implement GetNotifyUrl() method.
    }

    public function GetSignType()
    {
        // TODO: Implement GetSignType() method.
    }

    /**
     * TODO:这里设置代理机器,只有需要代理的时候才设置,不需要代理,请设置为0.0.0.0和0
     * 本例程通过curl使用HTTP POST方法,此处可修改代理服务器,
     * 默认CURL_PROXY_HOST=0.0.0.0和CURL_PROXY_PORT=0,此时不开启代理(如有需要才设置)
     * @var unknown_type
     */
    public function GetProxy(&$proxyHost, &$proxyPort)
    {
        // TODO: Implement GetProxy() method.
    }

    /**
     * TODO:接口调用上报等级,默认紧错误上报(注意:上报超时间为【1s】,上报无论成败【永不抛出异常】,
     * 不会影响接口调用流程),开启上报之后,方便微信监控请求调用的质量,建议至少
     * 开启错误上报。
     * 上报等级,0.关闭上报; 1.仅错误出错上报; 2.全量上报
     * @var int
     */
    public function GetReportLevenl()
    {
        // TODO: Implement GetReportLevenl() method.
    }

    public function GetKey()
    {
        // TODO: Implement GetKey() method.
    }

    public function GetAppSecret()
    {
        // TODO: Implement GetAppSecret() method.
    }

    /**
     * TODO:设置商户证书路径
     * 证书路径,注意应该填写绝对路径(仅退款、撤销订单时需要,可登录商户平台下载,
     * API证书下载地址:https://pay.weixin.qq.com/index.php/account/api_cert,下载之前需要安装商户操作证书)
     * 注意:
     * 1.证书文件不能放在web服务器虚拟目录,应放在有访问权限控制的目录中,防止被他人下载;
     * 2.建议将证书文件名改为复杂且不容易猜测的文件名;
     * 3.商户服务器要做好病毒和木马防护工作,不被非法侵入者窃取证书文件。
     * @var path
     */
    public function GetSSLCertPath(&$sslCertPath, &$sslKeyPath)
    {
        // TODO: Implement GetSSLCertPath() method.
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

这里可以看到我们填充了很多方法,但是方法体内都是空的,方法体内返回什么这里就是需要我们来实现的了。通过方法名和注释说明可以清楚的知道每个方法需要返回什么内容,这里返回的方法也很简单,就是直接在对应的方法体内直接return 'xxxxxx'即可,但是这里我们稍微优化一下。WxPayConfig类里的方法返回的都是一些跟微信支付商户有关的配置,如果有一天我们更换了微信支付商户,或者是需要在别的项目上复用这些代码,直接把微信支付商户的配置信息直接写在方法体中返回就显得不大合适了,一个是改动不方便另一个是不安全,我们需要把微信支付商户的配置信息放到某个配置文件中,WxPayConfig类只负责来读这个配置文件并返回对应的配置项信息即可。思路确定了,来实现一下,在项目根目录下的config目录下新建一个wx.php文件,并加入以下内容:

<?php

return [
    'app_id' => '', #绑定支付的APPID(必须配置,开户邮件中可查看)
    'merchant_id' => '', #商户号(必须配置,开户邮件中可查看)
    'sign_type' => 'MD5', #签名加密类型,直接MD5即可
    'key' => '', # 微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置
];
1
2
3
4
5
6
7
8

这里的几个配置项有微信支付商户的读者根据实际情况填写进去,没有的读者自己脑海中想象一下自己在填写即可。填写完毕之后,我们回到服务层下刚刚的WxPayConfig服务类,在类里面添加一个成员变量和构造方法:

<?php

namespace app\api\service;

use think\facade\Config;

require_once "../extend/wx_pay/WxPay.Config.Interface.php";

class WxPayConfig extends \WxPayConfigInterface
{
    private $wxConfig;

    public function __construct($config)
    {
        $this->wxConfig = Config::pull($config);
    }

    .......................................................
    .......................................................
    .......................................................
}    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在构造方法中,我们利用TP5的Config::pull()来获取我们刚刚配置文件内容并把结果赋值给一个私有成员变量$wxConfig。pull()方法接收一个字符串,代表了配置文件的文件名,这里我们考虑到灵活性,我们让这个参数值由外部来传递。接着我们来具体实现下父类的方法了:

<?php

namespace app\api\service;

use think\facade\Config;

require_once "../extend/wx_pay/WxPay.Config.Interface.php";

class WxPayConfig extends \WxPayConfigInterface
{
    private $wxConfig;

    public function __construct($config)
    {
        $this->wxConfig = Config::pull($config);
    }

    public function GetAppId()
    {
        return $this->wxConfig['app_id'];
    }

    public function GetMerchantId()
    {
        return $this->wxConfig['merchant_id'];
    }

    public function GetSignType()
    {
        return $this->wxConfig['sign_type'];
    }

    public function GetKey()
    {
        return $this->wxConfig['key'];
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

对于订单支付状态查询,我们暂时只需要实现这几个方法的返回值即可,这样我们的配置信息类就实现好了,只需要在调用的地方new一下这个WxPayConfig类就可以得到一个配置信息对象,让我们回到WxPay服务类中试一下:

<?php


namespace app\api\service;

// 注意这里的引用路径是以TP5框架入口文件所在目录算起的,即项目根目录\public\index.php
require_once "../extend/wx_pay/WxPay.Api.php";

class WxPay
{
    private $orderNo;
    private $config;


    public function __construct($orderNo)
    {
        $this->orderNo = $orderNo;
        $this->config = new WxPayConfig('wx');
    }

    public function getWxOrderStatus()
    {
       $payStatus = \WxPayApi::orderQuery($this->config);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

这里我们在构造方法中添加了一行代码,用于实例化我们刚刚实现的WxPayConfig类并指定要获取的配置文件文件名。然后把实例对象赋值给一个成员变量,在下面getWxOrderStatus()方法中调用微信支付接口的时候,把这个配置信息对象传递进去,这样子我们就搞定了第一个参数了。那第二个参数是怎么来的呢?第二个参数是一个查询对象的实例,同样翻看一下doc文档找找线索,文档中提示WxPay.Data.php是输入参数封装,我们只需要调用这个文件里面的某个类就可以实现,由于WxPay.Api.php文件已经引入了这个文件,所以这里我们不需要重复引入可以直接调用:

<?php


namespace app\api\service;

// 注意这里的引用路径是以TP5框架入口文件所在目录算起的,即项目根目录\public\index.php
require_once "../extend/wx_pay/WxPay.Api.php";

class WxPay
{
    private $orderNo;
    private $config;


    public function __construct($orderNo)
    {
        $this->orderNo = $orderNo;
        $this->config = new WxPayConfig();
    }

    public function getWxOrderStatus()
    {
       // 生成查询参数对象
       $inputObj = $this->generateOrderQuery();
       $payStatus = \WxPayApi::orderQuery($this->config,$inputObj);
    }

    /**
     * 生成微信支付订单查询参数对象
     */
    protected function generateOrderQuery()
    {
        // 实例化订单查询输入对象
        $inputObj = new \WxPayOrderQuery();
        // 设置商户订单号,用于查询条件
        $inputObj->SetOut_trade_no($this->orderNo);
        return $inputObj;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

这里我们封装了一个generateOrderQuery方法用于返回这个查询对象,方法中调用的WxPayOrderQuery类就是微信支付SDK提供的用于封装和实例化查询参数对象的,这里我们首先得到这个类的实例对象,然后调用对象的方法SetOut_trade_no来给对象的属性赋值,这里我们是通过商户订单号,最后把整个实例化好的查询参数对象返回出去。

微信支付的订单号有两种,一种是微信支付为每一笔生成的交易订单号,一种是商户也就是我们自己系统生成的订单号叫商户订单号,一个交易订单号会对应一个商户订单号。发起微信支付时商户订单号是一个必传的参数,通常传递的就是我们自己生成的订单号,微信支付订单查询接口支持通过交易单号或者商户订单号来查询订单。

这样子我们orderQuery()方法的两个必要参数就都准备好了,如果一切顺利,我们将会得到这个订单号的支付状态信息,但这里我们还需要对返回的格式做一些处理和对异常信息的捕获:

<?php


namespace app\api\service;

use app\lib\exception\pay\PayException;

// 注意这里的引用路径是以TP5框架入口文件所在目录算起的,即项目根目录\public\index.php
require_once "../extend/wx_pay/WxPay.Api.php";

class WxPay
{
    private $orderNo;
    private $config;


    public function __construct($orderNo)
    {
        $this->orderNo = $orderNo;
        $this->config = new WxPayConfig('wx');
    }

    public function getWxOrderStatus()
    {
       // 生成查询参数对象
        $inputObj = $this->generateOrderQuery();
        // 调用微信支付订单查询接口
        try {
            $payStatus = \WxPayApi::orderQuery($this->config, $inputObj);
            if ($payStatus['result_code'] === 'FAIL') {
                throw new PayException(['msg' => $payStatus['err_code_des']]);
            }
            $result = [
                'trade_state' => $payStatus['trade_state'],
                'trade_state_desc' => $payStatus['trade_state_desc'],
                'out_trade_no' => $payStatus['out_trade_no'],
                'transaction_id' => $payStatus['transaction_id'] ?? '',
                'is_subscribe' => $payStatus['is_subscribe'] ?? '',
                'total_fee' => $payStatus['total_fee'],
                'cash_fee' => $payStatus['cash_fee'] ?? '',
                'time_end' => $payStatus['time_end'] ?? '',
                'attach' => $payStatus['attach'] ?? '',
            ];
            return $result;
        } catch (\WxPayException $ex) {
            throw new PayException(['msg' => $ex->getMessage()]);
        }
    }

    /**生成微信支付订单查询参数对象*/
    protected function generateOrderQuery(){...}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

这里我们定义了一个$result数组,里面只定义了一些有用和不敏感的元素,在调用的外层我们使用一个try/catch包裹一下,这里同样是看了源码和看文档、测试发现会有异常情况,这里我们获取下具体的异常信息,然后以我们自定义异常类的格式抛出。

这里读者记得创建一下这个自定义异常类。另外由于本专栏是以文字为载体,在摸索微信支付SDK时的一些思路或者过程难以完全用文字的形式描述,写出来的内容可能会很绕,这也是为什么很多书籍都会选择直接告诉你就这么用而不是告诉你怎么知道是这么用的原因,这里还望读者理解。但首次接触一个陌生的SDK时,总的方式不过一个,就是看文档、看示例代码、伪代码测试。当然,如果我火了,XX网站邀请我做视频课程的话这个问题就可以完美解决。

到这里我们的订单支付状态查询的服务类就实现完毕了,我们来到控制层中调用一下,回到一开始的Order控制器类下的getOrderPayStatus()方法:

<?php


namespace app\api\controller\v1;

use app\api\model\Order as OrderModel;
use app\api\service\Order as OrderService;
use app\api\service\WxPay as WxPayService;
use app\api\model\DeliverRecord as DeliverRecordModel;
use app\lib\exception\order\OrderException;
use think\facade\Request;

class Order
{
    /**分页查询所有订单*/
    public function getOrders(){...}

    /**订单发货*/
    public function deliverGoods($id){...}

    /**分页查询订单发货记录*/
    public function getOrderDeliverRecord(){...}

    /**
     * @param $orderNo
     */
    public function getOrderPayStatus($orderNo)
    {
        $result = (new WxPayService($orderNo))->getWxOrderStatus();
        return $result;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

控制器方法定义好之后,接着来给这个控制器方法定义一条路由,打开route.php,在order路由分组下新增一条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        ............................
        ............................
        ............................
        ............................
        Route::group('order', function () {
            // 分页查询所有订单
            Route::get('', 'api/v1.Order/getOrders');
             // 订单发货
            Route::post('shipment/:id', 'api/v1.Order/deliverGoods');
            // 查询发货记录
            Route::get('shipment/record', 'api/v1.Order/getOrderDeliverRecord');
            // 查询订单支付状态
            Route::get('pay/:orderNo', 'api/v1.Order/getOrderPayStatus');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

路由定义完了之后打开Postman,按照路由信息新增并配置一个请求:

注意这里作者使用了公司线上业务里的一个真实有效的订单号,有微信支付商户的读者记得自行替换成自己真实业务中的订单号然后点击发送:

{
    "trade_state":"SUCCESS",
    "trade_state_desc": "支付成功",
    "out_trade_no": "201909210000000560",
    "transaction_id": "4200000407201909219556805597",
    "is_subscribe": "N",
    "total_fee": "40000",
    "cash_fee": "40000",
    "time_end": "20190921011753",
    "attach": []
}
1
2
3
4
5
6
7
8
9
10
11

没有报错,这里显示关于这笔订单的微信支付信息,这里显示已经是支付成功了,然后我们再来查询一笔已经退款过的,看看会是什么样的:

{
    "trade_state":"REFUND",
    "trade_state_desc": "订单发生过退款,退款详情请查询退款单",
    "out_trade_no": "201909100000016690",
    "transaction_id": "4200000408201909108584558917",
    "is_subscribe": "N",
    "total_fee": "42500",
    "cash_fee": "42500",
    "time_end": "20190910163847",
    "attach": []
}
1
2
3
4
5
6
7
8
9
10
11

这时候官方的开发文档就派上用场了,我们可以通过查阅官方的开发文档里关于订单查询接口返回字段的介绍来知道每个字段的含义。

这里显示该笔订单已经发生过退款,关于退款的详情需要调用另外的接口获取,关于退款详情查询的接口我们也会在后续的小节中学习,不过我相信其实读者这时候也可以自行实现了,到这里我们订单支付状态查询接口就已经实现完毕了,不过这里我们还需要稍微优化一下代码,打开我们服务层下面的WxPay服务类:


<?php


namespace app\api\service;

require_once "../extend/wx_pay/WxPay.Api.php";

use app\lib\exception\pay\PayException;

class WxPay
{
    private $orderNo;
    private $config;

    public function __construct($orderNo)
    {
        $this->orderNo = $orderNo;
        $this->config = new WxPayConfig('wx');
    }

    /**
     * 查询微信支付订单
     * @return array
     * @throws PayException
     */
    public function getWxOrderStatus()
    {
        // 生成查询参数对象
        $inputObj = $this->generateOrderQuery();
        // 调用微信支付订单查询接口
        try {
            $payStatus = \WxPayApi::orderQuery($this->config, $inputObj);
            ...........................
            ...........................
            ...........................
        } catch (\WxPayException $ex) {
            throw new PayException(['msg' => $ex->getMessage()]);
        }
    }

    /**生成微信支付订单查询参数对象*/
    protected function generateOrderQuery(){...}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

前面我们在编写WxPayConfig实现类的时候提到过,为了灵活性,我们让这个类要获取什么配置文件由外部调用者来决定,在WxPay服务类的构造方法中,我们在实例化WxPayConfig的时候传入了wx,这里不难理解,因为我们就是要获取这个配置文件,但是,假如在这个CMS项目中,我同时需要处理来自多个不同微信支付商户或者多个不同APPID的订单,虽然接口方法都一样,但是由于构造方法里面写死了就是获取wx配置文件,这个WayPay服务类就无法复用了,你就需要写多一个类文件,继承然后覆盖这个类的构造方法,这么做性价比显然不高;还有一种方法是在WxPay服务类中增加一个可以改变配置文件获取对象的方法,这里我们采用这种方式,在WayPay服务类中新增一个config()方法:


<?php


namespace app\api\service;

require_once "../extend/wx_pay/WxPay.Api.php";

use app\lib\exception\pay\PayException;

class WxPay
{
    private $orderNo;
    private $config;

    public function __construct($orderNo)
    {
        $this->orderNo = $orderNo;
        $this->config = new WxPayConfig('wx');
    }

    /**查询微信支付订单*/
    public function getWxOrderStatus(){...}

    /**生成微信支付订单查询参数对象*/
    protected function generateOrderQuery(){...}

    /**
     * 设置微信支付商户配置对象
     * @param $name 配置文件文件名
     * @return WxPay
     */
    public function config($name)
    {
        $this->config = new WxPayConfig($name);
        return $this;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

这里我们新增了一个config(),方法接收一个$name参数,方法内的实现就是对$this->config重新赋值,这里同时运用了一个设计技巧,就是最后我们直接返回了$this。类似$this->xxxx的语法我们用过很多,我们都知道$this是指向当前类的一个伪变量,这里直接就把$this返回了有什么目的呢?答案就在调用这个WxPay服务类的时候,让我们到控制器层中来实操一下:

<?php


namespace app\api\controller\v1;

use app\api\model\Order as OrderModel;
use app\api\service\Order as OrderService;
use app\api\service\WxPay as WxPayService;
use app\api\model\DeliverRecord as DeliverRecordModel;
use app\lib\exception\order\OrderException;
use think\facade\Request;

class Order
{
    .....................................................
    .....................................................
    .....................................................

    /**
     * @param $orderNo
     */
    public function getOrderPayStatus($orderNo)
    {
        $result = (new WxPayService($orderNo))->getWxOrderStatus();
        return $result;
    }

    /**
     * @param $orderNo
     */
    public function getSecondOrderPayStatus($orderNo)
    {
        $result = (new WxPayService($orderNo))->config('second_wx')
            ->getWxOrderStatus();
        return $result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

这里假设我们有一个新的接口getSecondOrderPayStatus(),这个接口同样是查询支付订单状态,区别是查询的商户是另外一个,那么我们就可以在调用服务层方法getWxOrderStatus()之前先调用config(),这样config方法就会根据我们的传参去获取对应的配置文件然后覆盖构造方法中的默认值,这种一路->调用的方式叫链式调用,这个实现的基础就在于我们前面在实现WxPay类下config()方法时最后直接返回的是$this,也就是WxPay服务类的实例,如果不是返回$this,那么你控制层这里的代码大概会是这样:

    /**
     * @param $orderNo
     */
    public function getSecondOrderPayStatus($orderNo)
    {
        // $result = (new WxPayService($orderNo))->config('second_wx')->getWxOrderStatus();
        $wxPayService = new WxPayService($orderNo);
        $wxPayService->config('second_wx');
        $result = $wxPayService->getWxOrderStatus();
        return $result;
    }
1
2
3
4
5
6
7
8
9
10
11

效果是一样的,但相比链式调用,这里的代码就显得比较臃肿了,如果一个类里面有操作成员变量的操作,特别是当数量比较多的时候,更优雅的方式是让操作方法返回类的实例对象,这样外部就可以实现链式调用让代码看起来更加简洁。

扩展知识
在第一章《关于教程——前后端分离》中我们对软件分层的初衷和设计模式做了初步的了解,控制层和模型层在目前的学习进度中我们也反复利用了很多次,我们还自己派生了验证层和服务层,层的作用本质上都是对代码、实现功能的归纳来实现结构清晰和功能解耦。学习到这里,我们不妨可以回顾一下之前的代码,看看是不是这样子。作者在初次接触分层这个概念的时候,一直处在一种跟着用但不知道有啥用、为什么这么用的状态,我相信很多读者学到这里,也有这种疑惑,你让我分层我分了,但是还是很懵。首先有一点要明确的是,即便你不分层,你依然可以开发一套可以上线的系统,但是,你这套系统绝对会是难以维护和扩展的系统。因为不分层的话,你的每一个类文件里要实现的功能就非常多,每个功能要实现的逻辑也会变得很复杂而且会存在重复实现某个功能的情况(比如不同类文件里都存在查询同一个数据的方法)。这种一个类同时承担了多个职责,同时又大量依赖其他类的情况,我们称之为低内聚、高耦合,而与之相反的就是最佳实践——高内聚、低耦合。这两个词相信大家都听过或者看过但是不知道是什么,标准的释义读者可自行搜索引擎,反正脱离了代码你也看不懂。这里我们以前面实现了的代码来尝试理解,比如说服务层下WxPay类。首先服务层的定位是提供某种特定的服务,服务层下的WxPay类是提供微信支付相关接口的调用服务,我们想实现的就是在其他地方只要调用这个WxPay类里的某个方法就能得到某个具体服务的结果,这个不难理解,但是实现起来就会有各种问题,举个例子,我们在前面学习中一开始只实现查询了一家微信支付商户产生的订单状态功能,如果你自己只有一家商户那很好办,照着代码抄就完事,那万一如果你同时有2家甚至3家往上的商户呢?又或者多个应用绑定同一个微信支付商户(这时候APPID会不一致),现有的代码是不是无法实现了?像这种情况,解决办法只有一个,那就是尽量把实现类的职责做得足够单一,单一到实现类只做一件事,就是负责实现功能,至于实现功能所需要的参数,都交给调用方来提供,想到这里,我们第一个想到的办法就是让服务类接收一堆的变量,这个方式早期是没问题的,但是后期作者就遇到了一个问题,在其他地方复用这个方法的时候,经常需要查看变量的传递顺序和数量,某一天我重构了这个服务类,参数的数量和顺序变了,调用的地方全部得修改。我们解决了参数变化的问题但是没有解决参数传递和接收的问题,这时候我们就需要在这个基础上再思考怎么解耦,我们把参数传递和接收这件事改成类成员属性赋值的形式来实现(面向对象),比如WxPay类下实现的config()方法,看起来是改变变量,实质也是改变了运行逻辑,改变了要读取的配置对象。我们在外部调用时只需要明确一个订单号,至于其他参数,通过WxPay暴露出去的方法来设置,WxPay的内部只管调用,自己的成员属性当前是什么不关心。这里读者可能会觉得,这里的WxPay的业务逻辑不复杂,还好理解,如果是复杂的,怎么办?比如说一个类里面同时依赖了好几个类,还有一堆业务逻辑判断。这里答案就是就是一层一层往上抽象,不停的把公共的逻辑提取出来成一个类,外部只和最“底层”的实现类打交道,这个实现类肯定有一个特点,就是高内聚、低耦合。说了那么多,这和分层又有啥关系呢?答案就是层是由一个个这种高内聚、低耦合的类组成的,而层与层之间又是一种高内聚、低耦合的关系,这样一路下来,你的整个系统就会变得很容易定位问题,也很容易扩展,即可维护、可扩展。如果读者还是理解不了以上内容,那么可以暂时先放弃理解这些概念和思想,你只要不停的追求代码可复用性即可,在这个过程中你就会不知不觉地对代码层层提炼。还有的读者可能会说道理我都懂了,可还是不会写,那么这里作者介绍一个终极大招——遇事不决,重构即可。没有人能一下子写出完美的代码,而事实上完美的代码也不存在,再好的代码也总能挑出毛病,我们所遵循的设计模式也都是前人通过不断重构和实践总结出来的,包括我们专栏中的代码,后面我们也会因为一些契机对现有的代码进行有目的性的重构。

订单退款

在真实项目中,只要涉及到支付,就必然会有退款的业务情况,一般正常情况下的退款操作都是在前端应用提供一个功能入口,由用户自己来发起,但是这个操作有一个必要的前提就是订单状态必须是已支付的状态。从上一小节的学习内容中我们可以了解到,在某些极端情况下,这个订单状态是有可能会出现问题的,那这时候如果用户想退款就没办法操作了;另一种情况是可能因为一些业务上的问题或者投诉,商家需要主动给用户退款保平安,这时候我们就需要实现能让管理人员从CMS发起一个针对某笔订单的退款操作。要实现退款我们同样是利用微信支付提供给我们的SDK,读者在看到这里的时候不妨可以按照前面我们实现订单支付状态查询时的思路,试着先去把对应接口方法找出来,如能自己先动手实现下则更佳!

首先我们同样需要在控制层中定义个方法,在控制层下Order控制器类下新增一个refund()方法:

<?php


namespace app\api\controller\v1;

use app\api\model\Order as OrderModel;
use app\api\service\Order as OrderService;
use app\api\model\DeliverRecord as DeliverRecordModel;
use app\lib\exception\order\OrderException;
use think\facade\Request;

class Order
{
    /**分页查询所有订单*/
    public function getOrders(){...}
    /**订单发货*/
    public function deliverGoods($id){...}
    /**订单支付状态查询*/
    public function getOrderPayStatus($orderNo){...}

    /**
     * 订单退款
     */
    public function refund()
    {
        $params = Request::post();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

接着我们就需要在服务层下实现一个退款服务,打开服务层下的WxPay服务类,新增一个refund方法:


<?php


namespace app\api\service;

require_once "../extend/wx_pay/WxPay.Api.php";

use app\lib\exception\pay\PayException;

class WxPay
{
    private $orderNo;
    private $config;

    public function __construct($orderNo)
    {
        $this->orderNo = $orderNo;
        $this->config = new WxPayConfig('wx');
    }

    /**查询微信支付订单*/
    public function getWxOrderStatus(){...}

    public function refund($refundFee)
    {
        $refundRes = \WxPayApi::refund($this->config, $inputObject);
    }

    .......................................
    .......................................

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

有了前面的踩坑经历,按照套路,我们宛如一名老司机,轻松就可以发现到WxPayApi下有个refound方法用于实现微信支付退款,这个方法的调用方式和前面的订单查询也是一模一样的,只不过参数对象的内容不一样,我们只要解决参数对象生成的问题即可。首先是$config参数,这里不需要做什么额外工作,因为前面我们已经封装好了,在实例化这个服务类时就会自动赋值好给成员变量,这里直接调用成员变量即可。关键的地方在于$inputObject对象,我们要退款,肯定需要告诉微信支付,退哪笔订单,退多少这两个基本问题,而这两个问题的答案自然都是要封装在$inputObject对象中了。和前面订单状态查询一样,我们需要利用SDK内置的方法来封装并生成这个参数对象,我们在当前的服务类中再新增一个generateRefundObject()方法:


<?php


namespace app\api\service;

require_once "../extend/wx_pay/WxPay.Api.php";

use app\lib\exception\pay\PayException;

class WxPay
{
    private $orderNo;
    private $config;

    public function __construct($orderNo)
    {
        $this->orderNo = $orderNo;
        $this->config = new WxPayConfig('wx');
    }

    /**查询微信支付订单*/
    public function getWxOrderStatus(){...}

    public function refund($refundFee){...}

    /**
     * 生成微信支付退款提交对象
     * @param $totalFee 订单总金额
     * @param $refundFee 退款金额
     */
    protected function generateRefundObject($totalFee, $refundFee)
    {
        $inputObject = new \WxPayRefund();
        // 设置要退款的商户订单号
        $inputObject->SetOut_trade_no($this->orderNo);
        // 设置退款订单号
        // 一笔微信支付订单是可以分开多次退款的,所以需要为每次退款都生成一个订单号作为退款订单号
        // 同一个退款订单号发起多次退款,不会进行多次退款。
        // 这里调用一个我们自己实现的方法用于生成退款订单号
        $inputObject->SetOut_refund_no($this->makeOrderNo());
        // 设置订单总金额,需与原支付订单总金额一致
        // 微信支付接口接收的金额单位是分为单位,所以我们要*100把元化成分
        $inputObject->SetTotal_fee($totalFee * 100);
        // 设置本次退款金额,单位为分
        $inputObject->SetRefund_fee($refundFee * 100);
        // 设置操作人信息,默认传微信支付商户的merchanId即可
        $inputObject->SetOp_user_id($this->config->GetMerchantId());
        // 返回封装好的对象
        return $inputObject;
    }

    /**
     * 生成订单号
     * @return string
     */
    public function makeOrderNo()
    {
        $yCode = array('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J');
        $orderSn =
            $yCode[intval(date('Y')) - 2017] . strtoupper(dechex(date('m'))) . date(
                'd') . substr(time(), -5) . substr(microtime(), 2, 5) . sprintf(
                '%02d', rand(0, 99));
        return $orderSn;
    }


    .......................................
    .......................................

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

generateRefundObject()方法接收两个参数,方法内调用SDK中的WxPayRefund类来生成退款接口所需的对象参数,方法体内平平无奇,索然无味,如果问怎么知道要调那些方法的,看文档,哪些是必传的参数,就调用哪个参数的设置方法。方法定义好之后,我们回到上面的refund()方法中来调用一下:


<?php


namespace app\api\service;

require_once "../extend/wx_pay/WxPay.Api.php";

use app\lib\exception\pay\PayException;

class WxPay
{
    private $orderNo;
    private $config;

    public function __construct($orderNo)
    {
        $this->orderNo = $orderNo;
        $this->config = new WxPayConfig('wx');
    }

    /**查询微信支付订单*/
    public function getWxOrderStatus(){...}

    public function refund($refundFee)
    {
        // 数据库中查询订单,因为只需要知道订单的订单总金额字段,在查询时指定了要列出的字段,节省性能
        $order = OrderModel::field('total_price')->where('order_no', $this->orderNo)
            ->find();
        // total_price通过查询数据库订单记录获得,refundFee由外部或者前端传递
        $inputObject = $this->generateRefundObject($order->total_price, $refundFee);
        $refundRes = \WxPayApi::refund($this->config, $inputObject);
    }

    /**生成微信支付退款提交对象*/
    protected function generateRefundObject($totalFee, $refundFee){...}

    .......................................
    .......................................

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

这里我们先通过数据库查询订单记录,拿到订单总金额,再加上由前端传递过来的退款金额一并传递给我们刚刚定义的generateRefundObject() 方法,这样就可以拿到封装好的对象参数了,然后把这个对象参数传递给\WxPayApi::refund方法,如果一切顺利,我们就可以对指定订单进行退款了,当然这里我们依然需要对这里的逻辑进行异常捕获和格式化输出:


<?php


namespace app\api\service;

require_once "../extend/wx_pay/WxPay.Api.php";

use app\lib\exception\pay\PayException;

class WxPay
{
    private $orderNo;
    private $config;

    public function __construct($orderNo)
    {
        $this->orderNo = $orderNo;
        $this->config = new WxPayConfig('wx');
    }

    /**查询微信支付订单*/
    public function getWxOrderStatus(){...}

    public function refund($refundFee)
    {
        try {
            // 数据库中查询订单,因为只需要知道订单的订单总金额字段,在查询时指定了要列出的字段,节省性能
            $order = OrderModel::field('total_price')->where('order_no', $this->orderNo)
                ->find();
            // total_price通过查询数据库订单记录获得,refundFee由外部或者前端传递
            $inputObject = $this->generateRefundObject($order->total_price, $refundFee);
            $refundRes = \WxPayApi::refund($this->config, $inputObject);

            if ($refundRes['return_code'] === 'FAIL') {
                throw new PayException(['msg' => $refundRes['return_msg']]);
            }

            if ($refundRes['result_code'] === 'FAIL') {
                throw new PayException(['msg' => $refundRes['err_code_des']]);
            }
        } catch (\WxPayException $ex) {
            throw new PayException(['msg' => $ex->getMessage()]);
        }

        // 这里提取一些关键的字段内容,可根据自己业务实际情况调整
        $result = [
            'result_code' => $refundRes['return_code'],
            'out_trade_no' => $refundRes['out_trade_no'],
            'out_refund_no' => $refundRes['out_refund_no'],
            'total_fee' => $refundRes['total_fee'],
            'refund_fee' => $refundRes['refund_fee'],
        ];

        return $result;
    }

    .......................................
    .......................................

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

到这里,我们服务层下的退款服务就定义好了,但是要进行微信支付订单的退款,我们还需要配置微信支付的证书,证书可以从微信支付商户的后台下载得到。有了证书之后,我们就需要添加一些配置项,打开根目录下config目录下的wx.php配置文件,添加两个元素:

<?php

return [
    'app_id' => '',
    'merchant_id' => '',
    'sign_type' => 'MD5',
    'key' => '',
    // 证书路径,一定要是绝对路径,如果服务器是Windows,这个路径分隔符注意要写/不是\
    'cert_path' => 'd:/cert/apiclient_cert.pem',
    'key_path' => 'd:/cert/apiclient_key.pem'
];
1
2
3
4
5
6
7
8
9
10
11

接着修改一下服务层下的WxPayConfig类,找到GetSSLCertPath()方法:

<?php

namespace app\api\service;

use think\facade\Config;

require_once "../extend/wx_pay/WxPay.Config.Interface.php";

class WxPayConfig extends \WxPayConfigInterface
{
    .............................................
    .............................................
    .............................................

     /**
     * TODO:设置商户证书路径
     * 证书路径,注意应该填写绝对路径(仅退款、撤销订单时需要,可登录商户平台下载,
     * API证书下载地址:https://pay.weixin.qq.com/index.php/account/api_cert,下载之前需要安装商户操作证书)
     * 注意:
     * 1.证书文件不能放在web服务器虚拟目录,应放在有访问权限控制的目录中,防止被他人下载;
     * 2.建议将证书文件名改为复杂且不容易猜测的文件名;
     * 3.商户服务器要做好病毒和木马防护工作,不被非法侵入者窃取证书文件。
     * @var path
     */
    public function GetSSLCertPath(&$sslCertPath, &$sslKeyPath)
    {
        $sslCertPath = $this->wxConfig['cert_path'];
        $sslKeyPath = $this->wxConfig['key_path'];
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

这样我们就配置好微信支付的证书了,接下来让我们回到控制层中调用一下:

<?php


namespace app\api\controller\v1;

use app\api\model\Order as OrderModel;
use app\api\service\Order as OrderService;
use app\api\model\DeliverRecord as DeliverRecordModel;
use app\lib\exception\order\OrderException;
use think\facade\Request;

class Order
{
    /**分页查询所有订单*/
    public function getOrders(){...}
    /**订单发货*/
    public function deliverGoods($id){...}
    /**订单支付状态查询*/
    public function getOrderPayStatus($orderNo){...}

    /**
     * 订单退款
     * @auth('订单退款','财务管理')
     * @params('order_no','订单号','require')
     * @params('refund_fee','退款金额','require|float|>:0')
     */
    public function refund()
    {
        $params = Request::post();
        $result = (new WxPay($params['order_no']))->refund($params['refund_fee']);

        Hook::listen('logger', "操作订单{$params['order_no']}退款,退款金额{$params['refund_fee']}");
        return $result;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

这里我们调用了服务层下WxPay服务类中的refund()方法,并把一些必要的参数传递进去。同时给接口添加了权限控制,确保不是随便哪个人都可以对订单进行退款。还有别忘了添加相应的注解参数验证,最后在返回结果之前我们记录一下本次请求的行为日志,这里我们使用了{}来包裹变量来实现字符串拼接,相比用.连接符的方式可以看起来更加简洁和方便不易出错,特别是在变量比较多的时候。控制器方法定义好之后,接着来给这个控制器方法定义一条路由,打开route.php,在order路由分组下新增一条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        ............................
        ............................
        ............................
        ............................
        Route::group('order', function () {
            // 分页查询所有订单
            Route::get('', 'api/v1.Order/getOrders');
             // 订单发货
            Route::post('shipment/:id', 'api/v1.Order/deliverGoods');
            // 查询发货记录
            Route::get('shipment/record', 'api/v1.Order/getOrderDeliverRecord');
            // 查询订单支付状态
            Route::get('pay/:orderNo', 'api/v1.Order/getOrderPayStatus');
            // 订单退款
            Route::post('pay/refund','api/v1.Order/refund');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

路由定义完了之后打开Postman,按照路由信息新增并配置一个请求:

注意,这里作者同样使用了真实支付数据作为演示,读者自行根据实际情况进行参数值调整。这笔订单的总金额是2块钱,现在我要退了这2块钱,点击发送:

{
    "result_code": "SUCCESS",
    "out_trade_no": "201910020000000150",
    "out_refund_no": "CA02088174509462",
    "total_fee": "200",
    "refund_fee": "200"
}
1
2
3
4
5
6
7

这里提示我们退款成功了,然后你的微信就会接收到一条服务通知:

然后我们可以尝试一下重复提交:

{
    "msg": "订单已全额退款",
    "error_code": 70000,
    "request_url": "POST /v1/order/pay/refund"
}
1
2
3
4
5

肯定是不会成功的嘛,这里微信支付退款接口异常的信息通过我们自定义异常类抛出了,说明我们退款功能的实现逻辑是可以正常工作的。

退款详情查询

退款详情的查询在真实业务场景中也是经常会使用到的,例如在前面订单支付状态查询小节最后的测试过程中,我们尝试查了一笔已经退款的订单,微信支付接口提示我们该笔订单发生过退款,要查询退款详情接口才能看到详情;另外一种情况是用户或者CMS管理员对订单执行了退款操作,但是退款迟迟没有到账,这时候我们就很有必要了解下这笔订单退款的详情了。要实现退款详情的查询同样是利用微信支付的SDK,和订单支付状态查询的逻辑基本一个样,就是方法名换了,让我们来动手实现一下,首先在控制层下的Order控制器类新增一个refundQuery()方法:

<?php


namespace app\api\controller\v1;

use app\api\model\Order as OrderModel;
use app\api\service\Order as OrderService;
use app\api\model\DeliverRecord as DeliverRecordModel;
use app\lib\exception\order\OrderException;
use think\facade\Request;

class Order
{
    /**分页查询所有订单*/
    public function getOrders(){...}
    /**订单发货*/
    public function deliverGoods($id){...}
    /**订单支付状态查询*/
    public function getOrderPayStatus($orderNo){...}
    /**订单退款*/
    public function refund(){...}

    /**
     * 订单退款查询
     * @param $orderNo
     * @return \成功时返回,其他抛异常
     */
    public function refundQuery($orderNo)
    {
        
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

接着我们来服务层下实现一个退款详情查询服务,在WxPay服务类下新增一个refundQuery()方法:


<?php


namespace app\api\service;

require_once "../extend/wx_pay/WxPay.Api.php";

use app\lib\exception\pay\PayException;

class WxPay
{
    private $orderNo;
    private $config;

    public function __construct($orderNo)
    {
        $this->orderNo = $orderNo;
        $this->config = new WxPayConfig('wx');
    }

    /**查询微信支付订单*/
    public function getWxOrderStatus(){...}
    /**订单退款*/
    public function refund($refundFee){...}
    
    /**
     * 退款详情查询
     */
    public function refundQuery()
    {
        try {
            $inputObject = $this->generateRefundQueryObject();
            $result = \WxPayApi::refundQuery($this->config, $inputObject);

            if ($result['return_code'] === 'FAIL') {
                throw new PayException(['msg' => $result['return_msg']]);
            }

            if ($result['result_code'] === 'FAIL') {
                throw new PayException(['msg' => $result['err_code_des']]);
            }
        } catch (\WxPayException $ex) {
            throw new PayException(['msg' => $ex->getMessage()]);
        }
        return $result;
    }

    /**
     * 生成微信支付退款详情查询参数对象
     */
    protected function generateRefundQueryObject()
    {
        $inputObject = new \WxPayRefundQuery();
        $inputObject->SetOut_trade_no($this->orderNo);
        return $inputObject;
    }

    .......................................
    .......................................

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

退款详情的查询实现起来非常简单,和订单支付状态查询是一样的逻辑,就是调用的接口方法和参数封装类不一样而已,这里就不再做过多的介绍了。服务层的方法定义完毕之后,回到控制层中调用一下:

<?php


namespace app\api\controller\v1;

use app\api\model\Order as OrderModel;
use app\api\service\Order as OrderService;
use app\api\model\DeliverRecord as DeliverRecordModel;
use app\lib\exception\order\OrderException;
use think\facade\Request;

class Order
{
    /**分页查询所有订单*/
    public function getOrders(){...}
    /**订单发货*/
    public function deliverGoods($id){...}
    /**订单支付状态查询*/
    public function getOrderPayStatus($orderNo){...}
    /**订单退款*/
    public function refund(){...}

    /**
     * 订单退款查询
     * @param $orderNo
     */
    public function refundQuery($orderNo)
    {
        $result = (new WxPay($orderNo))->refundQuery();
        return $result;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

控制器方法定义好之后,接着来给这个控制器方法定义一条路由,打开route.php,在order路由分组下新增一条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        ............................
        ............................
        ............................
        ............................
        Route::group('order', function () {
            // 分页查询所有订单
            Route::get('', 'api/v1.Order/getOrders');
             // 订单发货
            Route::post('shipment/:id', 'api/v1.Order/deliverGoods');
            // 查询发货记录
            Route::get('shipment/record', 'api/v1.Order/getOrderDeliverRecord');
            // 查询订单支付状态
            Route::get('pay/:orderNo', 'api/v1.Order/getOrderPayStatus');
            // 订单退款
            Route::post('pay/refund','api/v1.Order/refund');
            // 查询退款详情
            Route::get('pay/refund/:orderNo','api/v1.Order/refundQuery');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

路由定义完了之后打开Postman,按照路由信息新增并配置一个请求:

这里我们来查询一下上一小节操作了退款的订单,点击发送:

{
    "appid": "qinchenhaoshuai",
    "cash_fee": "200",
    "mch_id": "5201314",
    "nonce_str": "vuqri9Jm8zmcEJhy",
    "out_refund_no_0": "CA02088174509462",
    "out_trade_no": "201910020000000150",
    "refund_account_0": "REFUND_SOURCE_UNSETTLED_FUNDS",
    "refund_channel_0": "ORIGINAL",
    "refund_count": "1",
    "refund_fee": "200",
    "refund_fee_0": "200",
    "refund_id_0": "50000101962019100212657206649",
    "refund_recv_accout_0": "支付用户的零钱",
    "refund_status_0": "SUCCESS",
    "refund_success_time_0": "2019-10-02 17:34:52",
    "result_code": "SUCCESS",
    "return_code": "SUCCESS",
    "return_msg": "OK",
    "sign": "14F31328C7559BE2919B51DFA7740617",
    "total_fee": "200",
    "transaction_id": "4200000417201910024702060209"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

可以看到这里就显示出了关于订单退款的详情,这里字段内容很多,读者可以结合官方开发文档里API的介绍来理解。前面我们提到过,一笔订单是可以分多次来退款的,那有多次退款的订单,退款详情查询会返回什么呢?这里作者尝试查询一笔操作了两次退款的订单,结果如下:

{
    "appid": "qinchenhaoshuai",
    "cash_fee": "200",
    "mch_id": "5201314",
    "nonce_str": "xy6oyc6V2XZ3Qgvj",
    "out_refund_no_0": "CA02196519475582",
    "out_refund_no_1": "CA02196456198669",
    "out_trade_no": "201910020000000600",
    "refund_account_0": "REFUND_SOURCE_UNSETTLED_FUNDS",
    "refund_account_1": "REFUND_SOURCE_UNSETTLED_FUNDS",
    "refund_channel_0": "ORIGINAL",
    "refund_channel_1": "ORIGINAL",
    "refund_count": "2",
    "refund_fee": "200",
    "refund_fee_0": "100",
    "refund_fee_1": "100",
    "refund_id_0": "50000702222019100212109957367",
    "refund_id_1": "50000702222019100212637033495",
    "refund_recv_accout_0": "支付用户的零钱",
    "refund_recv_accout_1": "支付用户的零钱",
    "refund_status_0": "SUCCESS",
    "refund_status_1": "SUCCESS",
    "refund_success_time_0": "2019-10-02 20:35:26",
    "refund_success_time_1": "2019-10-02 20:35:21",
    "result_code": "SUCCESS",
    "return_code": "SUCCESS",
    "return_msg": "OK",
    "sign": "C5A7D4A6BB82DDF85535634E76AB4299",
    "total_fee": "200",
    "transaction_id": "4200000406201910022991219183"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

这里读者仔细对比下可以发现有些差别之处,如果你需要对退款详情查询返回的内容做入库或者前端页面展示就要留意这种差异。

章节回顾

本章节我们学习并实现了部分微信支付接口的开发,作者选取了3个真实业务场景下高频使用的微信支付接口作为讲解,让读者们上手微信支付接口相关的开发工作,相信各位读者在学习完本章节后就能根据自身业务情况完成定制开发了,至于章节中没有提到的其他接口,也可以通过自行阅读文档和SDK来实现。这里要提醒读者的是,不管是微信还是支付宝或者其他支付平台,单纯地实现接口对接是很简单的,就好比我们前面学习的过程一样;真正的难点是在于与自身系统业务做集成(实际上做任何第三方接口对接都是这样)。比如订单退款,订单退款后,有退货和不退货之分,甚至再复杂点,换货,这一部分才是业务实现的难点。有读者可能会问,那作者为什么不带着大家走一个?原因是这样的,就拿订单退款这件事来说,假设退款后要退货,那这个业务操作读者们觉得涉及几个流程步骤?每个流程步骤所需的知识点是不是都已经被专栏目前的内容所覆盖了呢?作者觉得是已经覆盖到了的,就差你思考怎么把他们串联起来。这也是作者在学习编程过程中的一个感受,跟在学英语一样,我认识26个字母,但是他们挑几个放一起我就不会了,回到编程这件事上来就是说到底还是写得太少。所以呢,即便作者这里给你来一套,变个需求你可能又不会了,我写了等于白写,我更推荐的是你自己动手去实现,发现问题然后解决问题,不断迭代,这样你才能做到对各个知识点的融会贯通。

最后更新: 2/12/2020, 6:04:59 PM
0/140
评论
0
暂无评论
  • 上一页
  • 首页
  • 1
  • 尾页
  • 下一页
  • 总共1页