lizhen_gitee 2 years ago
parent
commit
c924b4398d
96 changed files with 8498 additions and 0 deletions
  1. 1 0
      addons/epay/.addonrc
  2. 69 0
      addons/epay/Epay.php
  3. 0 0
      addons/epay/certs/alipayCertPublicKey.crt
  4. 0 0
      addons/epay/certs/alipayRootCert.crt
  5. 0 0
      addons/epay/certs/apiclient_cert.pem
  6. 0 0
      addons/epay/certs/apiclient_key.pem
  7. 0 0
      addons/epay/certs/appCertPublicKey.crt
  8. 394 0
      addons/epay/config.html
  9. 55 0
      addons/epay/config.php
  10. 218 0
      addons/epay/controller/Api.php
  11. 113 0
      addons/epay/controller/Index.php
  12. 10 0
      addons/epay/info.ini
  13. 18 0
      addons/epay/library/Collection.php
  14. 16 0
      addons/epay/library/OrderException.php
  15. 58 0
      addons/epay/library/RedirectResponse.php
  16. 26 0
      addons/epay/library/Response.php
  17. 310 0
      addons/epay/library/Service.php
  18. 110 0
      addons/epay/library/Wechat.php
  19. 83 0
      addons/epay/library/Yansongda/Pay/Contracts/GatewayApplicationInterface.php
  20. 20 0
      addons/epay/library/Yansongda/Pay/Contracts/GatewayInterface.php
  21. 98 0
      addons/epay/library/Yansongda/Pay/Events.php
  22. 31 0
      addons/epay/library/Yansongda/Pay/Events/ApiRequested.php
  23. 31 0
      addons/epay/library/Yansongda/Pay/Events/ApiRequesting.php
  24. 40 0
      addons/epay/library/Yansongda/Pay/Events/Event.php
  25. 33 0
      addons/epay/library/Yansongda/Pay/Events/MethodCalled.php
  26. 31 0
      addons/epay/library/Yansongda/Pay/Events/PayStarted.php
  27. 23 0
      addons/epay/library/Yansongda/Pay/Events/PayStarting.php
  28. 25 0
      addons/epay/library/Yansongda/Pay/Events/RequestReceived.php
  29. 25 0
      addons/epay/library/Yansongda/Pay/Events/SignFailed.php
  30. 19 0
      addons/epay/library/Yansongda/Pay/Exceptions/BusinessException.php
  31. 44 0
      addons/epay/library/Yansongda/Pay/Exceptions/Exception.php
  32. 20 0
      addons/epay/library/Yansongda/Pay/Exceptions/GatewayException.php
  33. 19 0
      addons/epay/library/Yansongda/Pay/Exceptions/InvalidArgumentException.php
  34. 19 0
      addons/epay/library/Yansongda/Pay/Exceptions/InvalidConfigException.php
  35. 19 0
      addons/epay/library/Yansongda/Pay/Exceptions/InvalidGatewayException.php
  36. 19 0
      addons/epay/library/Yansongda/Pay/Exceptions/InvalidSignException.php
  37. 422 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay.php
  38. 38 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/AppGateway.php
  39. 40 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/Gateway.php
  40. 46 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/MiniGateway.php
  41. 47 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/PosGateway.php
  42. 21 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/RefundGateway.php
  43. 41 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/ScanGateway.php
  44. 452 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/Support.php
  45. 49 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/TransferGateway.php
  46. 26 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/WapGateway.php
  47. 104 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/WebGateway.php
  48. 366 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat.php
  49. 62 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/AppGateway.php
  50. 88 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/Gateway.php
  51. 57 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/GroupRedpackGateway.php
  52. 35 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/MiniappGateway.php
  53. 59 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/MpGateway.php
  54. 44 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/PosGateway.php
  55. 61 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/RedpackGateway.php
  56. 50 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/RefundGateway.php
  57. 44 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/ScanGateway.php
  58. 453 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/Support.php
  59. 80 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/TransferGateway.php
  60. 47 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/WapGateway.php
  61. 86 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/WebGateway.php
  62. 20 0
      addons/epay/library/Yansongda/Pay/LICENSE
  63. 114 0
      addons/epay/library/Yansongda/Pay/Listeners/KernelLogSubscriber.php
  64. 49 0
      addons/epay/library/Yansongda/Pay/Log.php
  65. 131 0
      addons/epay/library/Yansongda/Pay/Pay.php
  66. 605 0
      addons/epay/library/Yansongda/Supports/Arr.php
  67. 363 0
      addons/epay/library/Yansongda/Supports/Collection.php
  68. 7 0
      addons/epay/library/Yansongda/Supports/Config.php
  69. 20 0
      addons/epay/library/Yansongda/Supports/LICENSE
  70. 91 0
      addons/epay/library/Yansongda/Supports/Log.php
  71. 240 0
      addons/epay/library/Yansongda/Supports/Logger.php
  72. 36 0
      addons/epay/library/Yansongda/Supports/Logger/StdoutHandler.php
  73. 570 0
      addons/epay/library/Yansongda/Supports/Str.php
  74. 142 0
      addons/epay/library/Yansongda/Supports/Traits/Accessable.php
  75. 32 0
      addons/epay/library/Yansongda/Supports/Traits/Arrayable.php
  76. 229 0
      addons/epay/library/Yansongda/Supports/Traits/HasHttpRequest.php
  77. 85 0
      addons/epay/library/Yansongda/Supports/Traits/Serializable.php
  78. 147 0
      addons/epay/library/Yansongda/Supports/Traits/ShouldThrottle.php
  79. 47 0
      addons/epay/view/api/alipay.html
  80. 90 0
      addons/epay/view/api/wechat.html
  81. 17 0
      addons/epay/view/index/index.html
  82. 104 0
      addons/epay/view/layout/default.html
  83. 39 0
      application/admin/controller/Epay.php
  84. 178 0
      public/assets/addons/epay/css/common.css
  85. BIN
      public/assets/addons/epay/images/alipay.png
  86. BIN
      public/assets/addons/epay/images/expired.png
  87. BIN
      public/assets/addons/epay/images/logo-alipay.png
  88. BIN
      public/assets/addons/epay/images/logo-wechat.png
  89. BIN
      public/assets/addons/epay/images/paid.png
  90. BIN
      public/assets/addons/epay/images/scan.png
  91. BIN
      public/assets/addons/epay/images/screenshot-alipay.png
  92. BIN
      public/assets/addons/epay/images/screenshot-wechat.png
  93. BIN
      public/assets/addons/epay/images/wechat.png
  94. 69 0
      public/assets/addons/epay/js/common.js
  95. 34 0
      public/assets/addons/epay/js/jquery.qrcode.min.js
  96. 224 0
      public/assets/addons/epay/less/common.less

+ 1 - 0
addons/epay/.addonrc

@@ -0,0 +1 @@
+{"files":["application\\admin\\controller\\Epay.php","public\\assets\\addons\\epay\\css\\common.css","public\\assets\\addons\\epay\\images\\alipay.png","public\\assets\\addons\\epay\\images\\expired.png","public\\assets\\addons\\epay\\images\\logo-alipay.png","public\\assets\\addons\\epay\\images\\logo-wechat.png","public\\assets\\addons\\epay\\images\\paid.png","public\\assets\\addons\\epay\\images\\scan.png","public\\assets\\addons\\epay\\images\\screenshot-alipay.png","public\\assets\\addons\\epay\\images\\screenshot-wechat.png","public\\assets\\addons\\epay\\images\\wechat.png","public\\assets\\addons\\epay\\js\\common.js","public\\assets\\addons\\epay\\js\\jquery.qrcode.min.js","public\\assets\\addons\\epay\\less\\common.less"],"license":"regular","licenseto":"38239","licensekey":"pGLwfFzONEy9kV28 Egnivn0pChkfdKTyOnQIfA==","domains":["shipinjiaoyu.com"],"licensecodes":[],"validations":["6adce6cd3c031d9e8f09bac54d9b05f2"]}

+ 69 - 0
addons/epay/Epay.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace addons\epay;
+
+use think\Addons;
+use think\Config;
+use think\Loader;
+
+/**
+ * 微信支付宝整合插件
+ */
+class Epay extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+
+        return true;
+    }
+
+    /**
+     * 插件启用方法
+     * @return bool
+     */
+    public function enable()
+    {
+
+        return true;
+    }
+
+    /**
+     * 插件禁用方法
+     * @return bool
+     */
+    public function disable()
+    {
+
+        return true;
+    }
+
+    /**
+     * 添加命名空间
+     */
+    public function appInit()
+    {
+        //添加命名空间
+        if (!class_exists('\Yansongda\Pay\Pay')) {
+            Loader::addNamespace('Yansongda\Pay', ADDON_PATH . 'epay' . DS . 'library' . DS . 'Yansongda' . DS . 'Pay' . DS);
+        }
+        if (!class_exists('\Yansongda\Supports\Logger')) {
+            Loader::addNamespace('Yansongda\Supports', ADDON_PATH . 'epay' . DS . 'library' . DS . 'Yansongda' . DS . 'Supports' . DS);
+        }
+    }
+
+}

+ 0 - 0
addons/epay/certs/alipayCertPublicKey.crt


+ 0 - 0
addons/epay/certs/alipayRootCert.crt


+ 0 - 0
addons/epay/certs/apiclient_cert.pem


+ 0 - 0
addons/epay/certs/apiclient_key.pem


+ 0 - 0
addons/epay/certs/appCertPublicKey.crt


+ 394 - 0
addons/epay/config.html

@@ -0,0 +1,394 @@
+<form id="config-form" class="edit-form form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+    <div class="panel panel-default panel-intro">
+        <div class="panel-heading">
+            <ul class="nav nav-tabs nav-group">
+                <li class="active"><a href="#wechat" data-toggle="tab">微信支付</a></li>
+                <li><a href="#alipay" data-toggle="tab">支付宝</a></li>
+            </ul>
+        </div>
+
+        <div class="panel-body">
+            <div id="myTabContent" class="tab-content">
+                {foreach $addon.config as $item}
+                {if $item.name=='wechat'}
+                <div class="tab-pane fade active in" id="wechat">
+                    <table class="table table-striped table-config">
+                        <tbody>
+                        <tr>
+                            <td width="15%">APP的app_id</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <input type="text" name="row[wechat][appid]" value="{$item.value.appid|default=''}" class="form-control" data-rule="" data-tip="APP应用中支付时使用"/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>公众号的app_id</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <input type="text" name="row[wechat][app_id]" value="{$item.value.app_id|default=''}" class="form-control" data-rule="" data-tip="公众号中支付时使用"/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>公众号的app_secret</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <input type="text" name="row[wechat][app_secret]" value="{$item.value.app_secret|default=''}" class="form-control" data-rule="" data-tip="仅在需要获取Openid时使用"/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>小程序的app_id</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <input type="text" name="row[wechat][miniapp_id]" value="{$item.value.miniapp_id|default=''}" class="form-control" data-rule="" data-tip="仅在小程序支付时使用"/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>微信支付商户号ID</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <input type="text" name="row[wechat][mch_id]" value="{$item.value.mch_id|default=''}" class="form-control" data-rule="" data-tip=""/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>微信支付商户的密钥</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <input type="text" name="row[wechat][key]" value="{$item.value.key|default=''}" class="form-control" data-rule="" data-tip=""/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>支付模式</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        {:Form::radios('row[wechat][mode]',['normal'=>'正式环境','dev'=>'沙箱环境','service'=>'服务商模式'],$item.value.mode??'normal')}
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}">
+                            <td>子商户商户号ID</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <input type="text" name="row[wechat][sub_mch_id]" value="{$item.value.sub_mch_id|default=''}" class="form-control" data-rule="" data-tip="如果未用到子商户,请勿填写"/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}">
+                            <td>子商户APP的app_id</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <input type="text" name="row[wechat][sub_appid]" value="{$item.value.sub_appid|default=''}" class="form-control" data-rule="" data-tip="如果未用到子商户,请勿填写"/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}">
+                            <td>子商户公众号的app_id</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <input type="text" name="row[wechat][sub_app_id]" value="{$item.value.sub_app_id|default=''}" class="form-control" data-rule="" data-tip="如果未用到子商户,请勿填写"/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}">
+                            <td>子商户小程序的app_id</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <input type="text" name="row[wechat][sub_miniapp_id]" value="{$item.value.sub_miniapp_id|default=''}" class="form-control" data-rule="" data-tip="如果未用到子商户,请勿填写"/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>回调通知地址</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <input type="text" name="row[wechat][notify_url]" value="{$item.value.notify_url|default=''}" class="form-control" data-rule="" data-tip="请勿随意修改,实际以逻辑代码中请求的为准"/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>微信支付API证书cert</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <div class="input-group">
+                                            <input id="c-cert_client" class="form-control" size="50" name="row[wechat][cert_client]" type="text" value="{$item.value.cert_client|htmlentities}" data-tip="可选, 仅在退款、红包等情况时需要用到">
+                                            <div class="input-group-addon no-border no-padding">
+                                                <span><button type="button" id="faupload-cert_client" class="btn btn-danger faupload" data-url="epay/upload" data-multipart='{"certname":"cert_client"}' data-mimetype="pem" data-input-id="c-cert_client" data-multiple="false"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                            </div>
+                                            <span class="msg-box n-right" for="c-cert_client"></span>
+                                        </div>
+                                        <div style="margin-top:5px;"><a href="https://pay.weixin.qq.com" target="_blank"><i class="fa fa-question-circle"></i> 如何获取微信支付API证书?</a></div>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>微信支付API证书key</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <div class="input-group">
+                                            <input id="c-cert_key" class="form-control" size="50" name="row[wechat][cert_key]" type="text" value="{$item.value.cert_key|htmlentities}" data-tip="可选, 仅在退款、红包等情况时需要用到">
+                                            <div class="input-group-addon no-border no-padding">
+                                                <span><button type="button" id="faupload-cert_key" class="btn btn-danger faupload" data-url="epay/upload" data-multipart='{"certname":"cert_key"}' data-mimetype="pem" data-input-id="c-cert_key" data-multiple="false"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                            </div>
+                                            <span class="msg-box n-right" for="c-cert_key"></span>
+                                        </div>
+                                        <div style="margin-top:5px;"><a href="https://pay.weixin.qq.com" target="_blank"><i class="fa fa-question-circle"></i> 如何获取微信支付API证书?</a></div>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+
+                        <tr>
+                            <td>记录日志</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        {:Form::radios('row[wechat][log]',['1'=>'开启','0'=>'关闭'],$item.value.log)}
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        </tbody>
+                    </table>
+                </div>
+                {elseif $item.name=='alipay'}
+                <div class="tab-pane fade" id="alipay">
+                    <table class="table table-striped table-config">
+                        <tbody>
+                        <tr>
+                            <td>支付模式</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-12 col-xs-12">
+                                        {:Form::radios('row[alipay][mode]',['normal'=>'正式环境','dev'=>'沙箱环境'],$item.value.mode??'normal')}
+
+                                        <div style="margin-top:5px;" data-mode="dev" class="text-muted {if ($item.value.mode??'')==='normal'}hidden{/if}">
+                                            <i class="fa fa-info-circle"></i> 如果使用沙箱环境,务必使用沙箱的app_id和沙箱证书,以及使用沙箱账号进行测试。<br>
+                                          沙箱环境:<a href="https://openhome.alipay.com/develop/sandbox/app" target="_blank">https://openhome.alipay.com/develop/sandbox/app</a>
+                                        </div>
+                                    </div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td width="15%">应用ID(app_id)</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <input type="text" name="row[alipay][app_id]" value="{$item.value.app_id|default=''}" class="form-control" data-rule="" data-tip=""/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>回调通知地址</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <input type="text" name="row[alipay][notify_url]" value="{$item.value.notify_url|default=''}" class="form-control" data-rule="" data-tip="请勿随意修改,实际以逻辑代码中请求的为准"/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>支付跳转地址</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <input type="text" name="row[alipay][return_url]" value="{$item.value.return_url|default=''}" class="form-control" data-rule="" data-tip="请勿随意修改,实际以逻辑代码中请求的为准"/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>应用私钥(private_key)</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <input type="text" name="row[alipay][private_key]" value="{$item.value.private_key|default=''}" class="form-control" data-rule=""/>
+                                        <div style="margin-top:5px;"><a href="https://opensupport.alipay.com/support/helpcenter/207/201602469554" target="_blank"><i class="fa fa-question-circle"></i> 如何获取应用私钥?</a></div>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>签名方式</td>
+                            <td>
+                                <div>
+                                    <div class="radio">
+                                        <label for="row[alipay][signtype]-publickey"><input id="row[alipay][signtype]-publickey" name="row[alipay][signtype]" {if isset($item.value.signtype)&&$item.value.signtype=='publickey'}checked{/if} type="radio" value="publickey"> 公钥</label>
+                                        <label for="row[alipay][signtype]-cert"><input id="row[alipay][signtype]-cert" {if isset($item.value.signtype)&&$item.value.signtype=='cert'}checked{/if} name="row[alipay][signtype]" type="radio" value="cert"> 公钥证书</label>
+                                    </div>
+                                </div>
+                                <div style="margin-top:5px;" class="text-muted">
+                                <i class="fa fa-info-circle"></i> 如果要使用转账、提现功能,则必须使用公钥证书
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>支付宝公钥路径(alipay_public_key)</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <div class="input-group">
+                                            <input id="c-ali_public_key" class="form-control" size="50" name="row[alipay][ali_public_key]" type="text" value="{$item.value.ali_public_key|default=''|htmlentities}" placeholder="公钥请直接粘贴,公钥证书请点击右侧的上传">
+                                            <div class="input-group-addon no-border no-padding">
+                                                <span><button type="button" id="faupload-ali_public_key" class="btn btn-danger faupload" data-url="epay/upload" data-multipart='{"certname":"ali_public_key"}' data-mimetype="crt" data-input-id="c-ali_public_key" data-multiple="false"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                            </div>
+                                            <span class="msg-box n-right" for="c-ali_public_key"></span>
+                                        </div>
+                                        <div style="margin-top:5px;"><a href="https://opensupport.alipay.com/support/helpcenter/271/201602474998" target="_blank"><i class="fa fa-question-circle"></i> 如何获取支付宝公钥证书?</a></div>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>应用公钥证书路径(app_cert_public_key)</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <div class="input-group">
+                                            <input id="c-app_cert_public_key" class="form-control" size="50" name="row[alipay][app_cert_public_key]" type="text" value="{$item.value.app_cert_public_key|default=''|htmlentities}">
+                                            <div class="input-group-addon no-border no-padding">
+                                                <span><button type="button" id="faupload-app_cert_public_key" class="btn btn-danger faupload" data-url="epay/upload" data-multipart='{"certname":"app_cert_public_key"}' data-mimetype="crt" data-input-id="c-app_cert_public_key" data-multiple="false"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                            </div>
+                                            <span class="msg-box n-right" for="c-app_cert_public_key"></span>
+                                        </div>
+                                        <div style="margin-top:5px;"><a href="https://opensupport.alipay.com/support/helpcenter/271/201602474998" target="_blank"><i class="fa fa-question-circle"></i> 如何获取应用公钥证书?</a></div>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>支付宝根证书路径(alipay_root_cert)</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <div class="input-group">
+                                            <input id="c-alipay_root_cert" class="form-control" size="50" name="row[alipay][alipay_root_cert]" type="text" value="{$item.value.alipay_root_cert|default=''|htmlentities}">
+                                            <div class="input-group-addon no-border no-padding">
+                                                <span><button type="button" id="faupload-alipay_root_cert" class="btn btn-danger faupload" data-url="epay/upload" data-multipart='{"certname":"alipay_root_cert"}' data-mimetype="crt" data-input-id="c-alipay_root_cert" data-multiple="false"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                            </div>
+                                            <span class="msg-box n-right" for="c-alipay_root_cert"></span>
+                                        </div>
+                                        <div style="margin-top:5px;"><a href="https://opensupport.alipay.com/support/helpcenter/271/201602474998" target="_blank"><i class="fa fa-question-circle"></i> 如何获取支付宝证书?</a></div>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+
+                        <tr>
+                            <td>记录日志</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        {:Form::radios('row[alipay][log]',['1'=>'开启','0'=>'关闭'],$item.value.log)}
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+
+                        <tr>
+                            <td>PC端使用扫码支付</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        {:Form::radios('row[alipay][scanpay]',['1'=>'开启','0'=>'关闭'],$item.value.scanpay??0)}
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        </tbody>
+                    </table>
+                </div>
+                {/if}
+                {/foreach}
+                <div class="form-group layer-footer">
+                    <label class="control-label col-xs-12 col-sm-2"></label>
+                    <div class="col-xs-12 col-sm-8">
+                        <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
+                        <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</form>
+<script>
+    document.querySelectorAll("input[name='row[wechat][mode]']").forEach(function (i, j) {
+        i.addEventListener("click", function () {
+            document.querySelectorAll("#wechat table tr[data-type]").forEach(function (m, n) {
+                m.classList.add("hidden");
+            });
+            document.querySelectorAll("#wechat table tr[data-type='" + this.value + "']").forEach(function (m, n) {
+                m.classList.remove("hidden");
+            });
+        });
+    });
+    document.querySelectorAll("input[name='row[alipay][mode]']").forEach(function (i, j) {
+        i.addEventListener("click", function () {
+            document.querySelectorAll("#alipay [data-mode]").forEach(function (m, n) {
+                m.classList.add("hidden");
+            });
+            document.querySelectorAll("#alipay [data-mode='" + this.value + "']").forEach(function (m, n) {
+                m.classList.remove("hidden");
+            });
+        });
+    });
+</script>

+ 55 - 0
addons/epay/config.php

@@ -0,0 +1,55 @@
+<?php
+
+return [
+    [
+        'name'    => 'wechat',
+        'title'   => '微信',
+        'type'    => 'array',
+        'content' => [],
+        'value'   => [
+            'appid'          => '',
+            'app_id'         => '',
+            'app_secret'     => '',
+            'miniapp_id'     => '',
+            'mch_id'         => '',
+            'key'            => '',
+            'mode'           => 'normal',
+            'sub_mch_id'     => '',
+            'sub_appid'      => '',
+            'sub_app_id'     => '',
+            'sub_miniapp_id' => '',
+            'notify_url'     => '/addons/epay/api/notifyx/type/wechat',
+            'cert_client'    => '/addons/epay/certs/apiclient_cert.pem',
+            'cert_key'       => '/addons/epay/certs/apiclient_key.pem',
+            'log'            => '1',
+        ],
+        'rule'    => '',
+        'msg'     => '',
+        'tip'     => '微信参数配置',
+        'ok'      => '',
+        'extend'  => '',
+    ],
+    [
+        'name'    => 'alipay',
+        'title'   => '支付宝',
+        'type'    => 'array',
+        'content' => [],
+        'value'   => [
+            'app_id'              => '',
+            'mode'                => 'normal',
+            'notify_url'          => '/addons/epay/api/notifyx/type/alipay',
+            'return_url'          => '/addons/epay/api/returnx/type/alipay',
+            'private_key'         => '',
+            'ali_public_key'      => '',
+            'app_cert_public_key' => '',
+            'alipay_root_cert'    => '',
+            'log'                 => '1',
+            'scanpay'             => '0',
+        ],
+        'rule'    => 'required',
+        'msg'     => '',
+        'tip'     => '支付宝参数配置',
+        'ok'      => '',
+        'extend'  => '',
+    ]
+];

+ 218 - 0
addons/epay/controller/Api.php

@@ -0,0 +1,218 @@
+<?php
+
+namespace addons\epay\controller;
+
+use addons\epay\library\Service;
+use addons\epay\library\Wechat;
+use addons\third\model\Third;
+use app\common\library\Auth;
+use think\addons\Controller;
+use think\Response;
+use think\Session;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Pay;
+
+/**
+ * API接口控制器
+ *
+ * @package addons\epay\controller
+ */
+class Api extends Controller
+{
+
+    protected $layout = 'default';
+    protected $config = [];
+
+    /**
+     * 默认方法
+     */
+    public function index()
+    {
+        return;
+    }
+
+    /**
+     * 外部提交
+     */
+    public function submit()
+    {
+        $this->request->filter('trim');
+        $out_trade_no = $this->request->request("out_trade_no");
+        $title = $this->request->request("title");
+        $amount = $this->request->request('amount');
+        $type = $this->request->request('type');
+        $method = $this->request->request('method', 'web');
+        $openid = $this->request->request('openid', '');
+        $auth_code = $this->request->request('auth_code', '');
+        $notifyurl = $this->request->request('notifyurl', '');
+        $returnurl = $this->request->request('returnurl', '');
+
+        if (!$amount || $amount < 0) {
+            $this->error("支付金额必须大于0");
+        }
+
+        if (!$type || !in_array($type, ['alipay', 'wechat'])) {
+            $this->error("支付类型错误");
+        }
+
+        $params = [
+            'type'         => $type,
+            'out_trade_no' => $out_trade_no,
+            'title'        => $title,
+            'amount'       => $amount,
+            'method'       => $method,
+            'openid'       => $openid,
+            'auth_code'    => $auth_code,
+            'notifyurl'    => $notifyurl,
+            'returnurl'    => $returnurl,
+        ];
+        return Service::submitOrder($params);
+    }
+
+    /**
+     * 微信支付(公众号支付&PC扫码支付)
+     * @return string
+     */
+    public function wechat()
+    {
+        $config = Service::getConfig('wechat');
+
+        $isWechat = stripos($this->request->server('HTTP_USER_AGENT'), 'MicroMessenger') !== false;
+        $isMobile = $this->request->isMobile();
+        $this->view->assign("isWechat", $isWechat);
+        $this->view->assign("isMobile", $isMobile);
+
+        //发起PC支付(Scan支付)(PC扫码模式)
+        if ($this->request->isAjax()) {
+            $pay = Pay::wechat($config);
+            $orderid = $this->request->post("orderid");
+            try {
+                $result = $pay->find($orderid, 'scan');
+                if ($result['return_code'] == 'SUCCESS' && $result['result_code'] == 'SUCCESS') {
+                    $this->success("", "", ['status' => $result['trade_state']]);
+                } else {
+                    $this->error("查询失败");
+                }
+            } catch (GatewayException $e) {
+                $this->error("查询失败");
+            }
+        }
+
+        $orderData = Session::get("wechatorderdata");
+        if (!$orderData) {
+            $this->error("请求参数错误");
+        }
+        if ($isWechat && $isMobile) {
+            //发起公众号(jsapi支付),openid必须
+
+            //如果没有openid,则自动去获取openid
+            if (!isset($orderData['openid']) || !$orderData['openid']) {
+                $orderData['openid'] = Service::getOpenid();
+            }
+
+            $orderData['method'] = 'mp';
+            $type = 'jsapi';
+            $payData = Service::submitOrder($orderData);
+            if (!isset($payData['paySign'])) {
+                $this->error("创建订单失败,请返回重试", "");
+            }
+        } else {
+            $orderData['method'] = 'scan';
+            $type = 'pc';
+            $payData = Service::submitOrder($orderData);
+            if (!isset($payData['code_url'])) {
+                $this->error("创建订单失败,请返回重试", "");
+            }
+        }
+        $this->view->assign("orderData", $orderData);
+        $this->view->assign("payData", $payData);
+        $this->view->assign("type", $type);
+
+        $this->view->assign("title", "微信支付");
+        return $this->view->fetch();
+    }
+
+    /**
+     * 支付宝支付(PC扫码支付)
+     * @return string
+     */
+    public function alipay()
+    {
+        $config = Service::getConfig('alipay');
+
+        $isWechat = stripos($this->request->server('HTTP_USER_AGENT'), 'MicroMessenger') !== false;
+        $isMobile = $this->request->isMobile();
+        $this->view->assign("isWechat", $isWechat);
+        $this->view->assign("isMobile", $isMobile);
+
+        if ($this->request->isAjax()) {
+            $orderid = $this->request->post("orderid");
+            $pay = Pay::alipay($config);
+            try {
+                $result = $pay->find($orderid, 'scan');
+                if ($result['code'] == '10000' && $result['trade_status'] == 'TRADE_SUCCESS') {
+                    $this->success("", "", ['status' => $result['trade_status']]);
+                } else {
+                    $this->error("查询失败");
+                }
+            } catch (GatewayException $e) {
+                $this->error("查询失败");
+            }
+        }
+
+        //发起PC支付(Scan支付)(PC扫码模式)
+        $orderData = Session::get("alipayorderdata");
+        if (!$orderData) {
+            $this->error("请求参数错误");
+        }
+
+        $orderData['method'] = 'scan';
+        $payData = Service::submitOrder($orderData);
+        if (!isset($payData['qr_code'])) {
+            $this->error("创建订单失败,请返回重试");
+        }
+
+        $type = 'pc';
+        $this->view->assign("orderData", $orderData);
+        $this->view->assign("payData", $payData);
+        $this->view->assign("type", $type);
+        $this->view->assign("title", "支付宝支付");
+        return $this->view->fetch();
+    }
+
+    /**
+     * 支付成功回调
+     */
+    public function notifyx()
+    {
+        $type = $this->request->param('type');
+        $pay = \addons\epay\library\Service::checkNotify($type);
+        if (!$pay) {
+            echo '签名错误';
+            return;
+        }
+        $data = $pay->verify();
+
+        //你可以在这里你的业务处理逻辑,比如处理你的订单状态、给会员加余额等等功能
+        //下面这句必须要执行,且在此之前不能有任何输出
+        return $pay->success()->send();
+    }
+
+    /**
+     * 支付成功返回
+     */
+    public function returnx()
+    {
+        $type = $this->request->param('type');
+        if (Service::checkReturn($type)) {
+            echo '签名错误';
+            return;
+        }
+
+        //你可以在这里定义你的提示信息,但切记不可在此编写逻辑
+        $this->success("恭喜你!支付成功!", addon_url("epay/index/index"));
+
+        return;
+    }
+
+}

+ 113 - 0
addons/epay/controller/Index.php

@@ -0,0 +1,113 @@
+<?php
+
+namespace addons\epay\controller;
+
+use addons\epay\library\Service;
+use fast\Random;
+use think\addons\Controller;
+use Exception;
+
+/**
+ * 微信支付宝整合插件首页
+ *
+ * 此控制器仅用于开发展示说明和测试,请自行添加一个新的控制器进行处理返回和回调事件,同时删除此控制器文件
+ *
+ * Class Index
+ * @package addons\epay\controller
+ */
+class Index extends Controller
+{
+
+    protected $layout = 'default';
+
+    protected $config = [];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        if (!config("app_debug")) {
+            $this->error("仅在开发环境下查看");
+        }
+    }
+
+    public function index()
+    {
+        $this->view->assign("title", "微信支付宝整合");
+        return $this->view->fetch();
+    }
+
+    /**
+     * 体验,仅供开发测试
+     */
+    public function experience()
+    {
+        $amount = $this->request->request('amount');
+        $type = $this->request->request('type');
+        $method = $this->request->request('method');
+
+        if (!$amount || $amount < 0) {
+            $this->error("支付金额必须大于0");
+        }
+
+        if (!$type || !in_array($type, ['alipay', 'wechat'])) {
+            $this->error("支付类型不能为空");
+        }
+
+        //订单号
+        $out_trade_no = date("YmdHis") . mt_rand(100000, 999999);
+
+        //订单标题
+        $title = '测试订单';
+
+        //回调链接
+        $notifyurl = $this->request->root(true) . '/addons/epay/index/notifyx/paytype/' . $type;
+        $returnurl = $this->request->root(true) . '/addons/epay/index/returnx/paytype/' . $type . '/out_trade_no/' . $out_trade_no;
+
+        $response = Service::submitOrder($amount, $out_trade_no, $type, $title, $notifyurl, $returnurl, $method);
+
+        return $response;
+    }
+
+    /**
+     * 支付成功,仅供开发测试
+     */
+    public function notifyx()
+    {
+        $paytype = $this->request->param('paytype');
+        $pay = Service::checkNotify($paytype);
+        if (!$pay) {
+            echo '签名错误';
+            return;
+        }
+        $data = $pay->verify();
+        try {
+            $payamount = $paytype == 'alipay' ? $data['total_amount'] : $data['total_fee'] / 100;
+            $out_trade_no = $data['out_trade_no'];
+
+            //你可以在此编写订单逻辑
+        } catch (Exception $e) {
+        }
+
+        //下面这句必须要执行,且在此之前不能有任何输出
+        return $pay->success()->send();
+    }
+
+    /**
+     * 支付返回,仅供开发测试
+     */
+    public function returnx()
+    {
+        $paytype = $this->request->param('paytype');
+        $out_trade_no = $this->request->param('out_trade_no');
+        $pay = Service::checkReturn($paytype);
+        if (!$pay) {
+            $this->error('签名错误', '');
+        }
+
+        //你可以在这里通过out_trade_no去验证订单状态
+        //但是不可以在此编写订单逻辑!!!
+
+        $this->success("请返回网站查看支付结果", addon_url("epay/index/index"));
+    }
+
+}

+ 10 - 0
addons/epay/info.ini

@@ -0,0 +1,10 @@
+name = epay
+title = 微信支付宝整合
+intro = 可用于快速整合企业微信、支付宝支付功能
+author = FastAdmin
+website = https://www.fastadmin.net
+version = 1.2.10
+state = 1
+url = /addons/epay
+license = regular
+licenseto = 38239

+ 18 - 0
addons/epay/library/Collection.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace addons\epay\library;
+
+class Collection extends \Yansongda\Supports\Collection
+{
+
+    /**
+     * 创建 Collection 实例
+     * @access public
+     * @param  array $items 数据
+     * @return static
+     */
+    public static function make($items = [])
+    {
+        return new static($items);
+    }
+}

+ 16 - 0
addons/epay/library/OrderException.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace addons\epay\library;
+
+use think\Exception;
+
+class OrderException extends Exception
+{
+    public function __construct($message = "", $code = 0, $data = [])
+    {
+        $this->message = $message;
+        $this->code = $code;
+        $this->data = $data;
+    }
+
+}

+ 58 - 0
addons/epay/library/RedirectResponse.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace addons\epay\library;
+
+class RedirectResponse extends \Symfony\Component\HttpFoundation\RedirectResponse implements \JsonSerializable, \Serializable
+{
+    public function __toString()
+    {
+        return $this->getContent();
+    }
+
+    public function setTargetUrl($url)
+    {
+        if ('' === ($url ?? '')) {
+            throw new \InvalidArgumentException('无法跳转到空页面');
+        }
+
+        $this->targetUrl = $url;
+
+        $this->setContent(
+            sprintf('<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="UTF-8" />
+        <meta http-equiv="refresh" content="0;url=\'%1$s\'" />
+
+        <title>正在跳转支付 %1$s</title>
+    </head>
+    <body>
+        <div id="redirect" style="display:none;">正在跳转支付 <a href="%1$s">%1$s</a></div>
+        <script type="text/javascript">
+            setTimeout(function(){
+                document.getElementById("redirect").style.display = "block";
+            }, 1000);
+        </script>
+    </body>
+</html>', htmlspecialchars($url, \ENT_QUOTES, 'UTF-8')));
+
+        $this->headers->set('Location', $url);
+
+        return $this;
+    }
+
+    public function jsonSerialize()
+    {
+        return $this->getContent();
+    }
+
+    public function serialize()
+    {
+        return serialize($this->content);
+    }
+
+    public function unserialize($serialized)
+    {
+        return $this->content = unserialize($serialized);
+    }
+}

+ 26 - 0
addons/epay/library/Response.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace addons\epay\library;
+
+class Response extends \Symfony\Component\HttpFoundation\Response implements \JsonSerializable, \Serializable
+{
+    public function __toString()
+    {
+        return $this->getContent();
+    }
+
+    public function jsonSerialize()
+    {
+        return $this->getContent();
+    }
+
+    public function serialize()
+    {
+        return serialize($this->content);
+    }
+
+    public function unserialize($serialized)
+    {
+        return $this->content = unserialize($serialized);
+    }
+}

+ 310 - 0
addons/epay/library/Service.php

@@ -0,0 +1,310 @@
+<?php
+
+namespace addons\epay\library;
+
+use addons\third\model\Third;
+use app\common\library\Auth;
+use Exception;
+use think\Session;
+use Yansongda\Pay\Pay;
+use Yansongda\Supports\Str;
+
+/**
+ * 订单服务类
+ *
+ * @package addons\epay\library
+ */
+class Service
+{
+
+    /**
+     * 提交订单
+     * @param array|float $amount    订单金额
+     * @param string      $orderid   订单号
+     * @param string      $type      支付类型,可选alipay或wechat
+     * @param string      $title     订单标题
+     * @param string      $notifyurl 通知回调URL
+     * @param string      $returnurl 跳转返回URL
+     * @param string      $method    支付方法
+     * @return Response|RedirectResponse|Collection
+     * @throws Exception
+     */
+    public static function submitOrder($amount, $orderid = null, $type = null, $title = null, $notifyurl = null, $returnurl = null, $method = null, $openid = '')
+    {
+        if (!is_array($amount)) {
+            $params = [
+                'amount'    => $amount,
+                'orderid'   => $orderid,
+                'type'      => $type,
+                'title'     => $title,
+                'notifyurl' => $notifyurl,
+                'returnurl' => $returnurl,
+                'method'    => $method,
+                'openid'    => $openid,
+            ];
+        } else {
+            $params = $amount;
+        }
+        $type = isset($params['type']) && in_array($params['type'], ['alipay', 'wechat']) ? $params['type'] : 'wechat';
+        $method = isset($params['method']) ? $params['method'] : 'web';
+        $orderid = isset($params['orderid']) ? $params['orderid'] : date("YmdHis") . mt_rand(100000, 999999);
+        $amount = isset($params['amount']) ? $params['amount'] : 1;
+        $title = isset($params['title']) ? $params['title'] : "支付";
+        $auth_code = isset($params['auth_code']) ? $params['auth_code'] : '';
+        $openid = isset($params['openid']) ? $params['openid'] : '';
+
+        $request = request();
+        $notifyurl = isset($params['notifyurl']) ? $params['notifyurl'] : $request->root(true) . '/addons/epay/index/' . $type . 'notify';
+        $returnurl = isset($params['returnurl']) ? $params['returnurl'] : $request->root(true) . '/addons/epay/index/' . $type . 'return/out_trade_no/' . $orderid;
+        $html = '';
+        $config = Service::getConfig($type);
+        $config['notify_url'] = $notifyurl;
+        $config['return_url'] = $returnurl;
+        $isWechat = strpos($request->server('HTTP_USER_AGENT'), 'MicroMessenger') !== false;
+
+        $result = null;
+        if ($type == 'alipay') {
+            //如果是PC支付,判断当前环境,进行跳转
+            if ($method == 'web') {
+                //如果是微信环境或后台配置PC使用扫码支付
+                if ($isWechat || $config['scanpay']) {
+                    Session::set("alipayorderdata", $params);
+                    $url = addon_url('epay/api/alipay', [], true, true);
+                    return RedirectResponse::create($url);
+                } elseif ($request->isMobile()) {
+                    $method = 'wap';
+                }
+            }
+            //创建支付对象
+            $pay = Pay::alipay($config);
+            $params = [
+                'out_trade_no' => $orderid,//你的订单号
+                'total_amount' => $amount,//单位元
+                'subject'      => $title,
+            ];
+
+            switch ($method) {
+                case 'web':
+                    //电脑支付
+                    $result = $pay->web($params);
+                    break;
+                case 'wap':
+                    //手机网页支付
+                    $result = $pay->wap($params);
+                    break;
+                case 'app':
+                    //APP支付
+                    $result = $pay->app($params);
+                    break;
+                case 'scan':
+                    //扫码支付
+                    $result = $pay->scan($params);
+                    break;
+                case 'pos':
+                    //刷卡支付必须要有auth_code
+                    $params['auth_code'] = $auth_code;
+                    $result = $pay->pos($params);
+                    break;
+                case 'mini':
+                case 'miniapp':
+                    //小程序支付
+                    //小程序支付,直接返回字符串
+                    //小程序支付必须要有buyer_id,这里使用openid
+                    $params['buyer_id'] = $openid;
+                    $result = $pay->mini($params);
+                    break;
+                default:
+            }
+        } else {
+            //如果是PC支付,判断当前环境,进行跳转
+            if ($method == 'web') {
+                //如果是移动端,但不是微信环境
+                if ($request->isMobile() && !$isWechat) {
+                    $method = 'wap';
+                } else {
+                    Session::set("wechatorderdata", $params);
+                    $url = addon_url('epay/api/wechat', [], true, true);
+                    return RedirectResponse::create($url);
+                }
+            }
+
+            //创建支付对象
+            $pay = Pay::wechat($config);
+            $params = [
+                'out_trade_no' => $orderid,//你的订单号
+                'body'         => $title,
+                'total_fee'    => $amount * 100, //单位分
+            ];
+            switch ($method) {
+                //case 'web':
+                //    //电脑支付,跳转到自定义展示页面
+                //    $result = $pay->web($params);
+                //    break;
+                case 'mp':
+                    //公众号支付
+                    //公众号支付必须有openid
+                    $params['openid'] = $openid;
+                    $result = $pay->mp($params);
+                    break;
+                case 'wap':
+                    //手机网页支付,跳转
+                    $params['spbill_create_ip'] = $request->ip(0, false);
+                    $result = $pay->wap($params);
+                    break;
+                case 'app':
+                    //APP支付,直接返回字符串
+                    $result = $pay->app($params);
+                    break;
+                case 'scan':
+                    //扫码支付,直接返回字符串
+                    $result = $pay->scan($params);
+                    break;
+                case 'pos':
+                    //刷卡支付,直接返回字符串
+                    //刷卡支付必须要有auth_code
+                    $params['auth_code'] = $auth_code;
+                    $result = $pay->pos($params);
+                    break;
+                case 'mini':
+                case 'miniapp':
+                    //小程序支付,直接返回字符串
+                    //小程序支付必须要有openid
+                    $params['openid'] = $openid;
+                    $result = $pay->miniapp($params);
+                    break;
+                default:
+            }
+        }
+
+        //使用重写的Response类、RedirectResponse、Collection类
+        if ($result instanceof \Symfony\Component\HttpFoundation\RedirectResponse) {
+            $result = RedirectResponse::create($result->getTargetUrl());
+        } elseif ($result instanceof \Symfony\Component\HttpFoundation\Response) {
+            $result = Response::create($result->getContent());
+        } elseif ($result instanceof \Yansongda\Supports\Collection) {
+            $result = Collection::make($result->all());
+        }
+
+        return $result;
+    }
+
+    /**
+     * 验证回调是否成功
+     * @param string $type   支付类型
+     * @param array  $config 配置信息
+     * @return bool|\Yansongda\Pay\Gateways\Alipay|\Yansongda\Pay\Gateways\Wechat
+     */
+    public static function checkNotify($type, $config = [])
+    {
+        $type = strtolower($type);
+        if (!in_array($type, ['wechat', 'alipay'])) {
+            return false;
+        }
+        try {
+            $config = self::getConfig($type);
+            $pay = $type == 'wechat' ? Pay::wechat($config) : Pay::alipay($config);
+            $data = $pay->verify();
+
+            if ($type == 'alipay') {
+                if (in_array($data['trade_status'], ['TRADE_SUCCESS', 'TRADE_FINISHED'])) {
+                    return $pay;
+                }
+            } else {
+                return $pay;
+            }
+        } catch (Exception $e) {
+            return false;
+        }
+
+        return false;
+    }
+
+    /**
+     * 验证返回是否成功,请勿用于判断是否支付成功的逻辑验证
+     * 已弃用
+     *
+     * @param string $type   支付类型
+     * @param array  $config 配置信息
+     * @return bool
+     * @deprecated  已弃用,请勿用于逻辑验证
+     */
+    public static function checkReturn($type, $config = [])
+    {
+        //由于PC及移动端无法获取请求的参数信息,取消return验证,均返回true
+        return true;
+    }
+
+    /**
+     * 获取配置
+     * @param string $type 支付类型
+     * @return array|mixed
+     */
+    public static function getConfig($type = 'wechat')
+    {
+        $config = get_addon_config('epay');
+        $config = isset($config[$type]) ? $config[$type] : $config['wechat'];
+        if ($config['log']) {
+            $config['log'] = [
+                'file'  => LOG_PATH . 'epaylogs' . DS . $type . '-' . date("Y-m-d") . '.log',
+                'level' => 'debug'
+            ];
+        }
+        if (isset($config['cert_client']) && substr($config['cert_client'], 0, 8) == '/addons/') {
+            $config['cert_client'] = ROOT_PATH . str_replace('/', DS, substr($config['cert_client'], 1));
+        }
+        if (isset($config['cert_key']) && substr($config['cert_key'], 0, 8) == '/addons/') {
+            $config['cert_key'] = ROOT_PATH . str_replace('/', DS, substr($config['cert_key'], 1));
+        }
+        if (isset($config['app_cert_public_key']) && substr($config['app_cert_public_key'], 0, 8) == '/addons/') {
+            $config['app_cert_public_key'] = ROOT_PATH . str_replace('/', DS, substr($config['app_cert_public_key'], 1));
+        }
+        if (isset($config['alipay_root_cert']) && substr($config['alipay_root_cert'], 0, 8) == '/addons/') {
+            $config['alipay_root_cert'] = ROOT_PATH . str_replace('/', DS, substr($config['alipay_root_cert'], 1));
+        }
+        if (isset($config['ali_public_key']) && (Str::endsWith($config['ali_public_key'], '.crt') || Str::endsWith($config['ali_public_key'], '.pem'))) {
+            $config['ali_public_key'] = ROOT_PATH . str_replace('/', DS, substr($config['ali_public_key'], 1));
+        }
+        // 可选
+        $config['http'] = [
+            'timeout'         => 10,
+            'connect_timeout' => 10,
+            // 更多配置项请参考 [Guzzle](https://guzzle-cn.readthedocs.io/zh_CN/latest/request-options.html)
+        ];
+
+        $config['notify_url'] = empty($config['notify_url']) ? addon_url('epay/api/notifyx', [], false) . '/type/' . $type : $config['notify_url'];
+        $config['notify_url'] = !preg_match("/^(http:\/\/|https:\/\/)/i", $config['notify_url']) ? request()->root(true) . $config['notify_url'] : $config['notify_url'];
+        $config['return_url'] = empty($config['return_url']) ? addon_url('epay/api/returnx', [], false) . '/type/' . $type : $config['return_url'];
+        $config['return_url'] = !preg_match("/^(http:\/\/|https:\/\/)/i", $config['return_url']) ? request()->root(true) . $config['return_url'] : $config['return_url'];
+        return $config;
+    }
+
+    /**
+     * 获取微信Openid
+     *
+     * @return mixed|string
+     */
+    public static function getOpenid()
+    {
+        $config = self::getConfig('wechat');
+        $openid = '';
+        $auth = Auth::instance();
+        if ($auth->isLogin()) {
+            $third = get_addon_info('third');
+            if ($third && $third['state']) {
+                $thirdInfo = Third::where('user_id', $auth->id)->where('platform', 'wechat')->where('apptype', 'mp')->find();
+                $openid = $thirdInfo ? $thirdInfo['openid'] : '';
+            }
+        }
+        if (!$openid) {
+            $openid = Session::get("openid");
+
+            //如果未传openid,则去读取openid
+            if (!$openid) {
+                $wechat = new Wechat($config['app_id'], $config['app_secret']);
+                $openid = $wechat->getOpenid();
+            }
+        }
+        return $openid;
+    }
+
+}

+ 110 - 0
addons/epay/library/Wechat.php

@@ -0,0 +1,110 @@
+<?php
+
+namespace addons\epay\library;
+
+use fast\Http;
+use think\Cache;
+use think\Session;
+
+/**
+ * 微信授权
+ *
+ */
+class Wechat
+{
+    private $app_id = '';
+    private $app_secret = '';
+    private $scope = 'snsapi_userinfo';
+
+    public function __construct($app_id, $app_secret)
+    {
+        $this->app_id = $app_id;
+        $this->app_secret = $app_secret;
+    }
+
+    /**
+     * 获取微信授权链接
+     *
+     * @return string
+     */
+    public function getAuthorizeUrl()
+    {
+        $redirect_uri = addon_url('epay/api/wechat', [], true, true);
+        $redirect_uri = urlencode($redirect_uri);
+        $state = \fast\Random::alnum();
+        Session::set('state', $state);
+        return "https://open.weixin.qq.com/connect/oauth2/authorize?appid={$this->app_id}&redirect_uri={$redirect_uri}&response_type=code&scope={$this->scope}&state={$state}#wechat_redirect";
+    }
+
+    /**
+     * 获取微信openid
+     *
+     * @return mixed|string
+     */
+    public function getOpenid()
+    {
+        $openid = Session::get('openid');
+        if (!$openid) {
+            if (!isset($_GET['code'])) {
+                $url = $this->getAuthorizeUrl();
+
+                Header("Location: $url");
+                exit();
+            } else {
+                $state = Session::get('state');
+                if ($state == $_GET['state']) {
+                    $code = $_GET['code'];
+                    $token = $this->getAccessToken($code);
+                    if (!isset($token['openid']) && isset($token['errmsg'])) {
+                        exception($token['errmsg']);
+                    }
+                    $openid = isset($token['openid']) ? $token['openid'] : '';
+                    if ($openid) {
+                        Session::set("openid", $openid);
+                    }
+                }
+            }
+        }
+        return $openid;
+    }
+
+    /**
+     * 获取授权token网页授权
+     *
+     * @param string $code
+     * @return mixed|string
+     */
+    public function getAccessToken($code = '')
+    {
+        $params = [
+            'appid'      => $this->app_id,
+            'secret'     => $this->app_secret,
+            'code'       => $code,
+            'grant_type' => 'authorization_code'
+        ];
+        $ret = Http::sendRequest('https://api.weixin.qq.com/sns/oauth2/access_token', $params, 'GET');
+        if ($ret['ret']) {
+            $ar = json_decode($ret['msg'], true);
+            return $ar;
+        }
+        return [];
+    }
+
+    public function getJsticket($code = '')
+    {
+        $jsticket = Session::get('jsticket');
+        if (!$jsticket) {
+            $token = $this->getAccessToken($code);
+            $params = [
+                'access_token' => 'token',
+                'type'         => 'jsapi',
+            ];
+            $ret = Http::sendRequest('https://api.weixin.qq.com/cgi-bin/ticket/getticket', $params, 'GET');
+            if ($ret['ret']) {
+                $ar = json_decode($ret['msg'], true);
+                return $ar;
+            }
+        }
+        return $jsticket;
+    }
+}

+ 83 - 0
addons/epay/library/Yansongda/Pay/Contracts/GatewayApplicationInterface.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace Yansongda\Pay\Contracts;
+
+use Symfony\Component\HttpFoundation\Response;
+use Yansongda\Supports\Collection;
+
+interface GatewayApplicationInterface
+{
+    /**
+     * To pay.
+     *
+     * @author yansongda <me@yansonga.cn>
+     *
+     * @param string $gateway
+     * @param array  $params
+     *
+     * @return Collection|Response
+     */
+    public function pay($gateway, $params);
+
+    /**
+     * Query an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string|array $order
+     *
+     * @return Collection
+     */
+    public function find($order, string $type);
+
+    /**
+     * Refund an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return Collection
+     */
+    public function refund(array $order);
+
+    /**
+     * Cancel an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string|array $order
+     *
+     * @return Collection
+     */
+    public function cancel($order);
+
+    /**
+     * Close an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string|array $order
+     *
+     * @return Collection
+     */
+    public function close($order);
+
+    /**
+     * Verify a request.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string|array|null $content
+     *
+     * @return Collection
+     */
+    public function verify($content, bool $refund);
+
+    /**
+     * Echo success to server.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return Response
+     */
+    public function success();
+}

+ 20 - 0
addons/epay/library/Yansongda/Pay/Contracts/GatewayInterface.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace Yansongda\Pay\Contracts;
+
+use Symfony\Component\HttpFoundation\Response;
+use Yansongda\Supports\Collection;
+
+interface GatewayInterface
+{
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @return Collection|Response
+     */
+    public function pay($endpoint, array $payload);
+}

+ 98 - 0
addons/epay/library/Yansongda/Pay/Events.php

@@ -0,0 +1,98 @@
+<?php
+
+namespace Yansongda\Pay;
+
+use Exception;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Contracts\EventDispatcher\Event;
+
+/**
+ * @author yansongda <me@yansongda.cn>
+ *
+ * @method static Event dispatch(Event $event)                                Dispatches an event to all registered listeners
+ * @method static array getListeners($eventName = null)                       Gets the listeners of a specific event or all listeners sorted by descending priority.
+ * @method static int|void getListenerPriority($eventName, $listener)         Gets the listener priority for a specific event.
+ * @method static bool hasListeners($eventName = null)                        Checks whether an event has any registered listeners.
+ * @method static void addListener($eventName, $listener, $priority = 0)      Adds an event listener that listens on the specified events.
+ * @method static removeListener($eventName, $listener)                       Removes an event listener from the specified events.
+ * @method static void addSubscriber(EventSubscriberInterface $subscriber)    Adds an event subscriber.
+ * @method static void removeSubscriber(EventSubscriberInterface $subscriber)
+ */
+class Events
+{
+    /**
+     * dispatcher.
+     *
+     * @var EventDispatcher
+     */
+    protected static $dispatcher;
+
+    /**
+     * Forward call.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $method
+     * @param array  $args
+     *
+     * @throws Exception
+     *
+     * @return mixed
+     */
+    public static function __callStatic($method, $args)
+    {
+        return call_user_func_array([self::getDispatcher(), $method], $args);
+    }
+
+    /**
+     * Forward call.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $method
+     * @param array  $args
+     *
+     * @throws Exception
+     *
+     * @return mixed
+     */
+    public function __call($method, $args)
+    {
+        return call_user_func_array([self::getDispatcher(), $method], $args);
+    }
+
+    /**
+     * setDispatcher.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public static function setDispatcher(EventDispatcher $dispatcher)
+    {
+        self::$dispatcher = $dispatcher;
+    }
+
+    /**
+     * getDispatcher.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public static function getDispatcher(): EventDispatcher
+    {
+        if (self::$dispatcher) {
+            return self::$dispatcher;
+        }
+
+        return self::$dispatcher = self::createDispatcher();
+    }
+
+    /**
+     * createDispatcher.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public static function createDispatcher(): EventDispatcher
+    {
+        return new EventDispatcher();
+    }
+}

+ 31 - 0
addons/epay/library/Yansongda/Pay/Events/ApiRequested.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace Yansongda\Pay\Events;
+
+class ApiRequested extends Event
+{
+    /**
+     * Endpoint.
+     *
+     * @var string
+     */
+    public $endpoint;
+
+    /**
+     * Result.
+     *
+     * @var array
+     */
+    public $result;
+
+    /**
+     * Bootstrap.
+     */
+    public function __construct(string $driver, string $gateway, string $endpoint, array $result)
+    {
+        $this->endpoint = $endpoint;
+        $this->result = $result;
+
+        parent::__construct($driver, $gateway);
+    }
+}

+ 31 - 0
addons/epay/library/Yansongda/Pay/Events/ApiRequesting.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace Yansongda\Pay\Events;
+
+class ApiRequesting extends Event
+{
+    /**
+     * Endpoint.
+     *
+     * @var string
+     */
+    public $endpoint;
+
+    /**
+     * Payload.
+     *
+     * @var array
+     */
+    public $payload;
+
+    /**
+     * Bootstrap.
+     */
+    public function __construct(string $driver, string $gateway, string $endpoint, array $payload)
+    {
+        $this->endpoint = $endpoint;
+        $this->payload = $payload;
+
+        parent::__construct($driver, $gateway);
+    }
+}

+ 40 - 0
addons/epay/library/Yansongda/Pay/Events/Event.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace Yansongda\Pay\Events;
+
+use Symfony\Contracts\EventDispatcher\Event as SymfonyEvent;
+
+class Event extends SymfonyEvent
+{
+    /**
+     * Driver.
+     *
+     * @var string
+     */
+    public $driver;
+
+    /**
+     * Method.
+     *
+     * @var string
+     */
+    public $gateway;
+
+    /**
+     * Extra attributes.
+     *
+     * @var mixed
+     */
+    public $attributes;
+
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function __construct(string $driver, string $gateway)
+    {
+        $this->driver = $driver;
+        $this->gateway = $gateway;
+    }
+}

+ 33 - 0
addons/epay/library/Yansongda/Pay/Events/MethodCalled.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Yansongda\Pay\Events;
+
+class MethodCalled extends Event
+{
+    /**
+     * endpoint.
+     *
+     * @var string
+     */
+    public $endpoint;
+
+    /**
+     * payload.
+     *
+     * @var array
+     */
+    public $payload;
+
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function __construct(string $driver, string $gateway, string $endpoint, array $payload = [])
+    {
+        $this->endpoint = $endpoint;
+        $this->payload = $payload;
+
+        parent::__construct($driver, $gateway);
+    }
+}

+ 31 - 0
addons/epay/library/Yansongda/Pay/Events/PayStarted.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace Yansongda\Pay\Events;
+
+class PayStarted extends Event
+{
+    /**
+     * Endpoint.
+     *
+     * @var string
+     */
+    public $endpoint;
+
+    /**
+     * Payload.
+     *
+     * @var array
+     */
+    public $payload;
+
+    /**
+     * Bootstrap.
+     */
+    public function __construct(string $driver, string $gateway, string $endpoint, array $payload)
+    {
+        $this->endpoint = $endpoint;
+        $this->payload = $payload;
+
+        parent::__construct($driver, $gateway);
+    }
+}

+ 23 - 0
addons/epay/library/Yansongda/Pay/Events/PayStarting.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace Yansongda\Pay\Events;
+
+class PayStarting extends Event
+{
+    /**
+     * Params.
+     *
+     * @var array
+     */
+    public $params;
+
+    /**
+     * Bootstrap.
+     */
+    public function __construct(string $driver, string $gateway, array $params)
+    {
+        $this->params = $params;
+
+        parent::__construct($driver, $gateway);
+    }
+}

+ 25 - 0
addons/epay/library/Yansongda/Pay/Events/RequestReceived.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace Yansongda\Pay\Events;
+
+class RequestReceived extends Event
+{
+    /**
+     * Received data.
+     *
+     * @var array
+     */
+    public $data;
+
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function __construct(string $driver, string $gateway, array $data)
+    {
+        $this->data = $data;
+
+        parent::__construct($driver, $gateway);
+    }
+}

+ 25 - 0
addons/epay/library/Yansongda/Pay/Events/SignFailed.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace Yansongda\Pay\Events;
+
+class SignFailed extends Event
+{
+    /**
+     * Received data.
+     *
+     * @var array
+     */
+    public $data;
+
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function __construct(string $driver, string $gateway, array $data)
+    {
+        $this->data = $data;
+
+        parent::__construct($driver, $gateway);
+    }
+}

+ 19 - 0
addons/epay/library/Yansongda/Pay/Exceptions/BusinessException.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace Yansongda\Pay\Exceptions;
+
+class BusinessException extends GatewayException
+{
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansonga.cn>
+     *
+     * @param string       $message
+     * @param array|string $raw
+     */
+    public function __construct($message, $raw = [])
+    {
+        parent::__construct('ERROR_BUSINESS: '.$message, $raw, self::ERROR_BUSINESS);
+    }
+}

+ 44 - 0
addons/epay/library/Yansongda/Pay/Exceptions/Exception.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace Yansongda\Pay\Exceptions;
+
+class Exception extends \Exception
+{
+    const UNKNOWN_ERROR = 9999;
+
+    const INVALID_GATEWAY = 1;
+
+    const INVALID_CONFIG = 2;
+
+    const INVALID_ARGUMENT = 3;
+
+    const ERROR_GATEWAY = 4;
+
+    const INVALID_SIGN = 5;
+
+    const ERROR_BUSINESS = 6;
+
+    /**
+     * Raw error info.
+     *
+     * @var array
+     */
+    public $raw;
+
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansonga.cn>
+     *
+     * @param string       $message
+     * @param array|string $raw
+     * @param int|string   $code
+     */
+    public function __construct($message = '', $raw = [], $code = self::UNKNOWN_ERROR)
+    {
+        $message = '' === $message ? 'Unknown Error' : $message;
+        $this->raw = is_array($raw) ? $raw : [$raw];
+
+        parent::__construct($message, intval($code));
+    }
+}

+ 20 - 0
addons/epay/library/Yansongda/Pay/Exceptions/GatewayException.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace Yansongda\Pay\Exceptions;
+
+class GatewayException extends Exception
+{
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansonga.cn>
+     *
+     * @param string       $message
+     * @param array|string $raw
+     * @param int          $code
+     */
+    public function __construct($message, $raw = [], $code = self::ERROR_GATEWAY)
+    {
+        parent::__construct('ERROR_GATEWAY: '.$message, $raw, $code);
+    }
+}

+ 19 - 0
addons/epay/library/Yansongda/Pay/Exceptions/InvalidArgumentException.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace Yansongda\Pay\Exceptions;
+
+class InvalidArgumentException extends Exception
+{
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansonga.cn>
+     *
+     * @param string       $message
+     * @param array|string $raw
+     */
+    public function __construct($message, $raw = [])
+    {
+        parent::__construct('INVALID_ARGUMENT: '.$message, $raw, self::INVALID_ARGUMENT);
+    }
+}

+ 19 - 0
addons/epay/library/Yansongda/Pay/Exceptions/InvalidConfigException.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace Yansongda\Pay\Exceptions;
+
+class InvalidConfigException extends Exception
+{
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansonga.cn>
+     *
+     * @param string       $message
+     * @param array|string $raw
+     */
+    public function __construct($message, $raw = [])
+    {
+        parent::__construct('INVALID_CONFIG: '.$message, $raw, self::INVALID_CONFIG);
+    }
+}

+ 19 - 0
addons/epay/library/Yansongda/Pay/Exceptions/InvalidGatewayException.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace Yansongda\Pay\Exceptions;
+
+class InvalidGatewayException extends Exception
+{
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansonga.cn>
+     *
+     * @param string       $message
+     * @param array|string $raw
+     */
+    public function __construct($message, $raw = [])
+    {
+        parent::__construct('INVALID_GATEWAY: '.$message, $raw, self::INVALID_GATEWAY);
+    }
+}

+ 19 - 0
addons/epay/library/Yansongda/Pay/Exceptions/InvalidSignException.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace Yansongda\Pay\Exceptions;
+
+class InvalidSignException extends Exception
+{
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansonga.cn>
+     *
+     * @param string       $message
+     * @param array|string $raw
+     */
+    public function __construct($message, $raw = [])
+    {
+        parent::__construct('INVALID_SIGN: '.$message, $raw, self::INVALID_SIGN);
+    }
+}

+ 422 - 0
addons/epay/library/Yansongda/Pay/Gateways/Alipay.php

@@ -0,0 +1,422 @@
+<?php
+
+namespace Yansongda\Pay\Gateways;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Yansongda\Pay\Contracts\GatewayApplicationInterface;
+use Yansongda\Pay\Contracts\GatewayInterface;
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidConfigException;
+use Yansongda\Pay\Exceptions\InvalidGatewayException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Pay\Gateways\Alipay\Support;
+use Yansongda\Supports\Collection;
+use Yansongda\Supports\Config;
+use Yansongda\Supports\Str;
+
+/**
+ * @method Response   app(array $config)      APP 支付
+ * @method Collection pos(array $config)      刷卡支付
+ * @method Collection scan(array $config)     扫码支付
+ * @method Collection transfer(array $config) 帐户转账
+ * @method Response   wap(array $config)      手机网站支付
+ * @method Response   web(array $config)      电脑支付
+ * @method Collection mini(array $config)     小程序支付
+ */
+class Alipay implements GatewayApplicationInterface
+{
+    /**
+     * Const mode_normal.
+     */
+    const MODE_NORMAL = 'normal';
+
+    /**
+     * Const mode_dev.
+     */
+    const MODE_DEV = 'dev';
+
+    /**
+     * Const mode_service.
+     */
+    const MODE_SERVICE = 'service';
+
+    /**
+     * Const url.
+     */
+    const URL = [
+        self::MODE_NORMAL => 'https://openapi.alipay.com/gateway.do?charset=utf-8',
+        self::MODE_DEV => 'https://openapi.alipaydev.com/gateway.do?charset=utf-8',
+    ];
+
+    /**
+     * Alipay payload.
+     *
+     * @var array
+     */
+    protected $payload;
+
+    /**
+     * Alipay gateway.
+     *
+     * @var string
+     */
+    protected $gateway;
+
+    /**
+     * extends.
+     *
+     * @var array
+     */
+    protected $extends;
+
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws \Exception
+     */
+    public function __construct(Config $config)
+    {
+        $this->gateway = Support::create($config)->getBaseUri();
+        $this->payload = [
+            'app_id' => $config->get('app_id'),
+            'method' => '',
+            'format' => 'JSON',
+            'charset' => 'utf-8',
+            'sign_type' => 'RSA2',
+            'version' => '1.0',
+            'return_url' => $config->get('return_url'),
+            'notify_url' => $config->get('notify_url'),
+            'timestamp' => date('Y-m-d H:i:s'),
+            'sign' => '',
+            'biz_content' => '',
+            'app_auth_token' => $config->get('app_auth_token'),
+        ];
+
+        if ($config->get('app_cert_public_key') && $config->get('alipay_root_cert')) {
+            $this->payload['app_cert_sn'] = Support::getCertSN($config->get('app_cert_public_key'));
+            $this->payload['alipay_root_cert_sn'] = Support::getRootCertSN($config->get('alipay_root_cert'));
+        }
+    }
+
+    /**
+     * Magic pay.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $method
+     * @param array  $params
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidConfigException
+     * @throws InvalidGatewayException
+     * @throws InvalidSignException
+     *
+     * @return Response|Collection
+     */
+    public function __call($method, $params)
+    {
+        if (isset($this->extends[$method])) {
+            return $this->makeExtend($method, ...$params);
+        }
+
+        return $this->pay($method, ...$params);
+    }
+
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $gateway
+     * @param array  $params
+     *
+     * @throws InvalidGatewayException
+     *
+     * @return Response|Collection
+     */
+    public function pay($gateway, $params = [])
+    {
+        Events::dispatch(new Events\PayStarting('Alipay', $gateway, $params));
+
+        $this->payload['return_url'] = $params['return_url'] ?? $this->payload['return_url'];
+        $this->payload['notify_url'] = $params['notify_url'] ?? $this->payload['notify_url'];
+
+        unset($params['return_url'], $params['notify_url']);
+
+        $this->payload['biz_content'] = json_encode($params);
+
+        $gateway = get_class($this).'\\'.Str::studly($gateway).'Gateway';
+
+        if (class_exists($gateway)) {
+            return $this->makePay($gateway);
+        }
+
+        throw new InvalidGatewayException("Pay Gateway [{$gateway}] not exists");
+    }
+
+    /**
+     * Verify sign.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param array|null $data
+     *
+     * @throws InvalidSignException
+     * @throws InvalidConfigException
+     */
+    public function verify($data = null, bool $refund = false): Collection
+    {
+        if (is_null($data)) {
+            $request = Request::createFromGlobals();
+
+            $data = $request->request->count() > 0 ? $request->request->all() : $request->query->all();
+        }
+
+        if (isset($data['fund_bill_list'])) {
+            $data['fund_bill_list'] = htmlspecialchars_decode($data['fund_bill_list']);
+        }
+
+        Events::dispatch(new Events\RequestReceived('Alipay', '', $data));
+
+        if (Support::verifySign($data)) {
+            return new Collection($data);
+        }
+
+        Events::dispatch(new Events\SignFailed('Alipay', '', $data));
+
+        throw new InvalidSignException('Alipay Sign Verify FAILED', $data);
+    }
+
+    /**
+     * Query an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string|array $order
+     *
+     * @throws GatewayException
+     * @throws InvalidConfigException
+     * @throws InvalidSignException
+     */
+    public function find($order, string $type = 'wap'): Collection
+    {
+        $gateway = get_class($this).'\\'.Str::studly($type).'Gateway';
+
+        if (!class_exists($gateway) || !is_callable([new $gateway(), 'find'])) {
+            throw new GatewayException("{$gateway} Done Not Exist Or Done Not Has FIND Method");
+        }
+
+        $config = call_user_func([new $gateway(), 'find'], $order);
+
+        $this->payload['method'] = $config['method'];
+        $this->payload['biz_content'] = $config['biz_content'];
+        $this->payload['sign'] = Support::generateSign($this->payload);
+
+        Events::dispatch(new Events\MethodCalled('Alipay', 'Find', $this->gateway, $this->payload));
+
+        return Support::requestApi($this->payload);
+    }
+
+    /**
+     * Refund an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws GatewayException
+     * @throws InvalidConfigException
+     * @throws InvalidSignException
+     */
+    public function refund(array $order): Collection
+    {
+        $this->payload['method'] = 'alipay.trade.refund';
+        $this->payload['biz_content'] = json_encode($order);
+        $this->payload['sign'] = Support::generateSign($this->payload);
+
+        Events::dispatch(new Events\MethodCalled('Alipay', 'Refund', $this->gateway, $this->payload));
+
+        return Support::requestApi($this->payload);
+    }
+
+    /**
+     * Cancel an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param array|string $order
+     *
+     * @throws GatewayException
+     * @throws InvalidConfigException
+     * @throws InvalidSignException
+     */
+    public function cancel($order): Collection
+    {
+        $this->payload['method'] = 'alipay.trade.cancel';
+        $this->payload['biz_content'] = json_encode(is_array($order) ? $order : ['out_trade_no' => $order]);
+        $this->payload['sign'] = Support::generateSign($this->payload);
+
+        Events::dispatch(new Events\MethodCalled('Alipay', 'Cancel', $this->gateway, $this->payload));
+
+        return Support::requestApi($this->payload);
+    }
+
+    /**
+     * Close an order.
+     *
+     * @param string|array $order
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws GatewayException
+     * @throws InvalidConfigException
+     * @throws InvalidSignException
+     */
+    public function close($order): Collection
+    {
+        $this->payload['method'] = 'alipay.trade.close';
+        $this->payload['biz_content'] = json_encode(is_array($order) ? $order : ['out_trade_no' => $order]);
+        $this->payload['sign'] = Support::generateSign($this->payload);
+
+        Events::dispatch(new Events\MethodCalled('Alipay', 'Close', $this->gateway, $this->payload));
+
+        return Support::requestApi($this->payload);
+    }
+
+    /**
+     * Download bill.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string|array $bill
+     *
+     * @throws GatewayException
+     * @throws InvalidConfigException
+     * @throws InvalidSignException
+     */
+    public function download($bill): string
+    {
+        $this->payload['method'] = 'alipay.data.dataservice.bill.downloadurl.query';
+        $this->payload['biz_content'] = json_encode(is_array($bill) ? $bill : ['bill_type' => 'trade', 'bill_date' => $bill]);
+        $this->payload['sign'] = Support::generateSign($this->payload);
+
+        Events::dispatch(new Events\MethodCalled('Alipay', 'Download', $this->gateway, $this->payload));
+
+        $result = Support::requestApi($this->payload);
+
+        return ($result instanceof Collection) ? $result->get('bill_download_url') : '';
+    }
+
+    /**
+     * Reply success to alipay.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function success(): Response
+    {
+        Events::dispatch(new Events\MethodCalled('Alipay', 'Success', $this->gateway));
+
+        return new Response('success');
+    }
+
+    /**
+     * extend.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws GatewayException
+     * @throws InvalidConfigException
+     * @throws InvalidSignException
+     * @throws InvalidArgumentException
+     */
+    public function extend(string $method, callable $function, bool $now = true): ?Collection
+    {
+        if (!$now && !method_exists($this, $method)) {
+            $this->extends[$method] = $function;
+
+            return null;
+        }
+
+        $customize = $function($this->payload);
+
+        if (!is_array($customize) && !($customize instanceof Collection)) {
+            throw new InvalidArgumentException('Return Type Must Be Array Or Collection');
+        }
+
+        Events::dispatch(new Events\MethodCalled('Alipay', 'extend', $this->gateway, $customize));
+
+        if (is_array($customize)) {
+            $this->payload = $customize;
+            $this->payload['sign'] = Support::generateSign($this->payload);
+
+            return Support::requestApi($this->payload);
+        }
+
+        return $customize;
+    }
+
+    /**
+     * Make pay gateway.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws InvalidGatewayException
+     *
+     * @return Response|Collection
+     */
+    protected function makePay(string $gateway)
+    {
+        $app = new $gateway();
+
+        if ($app instanceof GatewayInterface) {
+            return $app->pay($this->gateway, array_filter($this->payload, function ($value) {
+                return '' !== $value && !is_null($value);
+            }));
+        }
+
+        throw new InvalidGatewayException("Pay Gateway [{$gateway}] Must Be An Instance Of GatewayInterface");
+    }
+
+    /**
+     * makeExtend.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidConfigException
+     * @throws InvalidSignException
+     */
+    protected function makeExtend(string $method, array ...$params): Collection
+    {
+        $params = count($params) >= 1 ? $params[0] : $params;
+
+        $function = $this->extends[$method];
+
+        $customize = $function($this->payload, $params);
+
+        if (!is_array($customize) && !($customize instanceof Collection)) {
+            throw new InvalidArgumentException('Return Type Must Be Array Or Collection');
+        }
+
+        Events::dispatch(new Events\MethodCalled(
+            'Alipay',
+            'extend - '.$method,
+            $this->gateway,
+            is_array($customize) ? $customize : $customize->toArray()
+        ));
+
+        if (is_array($customize)) {
+            $this->payload = $customize;
+            $this->payload['sign'] = Support::generateSign($this->payload);
+
+            return Support::requestApi($this->payload);
+        }
+
+        return $customize;
+    }
+}

+ 38 - 0
addons/epay/library/Yansongda/Pay/Gateways/Alipay/AppGateway.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Alipay;
+
+use Symfony\Component\HttpFoundation\Response;
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidConfigException;
+use Yansongda\Pay\Gateways\Alipay;
+
+class AppGateway extends Gateway
+{
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @throws InvalidConfigException
+     * @throws InvalidArgumentException
+     */
+    public function pay($endpoint, array $payload): Response
+    {
+        $payload['method'] = 'alipay.trade.app.pay';
+
+        $biz_array = json_decode($payload['biz_content'], true);
+        if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) {
+            $biz_array['extend_params'] = is_array($biz_array['extend_params']) ? array_merge(['sys_service_provider_id' => Support::getInstance()->pid], $biz_array['extend_params']) : ['sys_service_provider_id' => Support::getInstance()->pid];
+        }
+        $payload['biz_content'] = json_encode(array_merge($biz_array, ['product_code' => 'QUICK_MSECURITY_PAY']));
+        $payload['sign'] = Support::generateSign($payload);
+
+        Events::dispatch(new Events\PayStarted('Alipay', 'App', $endpoint, $payload));
+
+        return new Response(http_build_query($payload));
+    }
+}

+ 40 - 0
addons/epay/library/Yansongda/Pay/Gateways/Alipay/Gateway.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Alipay;
+
+use Yansongda\Pay\Contracts\GatewayInterface;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Supports\Collection;
+
+abstract class Gateway implements GatewayInterface
+{
+    /**
+     * Mode.
+     *
+     * @var string
+     */
+    protected $mode;
+
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws InvalidArgumentException
+     */
+    public function __construct()
+    {
+        $this->mode = Support::getInstance()->mode;
+    }
+
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @return Collection
+     */
+    abstract public function pay($endpoint, array $payload);
+}

+ 46 - 0
addons/epay/library/Yansongda/Pay/Gateways/Alipay/MiniGateway.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Alipay;
+
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidConfigException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Pay\Gateways\Alipay;
+use Yansongda\Supports\Collection;
+
+class MiniGateway extends Gateway
+{
+    /**
+     * Pay an order.
+     *
+     * @author xiaozan <i@xiaozan.me>
+     *
+     * @param string $endpoint
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidConfigException
+     * @throws InvalidSignException
+     *
+     * @see https://docs.alipay.com/mini/introduce/pay
+     */
+    public function pay($endpoint, array $payload): Collection
+    {
+        $biz_array = json_decode($payload['biz_content'], true);
+        if (empty($biz_array['buyer_id'])) {
+            throw new InvalidArgumentException('buyer_id required');
+        }
+        if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) {
+            $biz_array['extend_params'] = is_array($biz_array['extend_params']) ? array_merge(['sys_service_provider_id' => Support::getInstance()->pid], $biz_array['extend_params']) : ['sys_service_provider_id' => Support::getInstance()->pid];
+        }
+        $payload['biz_content'] = json_encode($biz_array);
+        $payload['method'] = 'alipay.trade.create';
+        $payload['sign'] = Support::generateSign($payload);
+
+        Events::dispatch(new Events\PayStarted('Alipay', 'Mini', $endpoint, $payload));
+
+        return Support::requestApi($payload);
+    }
+}

+ 47 - 0
addons/epay/library/Yansongda/Pay/Gateways/Alipay/PosGateway.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Alipay;
+
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidConfigException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Pay\Gateways\Alipay;
+use Yansongda\Supports\Collection;
+
+class PosGateway extends Gateway
+{
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @throws InvalidArgumentException
+     * @throws GatewayException
+     * @throws InvalidConfigException
+     * @throws InvalidSignException
+     */
+    public function pay($endpoint, array $payload): Collection
+    {
+        $payload['method'] = 'alipay.trade.pay';
+        $biz_array = json_decode($payload['biz_content'], true);
+        if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) {
+            $biz_array['extend_params'] = is_array($biz_array['extend_params']) ? array_merge(['sys_service_provider_id' => Support::getInstance()->pid], $biz_array['extend_params']) : ['sys_service_provider_id' => Support::getInstance()->pid];
+        }
+        $payload['biz_content'] = json_encode(array_merge(
+            $biz_array,
+            [
+                'product_code' => 'FACE_TO_FACE_PAYMENT',
+                'scene' => 'bar_code',
+            ]
+        ));
+        $payload['sign'] = Support::generateSign($payload);
+
+        Events::dispatch(new Events\PayStarted('Alipay', 'Pos', $endpoint, $payload));
+
+        return Support::requestApi($payload);
+    }
+}

+ 21 - 0
addons/epay/library/Yansongda/Pay/Gateways/Alipay/RefundGateway.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Alipay;
+
+class RefundGateway
+{
+    /**
+     * Find.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param $order
+     */
+    public function find($order): array
+    {
+        return [
+            'method' => 'alipay.trade.fastpay.refund.query',
+            'biz_content' => json_encode(is_array($order) ? $order : ['out_trade_no' => $order]),
+        ];
+    }
+}

+ 41 - 0
addons/epay/library/Yansongda/Pay/Gateways/Alipay/ScanGateway.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Alipay;
+
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidConfigException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Pay\Gateways\Alipay;
+use Yansongda\Supports\Collection;
+
+class ScanGateway extends Gateway
+{
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidConfigException
+     * @throws InvalidSignException
+     */
+    public function pay($endpoint, array $payload): Collection
+    {
+        $payload['method'] = 'alipay.trade.precreate';
+        $biz_array = json_decode($payload['biz_content'], true);
+        if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) {
+            $biz_array['extend_params'] = is_array($biz_array['extend_params']) ? array_merge(['sys_service_provider_id' => Support::getInstance()->pid], $biz_array['extend_params']) : ['sys_service_provider_id' => Support::getInstance()->pid];
+        }
+        $payload['biz_content'] = json_encode(array_merge($biz_array, ['product_code' => '']));
+        $payload['sign'] = Support::generateSign($payload);
+
+        Events::dispatch(new Events\PayStarted('Alipay', 'Scan', $endpoint, $payload));
+
+        return Support::requestApi($payload);
+    }
+}

+ 452 - 0
addons/epay/library/Yansongda/Pay/Gateways/Alipay/Support.php

@@ -0,0 +1,452 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Alipay;
+
+use Exception;
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidConfigException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Pay\Gateways\Alipay;
+use Yansongda\Pay\Log;
+use Yansongda\Supports\Arr;
+use Yansongda\Supports\Collection;
+use Yansongda\Supports\Config;
+use Yansongda\Supports\Str;
+use Yansongda\Supports\Traits\HasHttpRequest;
+
+/**
+ * @author yansongda <me@yansongda.cn>
+ *
+ * @property string app_id alipay app_id
+ * @property string ali_public_key
+ * @property string private_key
+ * @property array http http options
+ * @property string mode current mode
+ * @property array log log options
+ * @property string pid ali pid
+ */
+class Support
+{
+    use HasHttpRequest;
+
+    /**
+     * Alipay gateway.
+     *
+     * @var string
+     */
+    protected $baseUri;
+
+    /**
+     * Config.
+     *
+     * @var Config
+     */
+    protected $config;
+
+    /**
+     * Instance.
+     *
+     * @var Support
+     */
+    private static $instance;
+
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    private function __construct(Config $config)
+    {
+        $this->baseUri = Alipay::URL[$config->get('mode', Alipay::MODE_NORMAL)];
+        $this->config = $config;
+
+        $this->setHttpOptions();
+    }
+
+    /**
+     * __get.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param $key
+     *
+     * @return mixed|Config|null
+     */
+    public function __get($key)
+    {
+        return $this->getConfig($key);
+    }
+
+    /**
+     * create.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return Support
+     */
+    public static function create(Config $config)
+    {
+        if ('cli' === php_sapi_name() || !(self::$instance instanceof self)) {
+            self::$instance = new self($config);
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * getInstance.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws InvalidArgumentException
+     *
+     * @return Support
+     */
+    public static function getInstance()
+    {
+        if (is_null(self::$instance)) {
+            throw new InvalidArgumentException('You Should [Create] First Before Using');
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * clear.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function clear()
+    {
+        self::$instance = null;
+    }
+
+    /**
+     * Get Alipay API result.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws GatewayException
+     * @throws InvalidConfigException
+     * @throws InvalidSignException
+     */
+    public static function requestApi(array $data): Collection
+    {
+        Events::dispatch(new Events\ApiRequesting('Alipay', '', self::$instance->getBaseUri(), $data));
+
+        $data = array_filter($data, function ($value) {
+            return ('' == $value || is_null($value)) ? false : true;
+        });
+
+        $result = json_decode(self::$instance->post('', $data), true);
+
+        Events::dispatch(new Events\ApiRequested('Alipay', '', self::$instance->getBaseUri(), $result));
+
+        return self::processingApiResult($data, $result);
+    }
+
+    /**
+     * Generate sign.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws InvalidConfigException
+     */
+    public static function generateSign(array $params): string
+    {
+        $privateKey = self::$instance->private_key;
+
+        if (is_null($privateKey)) {
+            throw new InvalidConfigException('Missing Alipay Config -- [private_key]');
+        }
+
+        if (Str::endsWith($privateKey, '.pem')) {
+            $privateKey = openssl_pkey_get_private(
+                Str::startsWith($privateKey, 'file://') ? $privateKey : 'file://'.$privateKey
+            );
+        } else {
+            $privateKey = "-----BEGIN RSA PRIVATE KEY-----\n".
+                wordwrap($privateKey, 64, "\n", true).
+                "\n-----END RSA PRIVATE KEY-----";
+        }
+
+        openssl_sign(self::getSignContent($params), $sign, $privateKey, OPENSSL_ALGO_SHA256);
+
+        $sign = base64_encode($sign);
+
+        Log::debug('Alipay Generate Sign', [$params, $sign]);
+
+        if (is_resource($privateKey)) {
+            openssl_free_key($privateKey);
+        }
+
+        return $sign;
+    }
+
+    /**
+     * Verify sign.
+     *
+     * @author yansongda <me@yansonga.cn>
+     *
+     * @param bool        $sync
+     * @param string|null $sign
+     *
+     * @throws InvalidConfigException
+     */
+    public static function verifySign(array $data, $sync = false, $sign = null): bool
+    {
+        $publicKey = self::$instance->ali_public_key;
+
+        if (is_null($publicKey)) {
+            throw new InvalidConfigException('Missing Alipay Config -- [ali_public_key]');
+        }
+
+        if (Str::endsWith($publicKey, '.crt')) {
+            $publicKey = file_get_contents($publicKey);
+        } elseif (Str::endsWith($publicKey, '.pem')) {
+            $publicKey = openssl_pkey_get_public(
+                Str::startsWith($publicKey, 'file://') ? $publicKey : 'file://'.$publicKey
+            );
+        } else {
+            $publicKey = "-----BEGIN PUBLIC KEY-----\n".
+                wordwrap($publicKey, 64, "\n", true).
+                "\n-----END PUBLIC KEY-----";
+        }
+
+        $sign = $sign ?? $data['sign'];
+
+        $toVerify = $sync ? json_encode($data, JSON_UNESCAPED_UNICODE) : self::getSignContent($data, true);
+
+        $isVerify = 1 === openssl_verify($toVerify, base64_decode($sign), $publicKey, OPENSSL_ALGO_SHA256);
+
+        if (is_resource($publicKey)) {
+            openssl_free_key($publicKey);
+        }
+
+        return $isVerify;
+    }
+
+    /**
+     * Get signContent that is to be signed.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param bool $verify
+     */
+    public static function getSignContent(array $data, $verify = false): string
+    {
+        ksort($data);
+
+        $stringToBeSigned = '';
+        foreach ($data as $k => $v) {
+            if ($verify && 'sign' != $k && 'sign_type' != $k) {
+                $stringToBeSigned .= $k.'='.$v.'&';
+            }
+            if (!$verify && '' !== $v && !is_null($v) && 'sign' != $k && '@' != substr($v, 0, 1)) {
+                $stringToBeSigned .= $k.'='.$v.'&';
+            }
+        }
+
+        Log::debug('Alipay Generate Sign Content Before Trim', [$data, $stringToBeSigned]);
+
+        return trim($stringToBeSigned, '&');
+    }
+
+    /**
+     * Convert encoding.
+     *
+     * @author yansongda <me@yansonga.cn>
+     *
+     * @param string|array $data
+     * @param string       $to
+     * @param string       $from
+     */
+    public static function encoding($data, $to = 'utf-8', $from = 'gb2312'): array
+    {
+        return Arr::encoding((array) $data, $to, $from);
+    }
+
+    /**
+     * Get service config.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string|null $key
+     * @param mixed|null  $default
+     *
+     * @return mixed|null
+     */
+    public function getConfig($key = null, $default = null)
+    {
+        if (is_null($key)) {
+            return $this->config->all();
+        }
+
+        if ($this->config->has($key)) {
+            return $this->config[$key];
+        }
+
+        return $default;
+    }
+
+    /**
+     * Get Base Uri.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return string
+     */
+    public function getBaseUri()
+    {
+        return $this->baseUri;
+    }
+
+    /**
+     * 生成应用证书SN.
+     *
+     * @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html
+     *
+     * @param $certPath
+     *
+     * @throws /Exception
+     */
+    public static function getCertSN($certPath): string
+    {
+        if (!is_file($certPath)) {
+            throw new Exception('unknown certPath -- [getCertSN]');
+        }
+        $x509data = file_get_contents($certPath);
+        if (false === $x509data) {
+            throw new Exception('Alipay CertSN Error -- [getCertSN]');
+        }
+        openssl_x509_read($x509data);
+        $certdata = openssl_x509_parse($x509data);
+        if (empty($certdata)) {
+            throw new Exception('Alipay openssl_x509_parse Error -- [getCertSN]');
+        }
+        $issuer_arr = [];
+        foreach ($certdata['issuer'] as $key => $val) {
+            $issuer_arr[] = $key.'='.$val;
+        }
+        $issuer = implode(',', array_reverse($issuer_arr));
+        Log::debug('getCertSN:', [$certPath, $issuer, $certdata['serialNumber']]);
+
+        return md5($issuer.$certdata['serialNumber']);
+    }
+
+    /**
+     * 生成支付宝根证书SN.
+     *
+     * @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html
+     *
+     * @param $certPath
+     *
+     * @return string
+     *
+     * @throws /Exception
+     */
+    public static function getRootCertSN($certPath)
+    {
+        if (!is_file($certPath)) {
+            throw new Exception('unknown certPath -- [getRootCertSN]');
+        }
+        $x509data = file_get_contents($certPath);
+        if (false === $x509data) {
+            throw new Exception('Alipay CertSN Error -- [getRootCertSN]');
+        }
+        $kCertificateEnd = '-----END CERTIFICATE-----';
+        $certStrList = explode($kCertificateEnd, $x509data);
+        $md5_arr = [];
+        foreach ($certStrList as $one) {
+            if (!empty(trim($one))) {
+                $_x509data = $one.$kCertificateEnd;
+                openssl_x509_read($_x509data);
+                $_certdata = openssl_x509_parse($_x509data);
+                if (in_array($_certdata['signatureTypeSN'], ['RSA-SHA256', 'RSA-SHA1'])) {
+                    $issuer_arr = [];
+                    foreach ($_certdata['issuer'] as $key => $val) {
+                        $issuer_arr[] = $key.'='.$val;
+                    }
+                    $_issuer = implode(',', array_reverse($issuer_arr));
+                    if (0 === strpos($_certdata['serialNumber'], '0x')) {
+                        $serialNumber = self::bchexdec($_certdata['serialNumber']);
+                    } else {
+                        $serialNumber = $_certdata['serialNumber'];
+                    }
+                    $md5_arr[] = md5($_issuer.$serialNumber);
+                    Log::debug('getRootCertSN Sub:', [$certPath, $_issuer, $serialNumber]);
+                }
+            }
+        }
+
+        return implode('_', $md5_arr);
+    }
+
+    /**
+     * processingApiResult.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param $data
+     * @param $result
+     *
+     * @throws GatewayException
+     * @throws InvalidConfigException
+     * @throws InvalidSignException
+     */
+    protected static function processingApiResult($data, $result): Collection
+    {
+        $method = str_replace('.', '_', $data['method']).'_response';
+
+        if (!isset($result['sign']) || '10000' != $result[$method]['code']) {
+            throw new GatewayException('Get Alipay API Error:'.$result[$method]['msg'].(isset($result[$method]['sub_code']) ? (' - '.$result[$method]['sub_code']) : ''), $result);
+        }
+
+        if (self::verifySign($result[$method], true, $result['sign'])) {
+            return new Collection($result[$method]);
+        }
+
+        Events::dispatch(new Events\SignFailed('Alipay', '', $result));
+
+        throw new InvalidSignException('Alipay Sign Verify FAILED', $result);
+    }
+
+    /**
+     * Set Http options.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    protected function setHttpOptions(): self
+    {
+        if ($this->config->has('http') && is_array($this->config->get('http'))) {
+            $this->config->forget('http.base_uri');
+            $this->httpOptions = $this->config->get('http');
+        }
+
+        return $this;
+    }
+
+    /**
+     * 0x转高精度数字.
+     *
+     * @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html
+     *
+     * @param $hex
+     *
+     * @return int|string
+     */
+    private static function bchexdec($hex)
+    {
+        $dec = 0;
+        $len = strlen($hex);
+        for ($i = 1; $i <= $len; ++$i) {
+            if (ctype_xdigit($hex[$i - 1])) {
+                $dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
+            }
+        }
+
+        return str_replace('.00', '', $dec);
+    }
+}

+ 49 - 0
addons/epay/library/Yansongda/Pay/Gateways/Alipay/TransferGateway.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Alipay;
+
+use Yansongda\Pay\Contracts\GatewayInterface;
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidConfigException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Supports\Collection;
+
+class TransferGateway implements GatewayInterface
+{
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @throws GatewayException
+     * @throws InvalidConfigException
+     * @throws InvalidSignException
+     */
+    public function pay($endpoint, array $payload): Collection
+    {
+        $payload['method'] = 'alipay.fund.trans.uni.transfer';
+        $payload['sign'] = Support::generateSign($payload);
+
+        Events::dispatch(new Events\PayStarted('Alipay', 'Transfer', $endpoint, $payload));
+
+        return Support::requestApi($payload);
+    }
+
+    /**
+     * Find.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param $order
+     */
+    public function find($order): array
+    {
+        return [
+            'method' => 'alipay.fund.trans.order.query',
+            'biz_content' => json_encode(is_array($order) ? $order : ['out_biz_no' => $order]),
+        ];
+    }
+}

+ 26 - 0
addons/epay/library/Yansongda/Pay/Gateways/Alipay/WapGateway.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Alipay;
+
+class WapGateway extends WebGateway
+{
+    /**
+     * Get method config.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    protected function getMethod(): string
+    {
+        return 'alipay.trade.wap.pay';
+    }
+
+    /**
+     * Get productCode config.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    protected function getProductCode(): string
+    {
+        return 'QUICK_WAP_WAY';
+    }
+}

+ 104 - 0
addons/epay/library/Yansongda/Pay/Gateways/Alipay/WebGateway.php

@@ -0,0 +1,104 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Alipay;
+
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Response;
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidConfigException;
+use Yansongda\Pay\Gateways\Alipay;
+
+class WebGateway extends Gateway
+{
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @throws InvalidConfigException
+     * @throws InvalidArgumentException
+     */
+    public function pay($endpoint, array $payload): Response
+    {
+        $biz_array = json_decode($payload['biz_content'], true);
+        $biz_array['product_code'] = $this->getProductCode();
+
+        $method = $biz_array['http_method'] ?? 'POST';
+
+        unset($biz_array['http_method']);
+        if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) {
+            $biz_array['extend_params'] = is_array($biz_array['extend_params']) ? array_merge(['sys_service_provider_id' => Support::getInstance()->pid], $biz_array['extend_params']) : ['sys_service_provider_id' => Support::getInstance()->pid];
+        }
+        $payload['method'] = $this->getMethod();
+        $payload['biz_content'] = json_encode($biz_array);
+        $payload['sign'] = Support::generateSign($payload);
+
+        Events::dispatch(new Events\PayStarted('Alipay', 'Web/Wap', $endpoint, $payload));
+
+        return $this->buildPayHtml($endpoint, $payload, $method);
+    }
+
+    /**
+     * Find.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param $order
+     */
+    public function find($order): array
+    {
+        return [
+            'method' => 'alipay.trade.query',
+            'biz_content' => json_encode(is_array($order) ? $order : ['out_trade_no' => $order]),
+        ];
+    }
+
+    /**
+     * Build Html response.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     * @param array  $payload
+     * @param string $method
+     */
+    protected function buildPayHtml($endpoint, $payload, $method = 'POST'): Response
+    {
+        if ('GET' === strtoupper($method)) {
+            return new RedirectResponse($endpoint.'&'.http_build_query($payload));
+        }
+
+        $sHtml = "<form id='alipay_submit' name='alipay_submit' action='".$endpoint."' method='".$method."'>";
+        foreach ($payload as $key => $val) {
+            $val = str_replace("'", '&apos;', $val);
+            $sHtml .= "<input type='hidden' name='".$key."' value='".$val."'/>";
+        }
+        $sHtml .= "<input type='submit' value='ok' style='display:none;'></form>";
+        $sHtml .= "<script>document.forms['alipay_submit'].submit();</script>";
+
+        return new Response($sHtml);
+    }
+
+    /**
+     * Get method config.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    protected function getMethod(): string
+    {
+        return 'alipay.trade.page.pay';
+    }
+
+    /**
+     * Get productCode config.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    protected function getProductCode(): string
+    {
+        return 'FAST_INSTANT_TRADE_PAY';
+    }
+}

+ 366 - 0
addons/epay/library/Yansongda/Pay/Gateways/Wechat.php

@@ -0,0 +1,366 @@
+<?php
+
+namespace Yansongda\Pay\Gateways;
+
+use Exception;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Yansongda\Pay\Contracts\GatewayApplicationInterface;
+use Yansongda\Pay\Contracts\GatewayInterface;
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidGatewayException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Pay\Gateways\Wechat\Support;
+use Yansongda\Pay\Log;
+use Yansongda\Supports\Collection;
+use Yansongda\Supports\Config;
+use Yansongda\Supports\Str;
+
+/**
+ * @method Response         app(array $config)          APP 支付
+ * @method Collection       groupRedpack(array $config) 分裂红包
+ * @method Collection       miniapp(array $config)      小程序支付
+ * @method Collection       mp(array $config)           公众号支付
+ * @method Collection       pos(array $config)          刷卡支付
+ * @method Collection       redpack(array $config)      普通红包
+ * @method Collection       scan(array $config)         扫码支付
+ * @method Collection       transfer(array $config)     企业付款
+ * @method RedirectResponse web(array $config)          Web 扫码支付
+ * @method RedirectResponse wap(array $config)          H5 支付
+ */
+class Wechat implements GatewayApplicationInterface
+{
+    /**
+     * 普通模式.
+     */
+    const MODE_NORMAL = 'normal';
+
+    /**
+     * 沙箱模式.
+     */
+    const MODE_DEV = 'dev';
+
+    /**
+     * 香港钱包 API.
+     */
+    const MODE_HK = 'hk';
+
+    /**
+     * 境外 API.
+     */
+    const MODE_US = 'us';
+
+    /**
+     * 服务商模式.
+     */
+    const MODE_SERVICE = 'service';
+
+    /**
+     * Const url.
+     */
+    const URL = [
+        self::MODE_NORMAL => 'https://api.mch.weixin.qq.com/',
+        self::MODE_DEV => 'https://api.mch.weixin.qq.com/xdc/apiv2sandbox/',
+        self::MODE_HK => 'https://apihk.mch.weixin.qq.com/',
+        self::MODE_SERVICE => 'https://api.mch.weixin.qq.com/',
+        self::MODE_US => 'https://apius.mch.weixin.qq.com/',
+    ];
+
+    /**
+     * Wechat payload.
+     *
+     * @var array
+     */
+    protected $payload;
+
+    /**
+     * Wechat gateway.
+     *
+     * @var string
+     */
+    protected $gateway;
+
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws Exception
+     */
+    public function __construct(Config $config)
+    {
+        $this->gateway = Support::create($config)->getBaseUri();
+        $this->payload = [
+            'appid' => $config->get('app_id', ''),
+            'mch_id' => $config->get('mch_id', ''),
+            'nonce_str' => Str::random(),
+            'notify_url' => $config->get('notify_url', ''),
+            'sign' => '',
+            'trade_type' => '',
+            'spbill_create_ip' => Request::createFromGlobals()->getClientIp(),
+        ];
+
+        if ($config->get('mode', self::MODE_NORMAL) === static::MODE_SERVICE) {
+            $this->payload = array_merge($this->payload, [
+                'sub_mch_id' => $config->get('sub_mch_id'),
+                'sub_appid' => $config->get('sub_app_id', ''),
+            ]);
+        }
+    }
+
+    /**
+     * Magic pay.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $method
+     * @param string $params
+     *
+     * @throws InvalidGatewayException
+     *
+     * @return Response|Collection
+     */
+    public function __call($method, $params)
+    {
+        return self::pay($method, ...$params);
+    }
+
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $gateway
+     * @param array  $params
+     *
+     * @throws InvalidGatewayException
+     *
+     * @return Response|Collection
+     */
+    public function pay($gateway, $params = [])
+    {
+        Events::dispatch(new Events\PayStarting('Wechat', $gateway, $params));
+
+        $this->payload = array_merge($this->payload, $params);
+
+        $gateway = get_class($this).'\\'.Str::studly($gateway).'Gateway';
+
+        if (class_exists($gateway)) {
+            return $this->makePay($gateway);
+        }
+
+        throw new InvalidGatewayException("Pay Gateway [{$gateway}] Not Exists");
+    }
+
+    /**
+     * Verify data.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string|null $content
+     *
+     * @throws InvalidSignException
+     * @throws InvalidArgumentException
+     */
+    public function verify($content = null, bool $refund = false): Collection
+    {
+        $content = $content ?? Request::createFromGlobals()->getContent();
+
+        Events::dispatch(new Events\RequestReceived('Wechat', '', [$content]));
+
+        $data = Support::fromXml($content);
+        if ($refund) {
+            $decrypt_data = Support::decryptRefundContents($data['req_info']);
+            $data = array_merge(Support::fromXml($decrypt_data), $data);
+        }
+
+        Log::debug('Resolved The Received Wechat Request Data', $data);
+
+        if ($refund || Support::generateSign($data) === $data['sign']) {
+            return new Collection($data);
+        }
+
+        Events::dispatch(new Events\SignFailed('Wechat', '', $data));
+
+        throw new InvalidSignException('Wechat Sign Verify FAILED', $data);
+    }
+
+    /**
+     * Query an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string|array $order
+     *
+     * @throws GatewayException
+     * @throws InvalidSignException
+     * @throws InvalidArgumentException
+     */
+    public function find($order, string $type = 'wap'): Collection
+    {
+        if ('wap' != $type) {
+            unset($this->payload['spbill_create_ip']);
+        }
+
+        $gateway = get_class($this).'\\'.Str::studly($type).'Gateway';
+
+        if (!class_exists($gateway) || !is_callable([new $gateway(), 'find'])) {
+            throw new GatewayException("{$gateway} Done Not Exist Or Done Not Has FIND Method");
+        }
+
+        $config = call_user_func([new $gateway(), 'find'], $order);
+
+        $this->payload = Support::filterPayload($this->payload, $config['order']);
+
+        Events::dispatch(new Events\MethodCalled('Wechat', 'Find', $this->gateway, $this->payload));
+
+        return Support::requestApi(
+            $config['endpoint'],
+            $this->payload,
+            $config['cert']
+        );
+    }
+
+    /**
+     * Refund an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws GatewayException
+     * @throws InvalidSignException
+     * @throws InvalidArgumentException
+     */
+    public function refund(array $order): Collection
+    {
+        $this->payload = Support::filterPayload($this->payload, $order, true);
+
+        Events::dispatch(new Events\MethodCalled('Wechat', 'Refund', $this->gateway, $this->payload));
+
+        return Support::requestApi(
+            'secapi/pay/refund',
+            $this->payload,
+            true
+        );
+    }
+
+    /**
+     * Cancel an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param array $order
+     *
+     * @throws GatewayException
+     * @throws InvalidSignException
+     * @throws InvalidArgumentException
+     */
+    public function cancel($order): Collection
+    {
+        unset($this->payload['spbill_create_ip']);
+
+        $this->payload = Support::filterPayload($this->payload, $order);
+
+        Events::dispatch(new Events\MethodCalled('Wechat', 'Cancel', $this->gateway, $this->payload));
+
+        return Support::requestApi(
+            'secapi/pay/reverse',
+            $this->payload,
+            true
+        );
+    }
+
+    /**
+     * Close an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string|array $order
+     *
+     * @throws GatewayException
+     * @throws InvalidSignException
+     * @throws InvalidArgumentException
+     */
+    public function close($order): Collection
+    {
+        unset($this->payload['spbill_create_ip']);
+
+        $this->payload = Support::filterPayload($this->payload, $order);
+
+        Events::dispatch(new Events\MethodCalled('Wechat', 'Close', $this->gateway, $this->payload));
+
+        return Support::requestApi('pay/closeorder', $this->payload);
+    }
+
+    /**
+     * Echo success to server.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws InvalidArgumentException
+     */
+    public function success(): Response
+    {
+        Events::dispatch(new Events\MethodCalled('Wechat', 'Success', $this->gateway));
+
+        return new Response(
+            Support::toXml(['return_code' => 'SUCCESS', 'return_msg' => 'OK']),
+            200,
+            ['Content-Type' => 'application/xml']
+        );
+    }
+
+    /**
+     * Download the bill.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     */
+    public function download(array $params): string
+    {
+        unset($this->payload['spbill_create_ip']);
+
+        $this->payload = Support::filterPayload($this->payload, $params, true);
+
+        Events::dispatch(new Events\MethodCalled('Wechat', 'Download', $this->gateway, $this->payload));
+
+        $result = Support::getInstance()->post(
+            'pay/downloadbill',
+            Support::getInstance()->toXml($this->payload)
+        );
+
+        if (is_array($result)) {
+            throw new GatewayException('Get Wechat API Error: '.$result['return_msg'], $result);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Make pay gateway.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $gateway
+     *
+     * @throws InvalidGatewayException
+     *
+     * @return Response|Collection
+     */
+    protected function makePay($gateway)
+    {
+        $app = new $gateway();
+
+        if ($app instanceof GatewayInterface) {
+            return $app->pay($this->gateway, array_filter($this->payload, function ($value) {
+                return '' !== $value && !is_null($value);
+            }));
+        }
+
+        throw new InvalidGatewayException("Pay Gateway [{$gateway}] Must Be An Instance Of GatewayInterface");
+    }
+}

+ 62 - 0
addons/epay/library/Yansongda/Pay/Gateways/Wechat/AppGateway.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Wechat;
+
+use Exception;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Response;
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Pay\Gateways\Wechat;
+use Yansongda\Supports\Str;
+
+class AppGateway extends Gateway
+{
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidSignException
+     * @throws Exception
+     */
+    public function pay($endpoint, array $payload): Response
+    {
+        $payload['appid'] = Support::getInstance()->appid;
+        $payload['trade_type'] = $this->getTradeType();
+
+        if (Wechat::MODE_SERVICE === $this->mode) {
+            $payload['sub_appid'] = Support::getInstance()->sub_appid;
+        }
+
+        $pay_request = [
+            'appid' => Wechat::MODE_SERVICE === $this->mode ? $payload['sub_appid'] : $payload['appid'],
+            'partnerid' => Wechat::MODE_SERVICE === $this->mode ? $payload['sub_mch_id'] : $payload['mch_id'],
+            'prepayid' => $this->preOrder($payload)->get('prepay_id'),
+            'timestamp' => strval(time()),
+            'noncestr' => Str::random(),
+            'package' => 'Sign=WXPay',
+        ];
+        $pay_request['sign'] = Support::generateSign($pay_request);
+
+        Events::dispatch(new Events\PayStarted('Wechat', 'App', $endpoint, $pay_request));
+
+        return new JsonResponse($pay_request);
+    }
+
+    /**
+     * Get trade type config.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    protected function getTradeType(): string
+    {
+        return 'APP';
+    }
+}

+ 88 - 0
addons/epay/library/Yansongda/Pay/Gateways/Wechat/Gateway.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Wechat;
+
+use Yansongda\Pay\Contracts\GatewayInterface;
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Supports\Collection;
+
+abstract class Gateway implements GatewayInterface
+{
+    /**
+     * Mode.
+     *
+     * @var string
+     */
+    protected $mode;
+
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws InvalidArgumentException
+     */
+    public function __construct()
+    {
+        $this->mode = Support::getInstance()->mode;
+    }
+
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @return Collection
+     */
+    abstract public function pay($endpoint, array $payload);
+
+    /**
+     * Find.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string|array $order
+     */
+    public function find($order): array
+    {
+        return [
+            'endpoint' => 'pay/orderquery',
+            'order' => is_array($order) ? $order : ['out_trade_no' => $order],
+            'cert' => false,
+        ];
+    }
+
+    /**
+     * Get trade type config.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return string
+     */
+    abstract protected function getTradeType();
+
+    /**
+     * Schedule an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param array $payload
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidSignException
+     */
+    protected function preOrder($payload): Collection
+    {
+        $payload['sign'] = Support::generateSign($payload);
+
+        Events::dispatch(new Events\MethodCalled('Wechat', 'PreOrder', '', $payload));
+
+        return Support::requestApi('pay/unifiedorder', $payload);
+    }
+}

+ 57 - 0
addons/epay/library/Yansongda/Pay/Gateways/Wechat/GroupRedpackGateway.php

@@ -0,0 +1,57 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Wechat;
+
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Pay\Gateways\Wechat;
+use Yansongda\Supports\Collection;
+
+class GroupRedpackGateway extends Gateway
+{
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidSignException
+     */
+    public function pay($endpoint, array $payload): Collection
+    {
+        $payload['wxappid'] = $payload['appid'];
+        $payload['amt_type'] = 'ALL_RAND';
+
+        if (Wechat::MODE_SERVICE === $this->mode) {
+            $payload['msgappid'] = $payload['appid'];
+        }
+
+        unset($payload['appid'], $payload['trade_type'],
+              $payload['notify_url'], $payload['spbill_create_ip']);
+
+        $payload['sign'] = Support::generateSign($payload);
+
+        Events::dispatch(new Events\PayStarted('Wechat', 'Group Redpack', $endpoint, $payload));
+
+        return Support::requestApi(
+            'mmpaymkttransfers/sendgroupredpack',
+            $payload,
+            true
+        );
+    }
+
+    /**
+     * Get trade type config.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    protected function getTradeType(): string
+    {
+        return '';
+    }
+}

+ 35 - 0
addons/epay/library/Yansongda/Pay/Gateways/Wechat/MiniappGateway.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Wechat;
+
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Pay\Gateways\Wechat;
+use Yansongda\Supports\Collection;
+
+class MiniappGateway extends MpGateway
+{
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidSignException
+     */
+    public function pay($endpoint, array $payload): Collection
+    {
+        $payload['appid'] = Support::getInstance()->miniapp_id;
+
+        if (Wechat::MODE_SERVICE === $this->mode) {
+            $payload['sub_appid'] = Support::getInstance()->sub_miniapp_id;
+            $this->payRequestUseSubAppId = true;
+        }
+
+        return parent::pay($endpoint, $payload);
+    }
+}

+ 59 - 0
addons/epay/library/Yansongda/Pay/Gateways/Wechat/MpGateway.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Wechat;
+
+use Exception;
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Supports\Collection;
+use Yansongda\Supports\Str;
+
+class MpGateway extends Gateway
+{
+    /**
+     * @var bool
+     */
+    protected $payRequestUseSubAppId = false;
+
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidSignException
+     * @throws Exception
+     */
+    public function pay($endpoint, array $payload): Collection
+    {
+        $payload['trade_type'] = $this->getTradeType();
+
+        $pay_request = [
+            'appId' => !$this->payRequestUseSubAppId ? $payload['appid'] : $payload['sub_appid'],
+            'timeStamp' => strval(time()),
+            'nonceStr' => Str::random(),
+            'package' => 'prepay_id='.$this->preOrder($payload)->get('prepay_id'),
+            'signType' => 'MD5',
+        ];
+        $pay_request['paySign'] = Support::generateSign($pay_request);
+
+        Events::dispatch(new Events\PayStarted('Wechat', 'JSAPI', $endpoint, $pay_request));
+
+        return new Collection($pay_request);
+    }
+
+    /**
+     * Get trade type config.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    protected function getTradeType(): string
+    {
+        return 'JSAPI';
+    }
+}

+ 44 - 0
addons/epay/library/Yansongda/Pay/Gateways/Wechat/PosGateway.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Wechat;
+
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Supports\Collection;
+
+class PosGateway extends Gateway
+{
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidSignException
+     */
+    public function pay($endpoint, array $payload): Collection
+    {
+        unset($payload['trade_type'], $payload['notify_url']);
+
+        $payload['sign'] = Support::generateSign($payload);
+
+        Events::dispatch(new Events\PayStarted('Wechat', 'Pos', $endpoint, $payload));
+
+        return Support::requestApi('pay/micropay', $payload);
+    }
+
+    /**
+     * Get trade type config.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    protected function getTradeType(): string
+    {
+        return 'MICROPAY';
+    }
+}

+ 61 - 0
addons/epay/library/Yansongda/Pay/Gateways/Wechat/RedpackGateway.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Wechat;
+
+use Symfony\Component\HttpFoundation\Request;
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Pay\Gateways\Wechat;
+use Yansongda\Supports\Collection;
+
+class RedpackGateway extends Gateway
+{
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidSignException
+     */
+    public function pay($endpoint, array $payload): Collection
+    {
+        $payload['wxappid'] = $payload['appid'];
+
+        if ('cli' !== php_sapi_name()) {
+            $payload['client_ip'] = Request::createFromGlobals()->server->get('SERVER_ADDR');
+        }
+
+        if (Wechat::MODE_SERVICE === $this->mode) {
+            $payload['msgappid'] = $payload['appid'];
+        }
+
+        unset($payload['appid'], $payload['trade_type'],
+              $payload['notify_url'], $payload['spbill_create_ip']);
+
+        $payload['sign'] = Support::generateSign($payload);
+
+        Events::dispatch(new Events\PayStarted('Wechat', 'Redpack', $endpoint, $payload));
+
+        return Support::requestApi(
+            'mmpaymkttransfers/sendredpack',
+            $payload,
+            true
+        );
+    }
+
+    /**
+     * Get trade type config.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    protected function getTradeType(): string
+    {
+        return '';
+    }
+}

+ 50 - 0
addons/epay/library/Yansongda/Pay/Gateways/Wechat/RefundGateway.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Wechat;
+
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+
+class RefundGateway extends Gateway
+{
+    /**
+     * Find.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param $order
+     */
+    public function find($order): array
+    {
+        return [
+            'endpoint' => 'pay/refundquery',
+            'order' => is_array($order) ? $order : ['out_trade_no' => $order],
+            'cert' => false,
+        ];
+    }
+
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @throws InvalidArgumentException
+     */
+    public function pay($endpoint, array $payload)
+    {
+        throw new InvalidArgumentException('Not Support Refund In Pay');
+    }
+
+    /**
+     * Get trade type config.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws InvalidArgumentException
+     */
+    protected function getTradeType()
+    {
+        throw new InvalidArgumentException('Not Support Refund In Pay');
+    }
+}

+ 44 - 0
addons/epay/library/Yansongda/Pay/Gateways/Wechat/ScanGateway.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Wechat;
+
+use Symfony\Component\HttpFoundation\Request;
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Supports\Collection;
+
+class ScanGateway extends Gateway
+{
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidSignException
+     */
+    public function pay($endpoint, array $payload): Collection
+    {
+        $payload['spbill_create_ip'] = Request::createFromGlobals()->server->get('SERVER_ADDR');
+        $payload['trade_type'] = $this->getTradeType();
+
+        Events::dispatch(new Events\PayStarted('Wechat', 'Scan', $endpoint, $payload));
+
+        return $this->preOrder($payload);
+    }
+
+    /**
+     * Get trade type config.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    protected function getTradeType(): string
+    {
+        return 'NATIVE';
+    }
+}

+ 453 - 0
addons/epay/library/Yansongda/Pay/Gateways/Wechat/Support.php

@@ -0,0 +1,453 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Wechat;
+
+use Exception;
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\BusinessException;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Pay\Gateways\Wechat;
+use Yansongda\Pay\Log;
+use Yansongda\Supports\Collection;
+use Yansongda\Supports\Config;
+use Yansongda\Supports\Str;
+use Yansongda\Supports\Traits\HasHttpRequest;
+
+/**
+ * @author yansongda <me@yansongda.cn>
+ *
+ * @property string appid
+ * @property string app_id
+ * @property string miniapp_id
+ * @property string sub_appid
+ * @property string sub_app_id
+ * @property string sub_miniapp_id
+ * @property string mch_id
+ * @property string sub_mch_id
+ * @property string key
+ * @property string return_url
+ * @property string cert_client
+ * @property string cert_key
+ * @property array log
+ * @property array http
+ * @property string mode
+ */
+class Support
+{
+    use HasHttpRequest;
+
+    /**
+     * Wechat gateway.
+     *
+     * @var string
+     */
+    protected $baseUri;
+
+    /**
+     * Config.
+     *
+     * @var Config
+     */
+    protected $config;
+
+    /**
+     * Instance.
+     *
+     * @var Support
+     */
+    private static $instance;
+
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    private function __construct(Config $config)
+    {
+        $this->baseUri = Wechat::URL[$config->get('mode', Wechat::MODE_NORMAL)];
+        $this->config = $config;
+
+        $this->setHttpOptions();
+    }
+
+    /**
+     * __get.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param $key
+     *
+     * @return mixed|Config|null
+     */
+    public function __get($key)
+    {
+        return $this->getConfig($key);
+    }
+
+    /**
+     * create.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidSignException
+     *
+     * @return Support
+     */
+    public static function create(Config $config)
+    {
+        if ('cli' === php_sapi_name() || !(self::$instance instanceof self)) {
+            self::$instance = new self($config);
+
+            self::setDevKey();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * getInstance.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws InvalidArgumentException
+     *
+     * @return Support
+     */
+    public static function getInstance()
+    {
+        if (is_null(self::$instance)) {
+            throw new InvalidArgumentException('You Should [Create] First Before Using');
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * clear.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public static function clear()
+    {
+        self::$instance = null;
+    }
+
+    /**
+     * Request wechat api.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     * @param array  $data
+     * @param bool   $cert
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidSignException
+     */
+    public static function requestApi($endpoint, $data, $cert = false): Collection
+    {
+        Events::dispatch(new Events\ApiRequesting('Wechat', '', self::$instance->getBaseUri().$endpoint, $data));
+
+        $result = self::$instance->post(
+            $endpoint,
+            self::toXml($data),
+            $cert ? [
+                'cert' => self::$instance->cert_client,
+                'ssl_key' => self::$instance->cert_key,
+            ] : [
+                'headers' => [
+                    'Content-Type'=>'application/xml',
+                ],
+            ]
+        );
+        $result = is_array($result) ? $result : self::fromXml($result);
+
+        Events::dispatch(new Events\ApiRequested('Wechat', '', self::$instance->getBaseUri().$endpoint, $result));
+
+        return self::processingApiResult($endpoint, $result);
+    }
+
+    /**
+     * Filter payload.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param array        $payload
+     * @param array|string $params
+     * @param bool         $preserve_notify_url
+     *
+     * @throws InvalidArgumentException
+     */
+    public static function filterPayload($payload, $params, $preserve_notify_url = false): array
+    {
+        $type = self::getTypeName($params['type'] ?? '');
+
+        $payload = array_merge(
+            $payload,
+            is_array($params) ? $params : ['out_trade_no' => $params]
+        );
+        $payload['appid'] = self::$instance->getConfig($type, '');
+
+        if (Wechat::MODE_SERVICE === self::$instance->getConfig('mode', Wechat::MODE_NORMAL)) {
+            $payload['sub_appid'] = self::$instance->getConfig('sub_'.$type, '');
+        }
+
+        unset($payload['trade_type'], $payload['type']);
+        if (!$preserve_notify_url) {
+            unset($payload['notify_url']);
+        }
+
+        $payload['sign'] = self::generateSign($payload);
+
+        return $payload;
+    }
+
+    /**
+     * Generate wechat sign.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param array $data
+     *
+     * @throws InvalidArgumentException
+     */
+    public static function generateSign($data): string
+    {
+        $key = self::$instance->key;
+
+        if (is_null($key)) {
+            throw new InvalidArgumentException('Missing Wechat Config -- [key]');
+        }
+
+        ksort($data);
+
+        $string = md5(self::getSignContent($data).'&key='.$key);
+
+        Log::debug('Wechat Generate Sign Before UPPER', [$data, $string]);
+
+        return strtoupper($string);
+    }
+
+    /**
+     * Generate sign content.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param array $data
+     */
+    public static function getSignContent($data): string
+    {
+        $buff = '';
+
+        foreach ($data as $k => $v) {
+            $buff .= ('sign' != $k && '' != $v && !is_array($v)) ? $k.'='.$v.'&' : '';
+        }
+
+        Log::debug('Wechat Generate Sign Content Before Trim', [$data, $buff]);
+
+        return trim($buff, '&');
+    }
+
+    /**
+     * Decrypt refund contents.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $contents
+     */
+    public static function decryptRefundContents($contents): string
+    {
+        return openssl_decrypt(
+            base64_decode($contents),
+            'AES-256-ECB',
+            md5(self::$instance->key),
+            OPENSSL_RAW_DATA
+        );
+    }
+
+    /**
+     * Convert array to xml.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param array $data
+     *
+     * @throws InvalidArgumentException
+     */
+    public static function toXml($data): string
+    {
+        if (!is_array($data) || count($data) <= 0) {
+            throw new InvalidArgumentException('Convert To Xml Error! Invalid Array!');
+        }
+
+        $xml = '<xml>';
+        foreach ($data as $key => $val) {
+            $xml .= is_numeric($val) ? '<'.$key.'>'.$val.'</'.$key.'>' :
+                                       '<'.$key.'><![CDATA['.$val.']]></'.$key.'>';
+        }
+        $xml .= '</xml>';
+
+        return $xml;
+    }
+
+    /**
+     * Convert xml to array.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $xml
+     *
+     * @throws InvalidArgumentException
+     */
+    public static function fromXml($xml): array
+    {
+        if (!$xml) {
+            throw new InvalidArgumentException('Convert To Array Error! Invalid Xml!');
+        }
+
+        libxml_disable_entity_loader(true);
+
+        return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true);
+    }
+
+    /**
+     * Get service config.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string|null $key
+     * @param mixed|null  $default
+     *
+     * @return mixed|null
+     */
+    public function getConfig($key = null, $default = null)
+    {
+        if (is_null($key)) {
+            return $this->config->all();
+        }
+
+        if ($this->config->has($key)) {
+            return $this->config[$key];
+        }
+
+        return $default;
+    }
+
+    /**
+     * Get app id according to param type.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $type
+     */
+    public static function getTypeName($type = ''): string
+    {
+        switch ($type) {
+            case '':
+                $type = 'app_id';
+                break;
+            case 'app':
+                $type = 'appid';
+                break;
+            default:
+                $type = $type.'_id';
+        }
+
+        return $type;
+    }
+
+    /**
+     * Get Base Uri.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return string
+     */
+    public function getBaseUri()
+    {
+        return $this->baseUri;
+    }
+
+    /**
+     * processingApiResult.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param $endpoint
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidSignException
+     *
+     * @return Collection
+     */
+    protected static function processingApiResult($endpoint, array $result)
+    {
+        if (!isset($result['return_code']) || 'SUCCESS' != $result['return_code']) {
+            throw new GatewayException('Get Wechat API Error:'.($result['return_msg'] ?? $result['retmsg'] ?? ''), $result);
+        }
+
+        if (isset($result['result_code']) && 'SUCCESS' != $result['result_code']) {
+            throw new BusinessException('Wechat Business Error: '.$result['err_code'].' - '.$result['err_code_des'], $result);
+        }
+
+        if (false !== strpos($endpoint, 'xdc/apiv2getsignkey') ||
+            false !== strpos($endpoint, 'mmpaymkttransfers') ||
+            self::generateSign($result) === $result['sign']) {
+            return new Collection($result);
+        }
+
+        Events::dispatch(new Events\SignFailed('Wechat', '', $result));
+
+        throw new InvalidSignException('Wechat Sign Verify FAILED', $result);
+    }
+
+    /**
+     * setDevKey.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidSignException
+     * @throws Exception
+     *
+     * @return Support
+     */
+    private static function setDevKey()
+    {
+        if (Wechat::MODE_DEV == self::$instance->mode) {
+            $data = [
+                'mch_id' => self::$instance->mch_id,
+                'nonce_str' => Str::random(),
+            ];
+            $data['sign'] = self::generateSign($data);
+
+            $result = self::requestApi('https://api.mch.weixin.qq.com/xdc/apiv2getsignkey/sign/getsignkey', $data);
+
+            self::$instance->config->set('key', $result['sandbox_signkey']);
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * Set Http options.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    private function setHttpOptions(): self
+    {
+        if ($this->config->has('http') && is_array($this->config->get('http'))) {
+            $this->config->forget('http.base_uri');
+            $this->httpOptions = $this->config->get('http');
+        }
+
+        return $this;
+    }
+}

+ 80 - 0
addons/epay/library/Yansongda/Pay/Gateways/Wechat/TransferGateway.php

@@ -0,0 +1,80 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Wechat;
+
+use Symfony\Component\HttpFoundation\Request;
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Pay\Gateways\Wechat;
+use Yansongda\Supports\Collection;
+
+class TransferGateway extends Gateway
+{
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidSignException
+     */
+    public function pay($endpoint, array $payload): Collection
+    {
+        if (Wechat::MODE_SERVICE === $this->mode) {
+            unset($payload['sub_mch_id'], $payload['sub_appid']);
+        }
+
+        $type = Support::getTypeName($payload['type'] ?? '');
+
+        $payload['mch_appid'] = Support::getInstance()->getConfig($type, '');
+        $payload['mchid'] = $payload['mch_id'];
+
+        if ('cli' !== php_sapi_name() && !isset($payload['spbill_create_ip'])) {
+            $payload['spbill_create_ip'] = Request::createFromGlobals()->server->get('SERVER_ADDR');
+        }
+
+        unset($payload['appid'], $payload['mch_id'], $payload['trade_type'],
+            $payload['notify_url'], $payload['type']);
+
+        $payload['sign'] = Support::generateSign($payload);
+
+        Events::dispatch(new Events\PayStarted('Wechat', 'Transfer', $endpoint, $payload));
+
+        return Support::requestApi(
+            'mmpaymkttransfers/promotion/transfers',
+            $payload,
+            true
+        );
+    }
+
+    /**
+     * Find.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param $order
+     */
+    public function find($order): array
+    {
+        return [
+            'endpoint' => 'mmpaymkttransfers/gettransferinfo',
+            'order' => is_array($order) ? $order : ['partner_trade_no' => $order],
+            'cert' => true,
+        ];
+    }
+
+    /**
+     * Get trade type config.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    protected function getTradeType(): string
+    {
+        return '';
+    }
+}

+ 47 - 0
addons/epay/library/Yansongda/Pay/Gateways/Wechat/WapGateway.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Wechat;
+
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+
+class WapGateway extends Gateway
+{
+    /**
+     * Pay an order.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $endpoint
+     *
+     * @throws GatewayException
+     * @throws InvalidArgumentException
+     * @throws InvalidSignException
+     */
+    public function pay($endpoint, array $payload): RedirectResponse
+    {
+        $payload['trade_type'] = $this->getTradeType();
+
+        Events::dispatch(new Events\PayStarted('Wechat', 'Wap', $endpoint, $payload));
+
+        $mweb_url = $this->preOrder($payload)->get('mweb_url');
+
+        $url = is_null(Support::getInstance()->return_url) ? $mweb_url : $mweb_url.
+                        '&redirect_url='.urlencode(Support::getInstance()->return_url);
+
+        return new RedirectResponse($url);
+    }
+
+    /**
+     * Get trade type config.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    protected function getTradeType(): string
+    {
+        return 'MWEB';
+    }
+}

+ 86 - 0
addons/epay/library/Yansongda/Pay/Gateways/Wechat/WebGateway.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace Yansongda\Pay\Gateways\Wechat;
+
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\Request;
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Exceptions\GatewayException;
+use Yansongda\Pay\Exceptions\InvalidArgumentException;
+use Yansongda\Pay\Exceptions\InvalidSignException;
+use Yansongda\Supports\Collection;
+
+class WebGateway extends Gateway
+{
+    /**
+     * Pay an order.
+     *
+     * @param string $endpoint
+     * @param array  $payload
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     */
+    public function pay($endpoint, array $payload): Response
+    {
+        $payload['spbill_create_ip'] = Request::createFromGlobals()->server->get('SERVER_ADDR');
+        $payload['trade_type'] = $this->getTradeType();
+
+        $code_url = $this->preOrder($payload)['code_url'];
+        $params = [
+            'body'         => $payload['body'],
+            'code_url'     => $code_url,
+            'out_trade_no' => $payload['out_trade_no'],
+            'return_url'   => Support::getInstance()->return_url,
+            'total_fee'    => $payload['total_fee'],
+        ];
+
+        $params['sign'] = md5(implode('', $params) . Support::getInstance()->app_id);
+        $endpoint = addon_url("epay/api/wechat");
+
+        Events::dispatch(new Events\PayStarted('Wechat', 'Web/Wap', $endpoint, $payload));
+
+        return $this->buildPayHtml($endpoint, $params);
+    }
+
+    /**
+     * Build Html response.
+     *
+     * @param string $endpoint
+     * @param array  $payload
+     * @param string $method
+     *
+     * @return Response
+     * @author yansongda <me@yansongda.cn>
+     *
+     */
+    protected function buildPayHtml($endpoint, $payload, $method = 'POST'): Response
+    {
+        if (strtoupper($method) === 'GET') {
+            return RedirectResponse::create($endpoint . '?' . http_build_query($payload));
+        }
+
+        $sHtml = "<form id='wechat_submit' name='wechat_submit' action='" . $endpoint . "' method='" . $method . "'>";
+        foreach ($payload as $key => $val) {
+            $val = str_replace("'", '&apos;', $val);
+            $sHtml .= "<input type='hidden' name='" . $key . "' value='" . $val . "'/>";
+        }
+        $sHtml .= "<input type='submit' value='ok' style='display:none;'></form>";
+        $sHtml .= "<script>document.forms['wechat_submit'].submit();</script>";
+
+        return Response::create($sHtml);
+    }
+
+    /**
+     * Get trade type config.
+     *
+     * @return string
+     * @author yansongda <me@yansongda.cn>
+     *
+     */
+    protected function getTradeType(): string
+    {
+        return 'NATIVE';
+    }
+}

+ 20 - 0
addons/epay/library/Yansongda/Pay/LICENSE

@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 yansongda <me@yansongda.cn>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 114 - 0
addons/epay/library/Yansongda/Pay/Listeners/KernelLogSubscriber.php

@@ -0,0 +1,114 @@
+<?php
+
+namespace Yansongda\Pay\Listeners;
+
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Yansongda\Pay\Events;
+use Yansongda\Pay\Log;
+
+class KernelLogSubscriber implements EventSubscriberInterface
+{
+    /**
+     * Returns an array of event names this subscriber wants to listen to.
+     *
+     * The array keys are event names and the value can be:
+     *
+     *  * The method name to call (priority defaults to 0)
+     *  * An array composed of the method name to call and the priority
+     *  * An array of arrays composed of the method names to call and respective
+     *    priorities, or 0 if unset
+     *
+     * For instance:
+     *
+     *  * array('eventName' => 'methodName')
+     *  * array('eventName' => array('methodName', $priority))
+     *  * array('eventName' => array(array('methodName1', $priority), array('methodName2')))
+     *
+     * @return array The event names to listen to
+     */
+    public static function getSubscribedEvents()
+    {
+        return [
+            Events\PayStarting::class => ['writePayStartingLog', 256],
+            Events\PayStarted::class => ['writePayStartedLog', 256],
+            Events\ApiRequesting::class => ['writeApiRequestingLog', 256],
+            Events\ApiRequested::class => ['writeApiRequestedLog', 256],
+            Events\SignFailed::class => ['writeSignFailedLog', 256],
+            Events\RequestReceived::class => ['writeRequestReceivedLog', 256],
+            Events\MethodCalled::class => ['writeMethodCalledLog', 256],
+        ];
+    }
+
+    /**
+     * writePayStartingLog.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function writePayStartingLog(Events\PayStarting $event)
+    {
+        Log::debug("Starting To {$event->driver}", [$event->gateway, $event->params]);
+    }
+
+    /**
+     * writePayStartedLog.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function writePayStartedLog(Events\PayStarted $event)
+    {
+        Log::info(
+            "{$event->driver} {$event->gateway} Has Started",
+            [$event->endpoint, $event->payload]
+        );
+    }
+
+    /**
+     * writeApiRequestingLog.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function writeApiRequestingLog(Events\ApiRequesting $event)
+    {
+        Log::debug("Requesting To {$event->driver} Api", [$event->endpoint, $event->payload]);
+    }
+
+    /**
+     * writeApiRequestedLog.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function writeApiRequestedLog(Events\ApiRequested $event)
+    {
+        Log::debug("Result Of {$event->driver} Api", $event->result);
+    }
+
+    /**
+     * writeSignFailedLog.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function writeSignFailedLog(Events\SignFailed $event)
+    {
+        Log::warning("{$event->driver} Sign Verify FAILED", $event->data);
+    }
+
+    /**
+     * writeRequestReceivedLog.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function writeRequestReceivedLog(Events\RequestReceived $event)
+    {
+        Log::info("Received {$event->driver} Request", $event->data);
+    }
+
+    /**
+     * writeMethodCalledLog.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function writeMethodCalledLog(Events\MethodCalled $event)
+    {
+        Log::info("{$event->driver} {$event->gateway} Method Has Called", [$event->endpoint, $event->payload]);
+    }
+}

+ 49 - 0
addons/epay/library/Yansongda/Pay/Log.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace Yansongda\Pay;
+
+use Yansongda\Supports\Log as BaseLog;
+
+/**
+ * @method static void emergency($message, array $context = array())
+ * @method static void alert($message, array $context = array())
+ * @method static void critical($message, array $context = array())
+ * @method static void error($message, array $context = array())
+ * @method static void warning($message, array $context = array())
+ * @method static void notice($message, array $context = array())
+ * @method static void info($message, array $context = array())
+ * @method static void debug($message, array $context = array())
+ * @method static void log($message, array $context = array())
+ */
+class Log
+{
+    /**
+     * Forward call.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $method
+     * @param array  $args
+     *
+     * @return mixed
+     */
+    public static function __callStatic($method, $args)
+    {
+        return forward_static_call_array([BaseLog::class, $method], $args);
+    }
+
+    /**
+     * Forward call.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $method
+     * @param array  $args
+     *
+     * @return mixed
+     */
+    public function __call($method, $args)
+    {
+        return call_user_func_array([BaseLog::class, $method], $args);
+    }
+}

+ 131 - 0
addons/epay/library/Yansongda/Pay/Pay.php

@@ -0,0 +1,131 @@
+<?php
+
+namespace Yansongda\Pay;
+
+use Exception;
+use Yansongda\Pay\Contracts\GatewayApplicationInterface;
+use Yansongda\Pay\Exceptions\InvalidGatewayException;
+use Yansongda\Pay\Gateways\Alipay;
+use Yansongda\Pay\Gateways\Wechat;
+use Yansongda\Pay\Listeners\KernelLogSubscriber;
+use Yansongda\Supports\Config;
+use Yansongda\Supports\Log;
+use Yansongda\Supports\Logger;
+use Yansongda\Supports\Str;
+
+/**
+ * @method static Alipay alipay(array $config) 支付宝
+ * @method static Wechat wechat(array $config) 微信
+ */
+class Pay
+{
+    /**
+     * Config.
+     *
+     * @var Config
+     */
+    protected $config;
+
+    /**
+     * Bootstrap.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws Exception
+     */
+    public function __construct(array $config)
+    {
+        $this->config = new Config($config);
+
+        $this->registerLogService();
+        $this->registerEventService();
+    }
+
+    /**
+     * Magic static call.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $method
+     * @param array  $params
+     *
+     * @throws InvalidGatewayException
+     * @throws Exception
+     */
+    public static function __callStatic($method, $params): GatewayApplicationInterface
+    {
+        $app = new self(...$params);
+
+        return $app->create($method);
+    }
+
+    /**
+     * Create a instance.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $method
+     *
+     * @throws InvalidGatewayException
+     */
+    protected function create($method): GatewayApplicationInterface
+    {
+        $gateway = __NAMESPACE__.'\\Gateways\\'.Str::studly($method);
+
+        if (class_exists($gateway)) {
+            return self::make($gateway);
+        }
+
+        throw new InvalidGatewayException("Gateway [{$method}] Not Exists");
+    }
+
+    /**
+     * Make a gateway.
+     *
+     * @author yansongda <me@yansonga.cn>
+     *
+     * @param string $gateway
+     *
+     * @throws InvalidGatewayException
+     */
+    protected function make($gateway): GatewayApplicationInterface
+    {
+        $app = new $gateway($this->config);
+
+        if ($app instanceof GatewayApplicationInterface) {
+            return $app;
+        }
+
+        throw new InvalidGatewayException("Gateway [{$gateway}] Must Be An Instance Of GatewayApplicationInterface");
+    }
+
+    /**
+     * Register log service.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws Exception
+     */
+    protected function registerLogService()
+    {
+        $config = $this->config->get('log');
+        $config['identify'] = 'yansongda.pay';
+
+        $logger = new Logger();
+        $logger->setConfig($config);
+
+        Log::setInstance($logger);
+    }
+
+    /**
+     * Register event service.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    protected function registerEventService()
+    {
+        Events::setDispatcher(Events::createDispatcher());
+
+        Events::addSubscriber(new KernelLogSubscriber());
+    }
+}

+ 605 - 0
addons/epay/library/Yansongda/Supports/Arr.php

@@ -0,0 +1,605 @@
+<?php
+
+namespace Yansongda\Supports;
+
+use ArrayAccess;
+
+/**
+ * Array helper from Illuminate\Support\Arr.
+ */
+class Arr
+{
+    /**
+     * Determine whether the given value is array accessible.
+     *
+     * @param mixed $value
+     */
+    public static function accessible($value): bool
+    {
+        return is_array($value) || $value instanceof ArrayAccess;
+    }
+
+    /**
+     * Add an element to an array using "dot" notation if it doesn't exist.
+     *
+     * @param mixed $value
+     */
+    public static function add(array $array, string $key, $value): array
+    {
+        if (is_null(static::get($array, $key))) {
+            static::set($array, $key, $value);
+        }
+
+        return $array;
+    }
+
+    /**
+     * Build a new array using a callback.
+     */
+    public static function build(array $array, callable $callback): array
+    {
+        $results = [];
+
+        foreach ($array as $key => $value) {
+            [$innerKey, $innerValue] = call_user_func($callback, $key, $value);
+            $results[$innerKey] = $innerValue;
+        }
+
+        return $results;
+    }
+
+    /**
+     * Divide an array into two arrays. One with keys and the other with values.
+     */
+    public static function divide(array $array): array
+    {
+        return [
+                array_keys($array),
+                array_values($array),
+               ];
+    }
+
+    /**
+     * Flatten a multi-dimensional associative array with dots.
+     */
+    public static function dot(array $array, string $prepend = ''): array
+    {
+        $results = [];
+
+        foreach ($array as $key => $value) {
+            if (is_array($value)) {
+                $results = array_merge($results, static::dot($value, $prepend.$key.'.'));
+            } else {
+                $results[$prepend.$key] = $value;
+            }
+        }
+
+        return $results;
+    }
+
+    /**
+     * Get all of the given array except for a specified array of items.
+     *
+     * @param array|string $keys
+     */
+    public static function except(array $array, $keys): array
+    {
+        return array_diff_key($array, array_flip((array) $keys));
+    }
+
+    /**
+     * access array.
+     *
+     * if not array access, return original.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param mixed $data
+     *
+     * @return mixed
+     */
+    public static function access($data)
+    {
+        if (!self::accessible($data) &&
+            !(is_object($data) && method_exists($data, 'toArray'))) {
+            return $data;
+        }
+
+        return is_object($data) ? $data->toArray() : $data;
+    }
+
+    /**
+     * Determine if the given key exists in the provided array.
+     *
+     * @param \ArrayAccess|array $array
+     * @param string|int         $key
+     *
+     * @return bool
+     */
+    public static function exists($array, $key)
+    {
+        $array = self::access($array);
+
+        if ($array instanceof ArrayAccess) {
+            return $array->offsetExists($key);
+        }
+
+        return array_key_exists($key, $array);
+    }
+
+    /**
+     * Check if an item or items exist in an array using "dot" notation.
+     *
+     * @param \ArrayAccess|array $array
+     * @param string|array       $keys
+     *
+     * @return bool
+     */
+    public static function has($array, $keys)
+    {
+        $array = self::access($array);
+
+        $keys = (array) $keys;
+
+        if (!$array || $keys === []) {
+            return false;
+        }
+
+        foreach ($keys as $key) {
+            $subKeyArray = $array;
+
+            if (static::exists($array, $key)) {
+                continue;
+            }
+
+            foreach (explode('.', $key) as $segment) {
+                if (static::accessible($subKeyArray) && static::exists($subKeyArray, $segment)) {
+                    $subKeyArray = $subKeyArray[$segment];
+                } else {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Determine if any of the keys exist in an array using "dot" notation.
+     *
+     * @param \ArrayAccess|array $array
+     * @param string|array       $keys
+     *
+     * @return bool
+     */
+    public static function hasAny($array, $keys)
+    {
+        $array = self::access($array);
+
+        if (is_null($keys)) {
+            return false;
+        }
+
+        $keys = (array) $keys;
+
+        if (!$array) {
+            return false;
+        }
+
+        if ($keys === []) {
+            return false;
+        }
+
+        foreach ($keys as $key) {
+            if (static::has($array, $key)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Fetch a flattened array of a nested array element.
+     */
+    public static function fetch(array $array, string $key): array
+    {
+        $results = [];
+
+        foreach (explode('.', $key) as $segment) {
+            $results = [];
+            foreach ($array as $value) {
+                $value = (array) $value;
+                $results[] = $value[$segment];
+            }
+            $array = array_values($results);
+        }
+
+        return array_values($results);
+    }
+
+    /**
+     * Return the first element in an array passing a given truth test.
+     *
+     * @param mixed $default
+     *
+     * @return mixed
+     */
+    public static function first(array $array, callable $callback, $default = null)
+    {
+        foreach ($array as $key => $value) {
+            if (call_user_func($callback, $key, $value)) {
+                return $value;
+            }
+        }
+
+        return $default;
+    }
+
+    /**
+     * Return the last element in an array passing a given truth test.
+     *
+     * @param mixed $default
+     *
+     * @return mixed
+     */
+    public static function last(array $array, callable $callback, $default = null)
+    {
+        return static::first(array_reverse($array), $callback, $default);
+    }
+
+    /**
+     * Flatten a multi-dimensional array into a single level.
+     */
+    public static function flatten(array $array): array
+    {
+        $return = [];
+        array_walk_recursive(
+            $array,
+            function ($x) use (&$return) {
+                $return[] = $x;
+            }
+        );
+
+        return $return;
+    }
+
+    /**
+     * Remove one or many array items from a given array using "dot" notation.
+     *
+     * @param array        $array
+     * @param array|string $keys
+     */
+    public static function forget(&$array, $keys)
+    {
+        $original = &$array;
+
+        $keys = (array) $keys;
+
+        if (0 === count($keys)) {
+            return;
+        }
+
+        foreach ($keys as $key) {
+            // if the exact key exists in the top-level, remove it
+            if (static::exists($array, $key)) {
+                unset($array[$key]);
+
+                continue;
+            }
+
+            $parts = explode('.', $key);
+
+            // clean up before each pass
+            $array = &$original;
+
+            while (count($parts) > 1) {
+                $part = array_shift($parts);
+
+                if (isset($array[$part]) && is_array($array[$part])) {
+                    $array = &$array[$part];
+                } else {
+                    continue 2;
+                }
+            }
+
+            unset($array[array_shift($parts)]);
+        }
+    }
+
+    /**
+     * Get an item from an array using "dot" notation.
+     *
+     * @param mixed $default
+     *
+     * @return mixed
+     */
+    public static function get(array $array, string $key, $default = null)
+    {
+        if (is_null($key)) {
+            return $array;
+        }
+
+        if (isset($array[$key])) {
+            return $array[$key];
+        }
+
+        foreach (explode('.', $key) as $segment) {
+            if (!is_array($array) || !array_key_exists($segment, $array)) {
+                return $default;
+            }
+            $array = $array[$segment];
+        }
+
+        return $array;
+    }
+
+    /**
+     * Get a subset of the items from the given array.
+     *
+     * @param array|string $keys
+     */
+    public static function only(array $array, $keys): array
+    {
+        return array_intersect_key($array, array_flip((array) $keys));
+    }
+
+    /**
+     * Pluck an array of values from an array.
+     *
+     * @param string $key
+     */
+    public static function pluck(array $array, string $value, string $key = null): array
+    {
+        $results = [];
+
+        foreach ($array as $item) {
+            $itemValue = is_object($item) ? $item->{$value} : $item[$value];
+            // If the key is "null", we will just append the value to the array and keep
+            // looping. Otherwise we will key the array using the value of the key we
+            // received from the developer. Then we'll return the final array form.
+            if (is_null($key)) {
+                $results[] = $itemValue;
+            } else {
+                $itemKey = is_object($item) ? $item->{$key} : $item[$key];
+                $results[$itemKey] = $itemValue;
+            }
+        }
+
+        return $results;
+    }
+
+    /**
+     * Push an item onto the beginning of an array.
+     *
+     * @param mixed $value
+     * @param mixed $key
+     *
+     * @return array
+     */
+    public static function prepend(array $array, $value, $key = null)
+    {
+        if (is_null($key)) {
+            array_unshift($array, $value);
+        } else {
+            $array = [$key => $value] + $array;
+        }
+
+        return $array;
+    }
+
+    /**
+     * Get a value from the array, and remove it.
+     *
+     * @param mixed $default
+     *
+     * @return mixed
+     */
+    public static function pull(array &$array, string $key, $default = null)
+    {
+        $value = static::get($array, $key, $default);
+
+        static::forget($array, $key);
+
+        return $value;
+    }
+
+    /**
+     * Get one or a specified number of random values from an array.
+     *
+     * @param array    $array
+     * @param int|null $number
+     *
+     * @return mixed
+     *
+     * @throws \InvalidArgumentException
+     */
+    public static function random(array $array, $number = null)
+    {
+        $requested = is_null($number) ? 1 : $number;
+
+        $count = count($array);
+
+        $number = $requested > $count ? $count : $requested;
+
+        if (is_null($number)) {
+            return $array[array_rand($array)];
+        }
+
+        if (0 === (int) $number) {
+            return [];
+        }
+
+        $keys = array_rand($array, $number);
+
+        $results = [];
+
+        foreach ((array) $keys as $key) {
+            $results[] = $array[$key];
+        }
+
+        return $results;
+    }
+
+    /**
+     * Set an array item to a given value using "dot" notation.
+     *
+     * If no key is given to the method, the entire array will be replaced.
+     *
+     * @param mixed $value
+     */
+    public static function set(array &$array, string $key, $value): array
+    {
+        if (is_null($key)) {
+            return $array = $value;
+        }
+
+        $keys = explode('.', $key);
+
+        while (count($keys) > 1) {
+            $key = array_shift($keys);
+            // If the key doesn't exist at this depth, we will just create an empty array
+            // to hold the next value, allowing us to create the arrays to hold final
+            // values at the correct depth. Then we'll keep digging into the array.
+            if (!isset($array[$key]) || !is_array($array[$key])) {
+                $array[$key] = [];
+            }
+            $array = &$array[$key];
+        }
+        $array[array_shift($keys)] = $value;
+
+        return $array;
+    }
+
+    /**
+     * Sort the array using the given Closure.
+     */
+    public static function sort(array $array, callable $callback): array
+    {
+        $results = [];
+
+        foreach ($array as $key => $value) {
+            $results[$key] = $callback($value);
+        }
+
+        return $results;
+    }
+
+    /**
+     * Shuffle the given array and return the result.
+     *
+     * @param array    $array
+     * @param int|null $seed
+     *
+     * @return array
+     */
+    public static function shuffle(array $array, $seed = null): array
+    {
+        if (is_null($seed)) {
+            shuffle($array);
+        } else {
+            mt_srand($seed);
+            shuffle($array);
+            mt_srand();
+        }
+
+        return $array;
+    }
+
+    /**
+     * Convert the array into a query string.
+     */
+    public static function query(array $array): string
+    {
+        return http_build_query($array, null, '&', PHP_QUERY_RFC3986);
+    }
+
+    /**
+     * Filter the array using the given callback.
+     */
+    public static function where(array $array, ?callable $callback = null): array
+    {
+        return array_filter($array, $callback ?? function ($value) use ($callback) {
+            if (static::accessible($value)) {
+                $value = static::where($value, $callback);
+            }
+
+            if (is_array($value) && 0 === count($value)) {
+                $value = null;
+            }
+
+            return '' !== $value && !is_null($value);
+        }, ARRAY_FILTER_USE_BOTH);
+    }
+
+    /**
+     * Convert encoding.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $from_encoding
+     */
+    public static function encoding(array $array, string $to_encoding, $from_encoding = 'gb2312'): array
+    {
+        $encoded = [];
+
+        foreach ($array as $key => $value) {
+            $encoded[$key] = is_array($value) ? self::encoding($value, $to_encoding, $from_encoding) :
+                                                mb_convert_encoding($value, $to_encoding, $from_encoding);
+        }
+
+        return $encoded;
+    }
+
+    /**
+     * camelCaseKey.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param mixed $data
+     *
+     * @return mixed
+     */
+    public static function camelCaseKey($data)
+    {
+        if (!self::accessible($data) &&
+            !(is_object($data) && method_exists($data, 'toArray'))) {
+            return $data;
+        }
+
+        $result = [];
+        $data = self::access($data);
+
+        foreach ($data as $key => $value) {
+            $result[is_string($key) ? Str::camel($key) : $key] = self::camelCaseKey($value);
+        }
+
+        return $result;
+    }
+
+    /**
+     * snakeCaseKey.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param mixed $data
+     *
+     * @return mixed
+     */
+    public static function snakeCaseKey($data)
+    {
+        if (!self::accessible($data) &&
+            !(is_object($data) && method_exists($data, 'toArray'))) {
+            return $data;
+        }
+
+        $data = self::access($data);
+        $result = [];
+
+        foreach ($data as $key => $value) {
+            $result[is_string($key) ? Str::snake($key) : $key] = self::snakeCaseKey($value);
+        }
+
+        return $result;
+    }
+}

+ 363 - 0
addons/epay/library/Yansongda/Supports/Collection.php

@@ -0,0 +1,363 @@
+<?php
+
+namespace Yansongda\Supports;
+
+use ArrayAccess;
+use ArrayIterator;
+use Countable;
+use IteratorAggregate;
+use JsonSerializable;
+use Serializable;
+
+class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable, Serializable
+{
+    /**
+     * The collection data.
+     *
+     * @var array
+     */
+    protected $items = [];
+
+    /**
+     * set data.
+     *
+     * @param mixed $items
+     */
+    public function __construct(array $items = [])
+    {
+        foreach ($items as $key => $value) {
+            $this->set($key, $value);
+        }
+    }
+
+    /**
+     * To string.
+     */
+    public function __toString(): string
+    {
+        return $this->toJson();
+    }
+
+    /**
+     * Get a data by key.
+     *
+     * @return mixed
+     */
+    public function __get(string $key)
+    {
+        return $this->get($key);
+    }
+
+    /**
+     * Assigns a value to the specified data.
+     *
+     * @param mixed $value
+     */
+    public function __set(string $key, $value)
+    {
+        $this->set($key, $value);
+    }
+
+    /**
+     * Whether or not an data exists by key.
+     */
+    public function __isset(string $key): bool
+    {
+        return $this->has($key);
+    }
+
+    /**
+     * Unsets an data by key.
+     */
+    public function __unset(string $key)
+    {
+        $this->forget($key);
+    }
+
+    /**
+     * Return all items.
+     */
+    public function all(): array
+    {
+        return $this->items;
+    }
+
+    /**
+     * Return specific items.
+     */
+    public function only(array $keys): array
+    {
+        $return = [];
+
+        foreach ($keys as $key) {
+            $value = $this->get($key);
+
+            if (!is_null($value)) {
+                $return[$key] = $value;
+            }
+        }
+
+        return $return;
+    }
+
+    /**
+     * Get all items except for those with the specified keys.
+     *
+     * @param mixed $keys
+     *
+     * @return static
+     */
+    public function except($keys)
+    {
+        $keys = is_array($keys) ? $keys : func_get_args();
+
+        return new static(Arr::except($this->items, $keys));
+    }
+
+    /**
+     * Merge data.
+     *
+     * @param Collection|array $items
+     */
+    public function merge($items): array
+    {
+        foreach ($items as $key => $value) {
+            $this->set($key, $value);
+        }
+
+        return $this->all();
+    }
+
+    /**
+     * To determine Whether the specified element exists.
+     */
+    public function has(string $key): bool
+    {
+        return !is_null(Arr::get($this->items, $key));
+    }
+
+    /**
+     * Retrieve the first item.
+     *
+     * @return mixed
+     */
+    public function first()
+    {
+        return reset($this->items);
+    }
+
+    /**
+     * Retrieve the last item.
+     *
+     * @return mixed
+     */
+    public function last()
+    {
+        $end = end($this->items);
+
+        reset($this->items);
+
+        return $end;
+    }
+
+    /**
+     * add the item value.
+     *
+     * @param mixed $value
+     */
+    public function add(string $key, $value)
+    {
+        Arr::set($this->items, $key, $value);
+    }
+
+    /**
+     * Set the item value.
+     *
+     * @param mixed $value
+     */
+    public function set(string $key, $value)
+    {
+        Arr::set($this->items, $key, $value);
+    }
+
+    /**
+     * Retrieve item from Collection.
+     *
+     * @param string $key
+     * @param mixed  $default
+     *
+     * @return mixed
+     */
+    public function get(?string $key = null, $default = null)
+    {
+        return Arr::get($this->items, $key, $default);
+    }
+
+    /**
+     * Remove item form Collection.
+     */
+    public function forget(string $key)
+    {
+        Arr::forget($this->items, $key);
+    }
+
+    /**
+     * Build to array.
+     */
+    public function toArray(): array
+    {
+        return $this->all();
+    }
+
+    /**
+     * Build to json.
+     */
+    public function toJson(int $option = JSON_UNESCAPED_UNICODE): string
+    {
+        return json_encode($this->all(), $option);
+    }
+
+    /**
+     * (PHP 5 &gt;= 5.4.0)<br/>
+     * Specify data which should be serialized to JSON.
+     *
+     * @see http://php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed data which can be serialized by <b>json_encode</b>,
+     *               which is a value of any type other than a resource
+     */
+    public function jsonSerialize()
+    {
+        return $this->items;
+    }
+
+    /**
+     * (PHP 5 &gt;= 5.1.0)<br/>
+     * String representation of object.
+     *
+     * @see http://php.net/manual/en/serializable.serialize.php
+     *
+     * @return string the string representation of the object or null
+     */
+    public function serialize()
+    {
+        return serialize($this->items);
+    }
+
+    /**
+     * (PHP 5 &gt;= 5.0.0)<br/>
+     * Retrieve an external iterator.
+     *
+     * @see http://php.net/manual/en/iteratoraggregate.getiterator.php
+     *
+     * @return ArrayIterator An instance of an object implementing <b>Iterator</b> or
+     *                       <b>ArrayIterator</b>
+     */
+    public function getIterator()
+    {
+        return new ArrayIterator($this->items);
+    }
+
+    /**
+     * (PHP 5 &gt;= 5.1.0)<br/>
+     * Count elements of an object.
+     *
+     * @see http://php.net/manual/en/countable.count.php
+     *
+     * @return int The custom count as an integer.
+     *             </p>
+     *             <p>
+     *             The return value is cast to an integer
+     */
+    public function count()
+    {
+        return count($this->items);
+    }
+
+    /**
+     * (PHP 5 &gt;= 5.1.0)<br/>
+     * Constructs the object.
+     *
+     * @see  http://php.net/manual/en/serializable.unserialize.php
+     *
+     * @param string $serialized <p>
+     *                           The string representation of the object.
+     *                           </p>
+     *
+     * @return mixed|void
+     */
+    public function unserialize($serialized)
+    {
+        return $this->items = unserialize($serialized);
+    }
+
+    /**
+     * (PHP 5 &gt;= 5.0.0)<br/>
+     * Whether a offset exists.
+     *
+     * @see http://php.net/manual/en/arrayaccess.offsetexists.php
+     *
+     * @param mixed $offset <p>
+     *                      An offset to check for.
+     *                      </p>
+     *
+     * @return bool true on success or false on failure.
+     *              The return value will be casted to boolean if non-boolean was returned
+     */
+    public function offsetExists($offset)
+    {
+        return $this->has($offset);
+    }
+
+    /**
+     * (PHP 5 &gt;= 5.0.0)<br/>
+     * Offset to unset.
+     *
+     * @see http://php.net/manual/en/arrayaccess.offsetunset.php
+     *
+     * @param mixed $offset <p>
+     *                      The offset to unset.
+     *                      </p>
+     */
+    public function offsetUnset($offset)
+    {
+        if ($this->offsetExists($offset)) {
+            $this->forget($offset);
+        }
+    }
+
+    /**
+     * (PHP 5 &gt;= 5.0.0)<br/>
+     * Offset to retrieve.
+     *
+     * @see http://php.net/manual/en/arrayaccess.offsetget.php
+     *
+     * @param mixed $offset <p>
+     *                      The offset to retrieve.
+     *                      </p>
+     *
+     * @return mixed Can return all value types
+     */
+    public function offsetGet($offset)
+    {
+        return $this->offsetExists($offset) ? $this->get($offset) : null;
+    }
+
+    /**
+     * (PHP 5 &gt;= 5.0.0)<br/>
+     * Offset to set.
+     *
+     * @see http://php.net/manual/en/arrayaccess.offsetset.php
+     *
+     * @param mixed $offset <p>
+     *                      The offset to assign the value to.
+     *                      </p>
+     * @param mixed $value  <p>
+     *                      The value to set.
+     *                      </p>
+     */
+    public function offsetSet($offset, $value)
+    {
+        $this->set($offset, $value);
+    }
+}

+ 7 - 0
addons/epay/library/Yansongda/Supports/Config.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Yansongda\Supports;
+
+class Config extends Collection
+{
+}

+ 20 - 0
addons/epay/library/Yansongda/Supports/LICENSE

@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 yansongda <me@yansongda.cn>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 91 - 0
addons/epay/library/Yansongda/Supports/Log.php

@@ -0,0 +1,91 @@
+<?php
+
+namespace Yansongda\Supports;
+
+/**
+ * @method static void emergency($message, array $context = array())
+ * @method static void alert($message, array $context = array())
+ * @method static void critical($message, array $context = array())
+ * @method static void error($message, array $context = array())
+ * @method static void warning($message, array $context = array())
+ * @method static void notice($message, array $context = array())
+ * @method static void info($message, array $context = array())
+ * @method static void debug($message, array $context = array())
+ * @method static void log($message, array $context = array())
+ */
+class Log extends Logger
+{
+    /**
+     * instance.
+     *
+     * @var \Psr\Log\LoggerInterface
+     */
+    private static $instance;
+
+    /**
+     * Bootstrap.
+     */
+    private function __construct()
+    {
+    }
+
+    /**
+     * __call.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $method
+     * @param array  $args
+     *
+     * @throws \Exception
+     */
+    public function __call($method, $args): void
+    {
+        call_user_func_array([self::getInstance(), $method], $args);
+    }
+
+    /**
+     * __callStatic.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $method
+     * @param array  $args
+     *
+     * @throws \Exception
+     */
+    public static function __callStatic($method, $args): void
+    {
+        forward_static_call_array([self::getInstance(), $method], $args);
+    }
+
+    /**
+     * getInstance.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return \Yansongda\Supports\Logger
+     */
+    public static function getInstance(): Logger
+    {
+        if (is_null(self::$instance)) {
+            self::$instance = new Logger();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * setInstance.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param \Yansongda\Supports\Logger $logger
+     *
+     * @throws \Exception
+     */
+    public static function setInstance(Logger $logger): void
+    {
+        self::$instance = $logger;
+    }
+}

+ 240 - 0
addons/epay/library/Yansongda/Supports/Logger.php

@@ -0,0 +1,240 @@
+<?php
+
+namespace Yansongda\Supports;
+
+use Exception;
+use Monolog\Formatter\FormatterInterface;
+use Monolog\Formatter\LineFormatter;
+use Monolog\Handler\AbstractHandler;
+use Monolog\Handler\RotatingFileHandler;
+use Monolog\Handler\StreamHandler;
+use Monolog\Logger as BaseLogger;
+use Psr\Log\LoggerInterface;
+
+/**
+ * @method void emergency($message, array $context = array())
+ * @method void alert($message, array $context = array())
+ * @method void critical($message, array $context = array())
+ * @method void error($message, array $context = array())
+ * @method void warning($message, array $context = array())
+ * @method void notice($message, array $context = array())
+ * @method void info($message, array $context = array())
+ * @method void debug($message, array $context = array())
+ * @method void log($message, array $context = array())
+ */
+class Logger
+{
+    /**
+     * Logger instance.
+     *
+     * @var LoggerInterface
+     */
+    protected $logger;
+
+    /**
+     * formatter.
+     *
+     * @var \Monolog\Formatter\FormatterInterface
+     */
+    protected $formatter;
+
+    /**
+     * handler.
+     *
+     * @var AbstractHandler
+     */
+    protected $handler;
+
+    /**
+     * config.
+     *
+     * @var array
+     */
+    protected $config = [
+        'file' => null,
+        'identify' => 'yansongda.supports',
+        'level' => BaseLogger::DEBUG,
+        'type' => 'daily',
+        'max_files' => 30,
+    ];
+
+    /**
+     * Forward call.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $method
+     * @param array  $args
+     *
+     * @throws Exception
+     */
+    public function __call($method, $args): void
+    {
+        call_user_func_array([$this->getLogger(), $method], $args);
+    }
+
+    /**
+     * Set logger.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function setLogger(LoggerInterface $logger): Logger
+    {
+        $this->logger = $logger;
+
+        return $this;
+    }
+
+    /**
+     * Return the logger instance.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws Exception
+     */
+    public function getLogger(): LoggerInterface
+    {
+        if (is_null($this->logger)) {
+            $this->logger = $this->createLogger();
+        }
+
+        return $this->logger;
+    }
+
+    /**
+     * Make a default log instance.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws Exception
+     */
+    public function createLogger(): BaseLogger
+    {
+        $handler = $this->getHandler();
+
+        $handler->setFormatter($this->getFormatter());
+
+        $logger = new BaseLogger($this->config['identify']);
+
+        $logger->pushHandler($handler);
+
+        return $logger;
+    }
+
+    /**
+     * setFormatter.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return $this
+     */
+    public function setFormatter(FormatterInterface $formatter): self
+    {
+        $this->formatter = $formatter;
+
+        return $this;
+    }
+
+    /**
+     * getFormatter.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function getFormatter(): FormatterInterface
+    {
+        if (is_null($this->formatter)) {
+            $this->formatter = $this->createFormatter();
+        }
+
+        return $this->formatter;
+    }
+
+    /**
+     * createFormatter.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function createFormatter(): LineFormatter
+    {
+        return new LineFormatter(
+            "%datetime% > %channel%.%level_name% > %message% %context% %extra%\n\n",
+            null,
+            false,
+            true
+        );
+    }
+
+    /**
+     * setHandler.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return $this
+     */
+    public function setHandler(AbstractHandler $handler): self
+    {
+        $this->handler = $handler;
+
+        return $this;
+    }
+
+    /**
+     * getHandler.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws \Exception
+     */
+    public function getHandler(): AbstractHandler
+    {
+        if (is_null($this->handler)) {
+            $this->handler = $this->createHandler();
+        }
+
+        return $this->handler;
+    }
+
+    /**
+     * createHandler.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws \Exception
+     *
+     * @return \Monolog\Handler\RotatingFileHandler|\Monolog\Handler\StreamHandler
+     */
+    public function createHandler(): AbstractHandler
+    {
+        $file = $this->config['file'] ?? sys_get_temp_dir().'/logs/'.$this->config['identify'].'.log';
+
+        if ('single' === $this->config['type']) {
+            return new StreamHandler($file, $this->config['level']);
+        }
+
+        return new RotatingFileHandler($file, $this->config['max_files'], $this->config['level']);
+    }
+
+    /**
+     * setConfig.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return $this
+     */
+    public function setConfig(array $config): self
+    {
+        $this->config = array_merge($this->config, $config);
+
+        return $this;
+    }
+
+    /**
+     * getConfig.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function getConfig(): array
+    {
+        return $this->config;
+    }
+}

+ 36 - 0
addons/epay/library/Yansongda/Supports/Logger/StdoutHandler.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace Yansongda\Supports\Logger;
+
+use Monolog\Handler\AbstractProcessingHandler;
+use Monolog\Logger;
+use Symfony\Component\Console\Output\ConsoleOutput;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class StdoutHandler extends AbstractProcessingHandler
+{
+    /**
+     * @var OutputInterface
+     */
+    private $output;
+
+    /**
+     * Bootstrap.
+     *
+     * @param int  $level
+     * @param bool $bubble
+     */
+    public function __construct($level = Logger::DEBUG, $bubble = true, ?OutputInterface $output = null)
+    {
+        $this->output = $output ?? new ConsoleOutput();
+        parent::__construct($level, $bubble);
+    }
+
+    /**
+     * Writes the record down to the log of the implementing handler.
+     */
+    protected function write(array $record): void
+    {
+        $this->output->writeln($record['formatted']);
+    }
+}

+ 570 - 0
addons/epay/library/Yansongda/Supports/Str.php

@@ -0,0 +1,570 @@
+<?php
+
+namespace Yansongda\Supports;
+
+use Exception;
+
+/**
+ * modify from Illuminate\Support.
+ */
+class Str
+{
+    /**
+     * The cache of snake-cased words.
+     *
+     * @var array
+     */
+    protected static $snakeCache = [];
+
+    /**
+     * The cache of camel-cased words.
+     *
+     * @var array
+     */
+    protected static $camelCache = [];
+
+    /**
+     * The cache of studly-cased words.
+     *
+     * @var array
+     */
+    protected static $studlyCache = [];
+
+    /**
+     * Return the remainder of a string after a given value.
+     */
+    public static function after(string $subject, string $search): string
+    {
+        return '' === $search ? $subject : array_reverse(explode($search, $subject, 2))[0];
+    }
+
+    /**
+     * Transliterate a UTF-8 value to ASCII.
+     */
+    public static function ascii(string $value, string $language = 'en'): string
+    {
+        $languageSpecific = static::languageSpecificCharsArray($language);
+
+        if (!is_null($languageSpecific)) {
+            $value = str_replace($languageSpecific[0], $languageSpecific[1], $value);
+        }
+
+        foreach (static::charsArray() as $key => $val) {
+            $value = str_replace($val, $key, $value);
+        }
+
+        return preg_replace('/[^\x20-\x7E]/u', '', $value);
+    }
+
+    /**
+     * Get the portion of a string before a given value.
+     */
+    public static function before(string $subject, string $search): string
+    {
+        return '' === $search ? $subject : explode($search, $subject)[0];
+    }
+
+    /**
+     * Convert a value to camel case.
+     */
+    public static function camel(string $value): string
+    {
+        if (isset(static::$camelCache[$value])) {
+            return static::$camelCache[$value];
+        }
+
+        return static::$camelCache[$value] = lcfirst(static::studly($value));
+    }
+
+    /**
+     * Determine if a given string contains a given substring.
+     *
+     * @param string|array $needles
+     */
+    public static function contains(string $haystack, $needles): bool
+    {
+        foreach ((array) $needles as $needle) {
+            if ('' !== $needle && false !== mb_strpos($haystack, $needle)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Determine if a given string ends with a given substring.
+     *
+     * @param string|array $needles
+     */
+    public static function endsWith(string $haystack, $needles): bool
+    {
+        foreach ((array) $needles as $needle) {
+            if (substr($haystack, -strlen($needle)) === (string) $needle) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Cap a string with a single instance of a given value.
+     */
+    public static function finish(string $value, string $cap): string
+    {
+        $quoted = preg_quote($cap, '/');
+
+        return preg_replace('/(?:'.$quoted.')+$/u', '', $value).$cap;
+    }
+
+    /**
+     * Determine if a given string matches a given pattern.
+     *
+     * @param string|array $pattern
+     */
+    public static function is($pattern, string $value): bool
+    {
+        $patterns = is_array($pattern) ? $pattern : (array) $pattern;
+
+        if (empty($patterns)) {
+            return false;
+        }
+
+        foreach ($patterns as $pattern) {
+            // If the given value is an exact match we can of course return true right
+            // from the beginning. Otherwise, we will translate asterisks and do an
+            // actual pattern match against the two strings to see if they match.
+            if ($pattern == $value) {
+                return true;
+            }
+
+            $pattern = preg_quote($pattern, '#');
+
+            // Asterisks are translated into zero-or-more regular expression wildcards
+            // to make it convenient to check if the strings starts with the given
+            // pattern such as "library/*", making any string check convenient.
+            $pattern = str_replace('\*', '.*', $pattern);
+
+            if (1 === preg_match('#^'.$pattern.'\z#u', $value)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Convert a string to kebab case.
+     */
+    public static function kebab(string $value): string
+    {
+        return static::snake($value, '-');
+    }
+
+    /**
+     * Return the length of the given string.
+     *
+     * @param string $encoding
+     */
+    public static function length(string $value, ?string $encoding = null): int
+    {
+        if (null !== $encoding) {
+            return mb_strlen($value, $encoding);
+        }
+
+        return mb_strlen($value);
+    }
+
+    /**
+     * Limit the number of characters in a string.
+     */
+    public static function limit(string $value, int $limit = 100, string $end = '...'): string
+    {
+        if (mb_strwidth($value, 'UTF-8') <= $limit) {
+            return $value;
+        }
+
+        return rtrim(mb_strimwidth($value, 0, $limit, '', 'UTF-8')).$end;
+    }
+
+    /**
+     * Convert the given string to lower-case.
+     */
+    public static function lower(string $value): string
+    {
+        return mb_strtolower($value, 'UTF-8');
+    }
+
+    /**
+     * Limit the number of words in a string.
+     */
+    public static function words(string $value, int $words = 100, string $end = '...'): string
+    {
+        preg_match('/^\s*+(?:\S++\s*+){1,'.$words.'}/u', $value, $matches);
+
+        if (!isset($matches[0]) || static::length($value) === static::length($matches[0])) {
+            return $value;
+        }
+
+        return rtrim($matches[0]).$end;
+    }
+
+    /**
+     * Parse a Class.
+     */
+    public static function parseCallback(string $callback, ?string $default = null): array
+    {
+        return static::contains($callback, '@') ? explode('@', $callback, 2) : [$callback, $default];
+    }
+
+    /**
+     * Generate a more truly "random" alpha-numeric string.
+     *
+     * @throws Exception
+     */
+    public static function random(int $length = 16): string
+    {
+        $string = '';
+
+        while (($len = strlen($string)) < $length) {
+            $size = $length - $len;
+
+            $bytes = function_exists('random_bytes') ? random_bytes($size) : mt_rand();
+
+            $string .= substr(str_replace(['/', '+', '='], '', base64_encode($bytes)), 0, $size);
+        }
+
+        return $string;
+    }
+
+    /**
+     * Replace a given value in the string sequentially with an array.
+     */
+    public static function replaceArray(string $search, array $replace, string $subject): string
+    {
+        foreach ($replace as $value) {
+            $subject = static::replaceFirst($search, $value, $subject);
+        }
+
+        return $subject;
+    }
+
+    /**
+     * Replace the first occurrence of a given value in the string.
+     */
+    public static function replaceFirst(string $search, string $replace, string $subject): string
+    {
+        if ('' == $search) {
+            return $subject;
+        }
+
+        $position = strpos($subject, $search);
+
+        if (false !== $position) {
+            return substr_replace($subject, $replace, $position, strlen($search));
+        }
+
+        return $subject;
+    }
+
+    /**
+     * Replace the last occurrence of a given value in the string.
+     */
+    public static function replaceLast(string $search, string $replace, string $subject): string
+    {
+        $position = strrpos($subject, $search);
+
+        if (false !== $position) {
+            return substr_replace($subject, $replace, $position, strlen($search));
+        }
+
+        return $subject;
+    }
+
+    /**
+     * Begin a string with a single instance of a given value.
+     */
+    public static function start(string $value, string $prefix): string
+    {
+        $quoted = preg_quote($prefix, '/');
+
+        return $prefix.preg_replace('/^(?:'.$quoted.')+/u', '', $value);
+    }
+
+    /**
+     * Convert the given string to upper-case.
+     */
+    public static function upper(string $value): string
+    {
+        return mb_strtoupper($value, 'UTF-8');
+    }
+
+    /**
+     * Convert the given string to title case.
+     */
+    public static function title(string $value): string
+    {
+        return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8');
+    }
+
+    /**
+     * Generate a URL friendly "slug" from a given string.
+     */
+    public static function slug(string $title, string $separator = '-', string $language = 'en'): string
+    {
+        $title = static::ascii($title, $language);
+
+        // Convert all dashes/underscores into separator
+        $flip = '-' == $separator ? '_' : '-';
+
+        $title = preg_replace('!['.preg_quote($flip).']+!u', $separator, $title);
+
+        // Replace @ with the word 'at'
+        $title = str_replace('@', $separator.'at'.$separator, $title);
+
+        // Remove all characters that are not the separator, letters, numbers, or whitespace.
+        $title = preg_replace('![^'.preg_quote($separator).'\pL\pN\s]+!u', '', mb_strtolower($title));
+
+        // Replace all separator characters and whitespace by a single separator
+        $title = preg_replace('!['.preg_quote($separator).'\s]+!u', $separator, $title);
+
+        return trim($title, $separator);
+    }
+
+    /**
+     * Convert a string to snake case.
+     */
+    public static function snake(string $value, string $delimiter = '_'): string
+    {
+        $key = $value;
+
+        if (isset(static::$snakeCache[$key][$delimiter])) {
+            return static::$snakeCache[$key][$delimiter];
+        }
+
+        if (!ctype_lower($value)) {
+            $value = preg_replace('/\s+/u', '', ucwords($value));
+
+            $value = static::lower(preg_replace('/(.)(?=[A-Z])/u', '$1'.$delimiter, $value));
+        }
+
+        return static::$snakeCache[$key][$delimiter] = $value;
+    }
+
+    /**
+     * Determine if a given string starts with a given substring.
+     *
+     * @param string|array $needles
+     */
+    public static function startsWith(string $haystack, $needles): bool
+    {
+        foreach ((array) $needles as $needle) {
+            if ('' !== $needle && substr($haystack, 0, strlen($needle)) === (string) $needle) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Convert a value to studly caps case.
+     */
+    public static function studly(string $value): string
+    {
+        $key = $value;
+
+        if (isset(static::$studlyCache[$key])) {
+            return static::$studlyCache[$key];
+        }
+
+        $value = ucwords(str_replace(['-', '_'], ' ', $value));
+
+        return static::$studlyCache[$key] = str_replace(' ', '', $value);
+    }
+
+    /**
+     * Returns the portion of string specified by the start and length parameters.
+     */
+    public static function substr(string $string, int $start, ?int $length = null): string
+    {
+        return mb_substr($string, $start, $length, 'UTF-8');
+    }
+
+    /**
+     * Make a string's first character uppercase.
+     */
+    public static function ucfirst(string $string): string
+    {
+        return static::upper(static::substr($string, 0, 1)).static::substr($string, 1);
+    }
+
+    /**
+     * Convert string's encoding.
+     *
+     * @author yansongda <me@yansonga.cn>
+     */
+    public static function encoding(string $string, string $to = 'utf-8', string $from = 'gb2312'): string
+    {
+        return mb_convert_encoding($string, $to, $from);
+    }
+
+    /**
+     * Returns the replacements for the ascii method.
+     *
+     * Note: Adapted from Stringy\Stringy.
+     *
+     * @see https://github.com/danielstjules/Stringy/blob/3.1.0/LICENSE.txt
+     */
+    protected static function charsArray(): array
+    {
+        static $charsArray;
+
+        if (isset($charsArray)) {
+            return $charsArray;
+        }
+
+        return $charsArray = [
+            '0' => ['°', '₀', '۰', '0'],
+            '1' => ['¹', '₁', '۱', '1'],
+            '2' => ['²', '₂', '۲', '2'],
+            '3' => ['³', '₃', '۳', '3'],
+            '4' => ['⁴', '₄', '۴', '٤', '4'],
+            '5' => ['⁵', '₅', '۵', '٥', '5'],
+            '6' => ['⁶', '₆', '۶', '٦', '6'],
+            '7' => ['⁷', '₇', '۷', '7'],
+            '8' => ['⁸', '₈', '۸', '8'],
+            '9' => ['⁹', '₉', '۹', '9'],
+            'a' => ['à', 'á', 'ả', 'ã', 'ạ', 'ă', 'ắ', 'ằ', 'ẳ', 'ẵ', 'ặ', 'â', 'ấ', 'ầ', 'ẩ', 'ẫ', 'ậ', 'ā', 'ą', 'å', 'α', 'ά', 'ἀ', 'ἁ', 'ἂ', 'ἃ', 'ἄ', 'ἅ', 'ἆ', 'ἇ', 'ᾀ', 'ᾁ', 'ᾂ', 'ᾃ', 'ᾄ', 'ᾅ', 'ᾆ', 'ᾇ', 'ὰ', 'ά', 'ᾰ', 'ᾱ', 'ᾲ', 'ᾳ', 'ᾴ', 'ᾶ', 'ᾷ', 'а', 'أ', 'အ', 'ာ', 'ါ', 'ǻ', 'ǎ', 'ª', 'ა', 'अ', 'ا', 'a', 'ä'],
+            'b' => ['б', 'β', 'ب', 'ဗ', 'ბ', 'b'],
+            'c' => ['ç', 'ć', 'č', 'ĉ', 'ċ', 'c'],
+            'd' => ['ď', 'ð', 'đ', 'ƌ', 'ȡ', 'ɖ', 'ɗ', 'ᵭ', 'ᶁ', 'ᶑ', 'д', 'δ', 'د', 'ض', 'ဍ', 'ဒ', 'დ', 'd'],
+            'e' => ['é', 'è', 'ẻ', 'ẽ', 'ẹ', 'ê', 'ế', 'ề', 'ể', 'ễ', 'ệ', 'ë', 'ē', 'ę', 'ě', 'ĕ', 'ė', 'ε', 'έ', 'ἐ', 'ἑ', 'ἒ', 'ἓ', 'ἔ', 'ἕ', 'ὲ', 'έ', 'е', 'ё', 'э', 'є', 'ə', 'ဧ', 'ေ', 'ဲ', 'ე', 'ए', 'إ', 'ئ', 'e'],
+            'f' => ['ф', 'φ', 'ف', 'ƒ', 'ფ', 'f'],
+            'g' => ['ĝ', 'ğ', 'ġ', 'ģ', 'г', 'ґ', 'γ', 'ဂ', 'გ', 'گ', 'g'],
+            'h' => ['ĥ', 'ħ', 'η', 'ή', 'ح', 'ه', 'ဟ', 'ှ', 'ჰ', 'h'],
+            'i' => ['í', 'ì', 'ỉ', 'ĩ', 'ị', 'î', 'ï', 'ī', 'ĭ', 'į', 'ı', 'ι', 'ί', 'ϊ', 'ΐ', 'ἰ', 'ἱ', 'ἲ', 'ἳ', 'ἴ', 'ἵ', 'ἶ', 'ἷ', 'ὶ', 'ί', 'ῐ', 'ῑ', 'ῒ', 'ΐ', 'ῖ', 'ῗ', 'і', 'ї', 'и', 'ဣ', 'ိ', 'ီ', 'ည်', 'ǐ', 'ი', 'इ', 'ی', 'i'],
+            'j' => ['ĵ', 'ј', 'Ј', 'ჯ', 'ج', 'j'],
+            'k' => ['ķ', 'ĸ', 'к', 'κ', 'Ķ', 'ق', 'ك', 'က', 'კ', 'ქ', 'ک', 'k'],
+            'l' => ['ł', 'ľ', 'ĺ', 'ļ', 'ŀ', 'л', 'λ', 'ل', 'လ', 'ლ', 'l'],
+            'm' => ['м', 'μ', 'م', 'မ', 'მ', 'm'],
+            'n' => ['ñ', 'ń', 'ň', 'ņ', 'ʼn', 'ŋ', 'ν', 'н', 'ن', 'န', 'ნ', 'n'],
+            'o' => ['ó', 'ò', 'ỏ', 'õ', 'ọ', 'ô', 'ố', 'ồ', 'ổ', 'ỗ', 'ộ', 'ơ', 'ớ', 'ờ', 'ở', 'ỡ', 'ợ', 'ø', 'ō', 'ő', 'ŏ', 'ο', 'ὀ', 'ὁ', 'ὂ', 'ὃ', 'ὄ', 'ὅ', 'ὸ', 'ό', 'о', 'و', 'θ', 'ို', 'ǒ', 'ǿ', 'º', 'ო', 'ओ', 'o', 'ö'],
+            'p' => ['п', 'π', 'ပ', 'პ', 'پ', 'p'],
+            'q' => ['ყ', 'q'],
+            'r' => ['ŕ', 'ř', 'ŗ', 'р', 'ρ', 'ر', 'რ', 'r'],
+            's' => ['ś', 'š', 'ş', 'с', 'σ', 'ș', 'ς', 'س', 'ص', 'စ', 'ſ', 'ს', 's'],
+            't' => ['ť', 'ţ', 'т', 'τ', 'ț', 'ت', 'ط', 'ဋ', 'တ', 'ŧ', 'თ', 'ტ', 't'],
+            'u' => ['ú', 'ù', 'ủ', 'ũ', 'ụ', 'ư', 'ứ', 'ừ', 'ử', 'ữ', 'ự', 'û', 'ū', 'ů', 'ű', 'ŭ', 'ų', 'µ', 'у', 'ဉ', 'ု', 'ူ', 'ǔ', 'ǖ', 'ǘ', 'ǚ', 'ǜ', 'უ', 'उ', 'u', 'ў', 'ü'],
+            'v' => ['в', 'ვ', 'ϐ', 'v'],
+            'w' => ['ŵ', 'ω', 'ώ', 'ဝ', 'ွ', 'w'],
+            'x' => ['χ', 'ξ', 'x'],
+            'y' => ['ý', 'ỳ', 'ỷ', 'ỹ', 'ỵ', 'ÿ', 'ŷ', 'й', 'ы', 'υ', 'ϋ', 'ύ', 'ΰ', 'ي', 'ယ', 'y'],
+            'z' => ['ź', 'ž', 'ż', 'з', 'ζ', 'ز', 'ဇ', 'ზ', 'z'],
+            'aa' => ['ع', 'आ', 'آ'],
+            'ae' => ['æ', 'ǽ'],
+            'ai' => ['ऐ'],
+            'ch' => ['ч', 'ჩ', 'ჭ', 'چ'],
+            'dj' => ['ђ', 'đ'],
+            'dz' => ['џ', 'ძ'],
+            'ei' => ['ऍ'],
+            'gh' => ['غ', 'ღ'],
+            'ii' => ['ई'],
+            'ij' => ['ij'],
+            'kh' => ['х', 'خ', 'ხ'],
+            'lj' => ['љ'],
+            'nj' => ['њ'],
+            'oe' => ['ö', 'œ', 'ؤ'],
+            'oi' => ['ऑ'],
+            'oii' => ['ऒ'],
+            'ps' => ['ψ'],
+            'sh' => ['ш', 'შ', 'ش'],
+            'shch' => ['щ'],
+            'ss' => ['ß'],
+            'sx' => ['ŝ'],
+            'th' => ['þ', 'ϑ', 'ث', 'ذ', 'ظ'],
+            'ts' => ['ц', 'ც', 'წ'],
+            'ue' => ['ü'],
+            'uu' => ['ऊ'],
+            'ya' => ['я'],
+            'yu' => ['ю'],
+            'zh' => ['ж', 'ჟ', 'ژ'],
+            '(c)' => ['©'],
+            'A' => ['Á', 'À', 'Ả', 'Ã', 'Ạ', 'Ă', 'Ắ', 'Ằ', 'Ẳ', 'Ẵ', 'Ặ', 'Â', 'Ấ', 'Ầ', 'Ẩ', 'Ẫ', 'Ậ', 'Å', 'Ā', 'Ą', 'Α', 'Ά', 'Ἀ', 'Ἁ', 'Ἂ', 'Ἃ', 'Ἄ', 'Ἅ', 'Ἆ', 'Ἇ', 'ᾈ', 'ᾉ', 'ᾊ', 'ᾋ', 'ᾌ', 'ᾍ', 'ᾎ', 'ᾏ', 'Ᾰ', 'Ᾱ', 'Ὰ', 'Ά', 'ᾼ', 'А', 'Ǻ', 'Ǎ', 'A', 'Ä'],
+            'B' => ['Б', 'Β', 'ब', 'B'],
+            'C' => ['Ç', 'Ć', 'Č', 'Ĉ', 'Ċ', 'C'],
+            'D' => ['Ď', 'Ð', 'Đ', 'Ɖ', 'Ɗ', 'Ƌ', 'ᴅ', 'ᴆ', 'Д', 'Δ', 'D'],
+            'E' => ['É', 'È', 'Ẻ', 'Ẽ', 'Ẹ', 'Ê', 'Ế', 'Ề', 'Ể', 'Ễ', 'Ệ', 'Ë', 'Ē', 'Ę', 'Ě', 'Ĕ', 'Ė', 'Ε', 'Έ', 'Ἐ', 'Ἑ', 'Ἒ', 'Ἓ', 'Ἔ', 'Ἕ', 'Έ', 'Ὲ', 'Е', 'Ё', 'Э', 'Є', 'Ə', 'E'],
+            'F' => ['Ф', 'Φ', 'F'],
+            'G' => ['Ğ', 'Ġ', 'Ģ', 'Г', 'Ґ', 'Γ', 'G'],
+            'H' => ['Η', 'Ή', 'Ħ', 'H'],
+            'I' => ['Í', 'Ì', 'Ỉ', 'Ĩ', 'Ị', 'Î', 'Ï', 'Ī', 'Ĭ', 'Į', 'İ', 'Ι', 'Ί', 'Ϊ', 'Ἰ', 'Ἱ', 'Ἳ', 'Ἴ', 'Ἵ', 'Ἶ', 'Ἷ', 'Ῐ', 'Ῑ', 'Ὶ', 'Ί', 'И', 'І', 'Ї', 'Ǐ', 'ϒ', 'I'],
+            'J' => ['J'],
+            'K' => ['К', 'Κ', 'K'],
+            'L' => ['Ĺ', 'Ł', 'Л', 'Λ', 'Ļ', 'Ľ', 'Ŀ', 'ल', 'L'],
+            'M' => ['М', 'Μ', 'M'],
+            'N' => ['Ń', 'Ñ', 'Ň', 'Ņ', 'Ŋ', 'Н', 'Ν', 'N'],
+            'O' => ['Ó', 'Ò', 'Ỏ', 'Õ', 'Ọ', 'Ô', 'Ố', 'Ồ', 'Ổ', 'Ỗ', 'Ộ', 'Ơ', 'Ớ', 'Ờ', 'Ở', 'Ỡ', 'Ợ', 'Ø', 'Ō', 'Ő', 'Ŏ', 'Ο', 'Ό', 'Ὀ', 'Ὁ', 'Ὂ', 'Ὃ', 'Ὄ', 'Ὅ', 'Ὸ', 'Ό', 'О', 'Θ', 'Ө', 'Ǒ', 'Ǿ', 'O', 'Ö'],
+            'P' => ['П', 'Π', 'P'],
+            'Q' => ['Q'],
+            'R' => ['Ř', 'Ŕ', 'Р', 'Ρ', 'Ŗ', 'R'],
+            'S' => ['Ş', 'Ŝ', 'Ș', 'Š', 'Ś', 'С', 'Σ', 'S'],
+            'T' => ['Ť', 'Ţ', 'Ŧ', 'Ț', 'Т', 'Τ', 'T'],
+            'U' => ['Ú', 'Ù', 'Ủ', 'Ũ', 'Ụ', 'Ư', 'Ứ', 'Ừ', 'Ử', 'Ữ', 'Ự', 'Û', 'Ū', 'Ů', 'Ű', 'Ŭ', 'Ų', 'У', 'Ǔ', 'Ǖ', 'Ǘ', 'Ǚ', 'Ǜ', 'U', 'Ў', 'Ü'],
+            'V' => ['В', 'V'],
+            'W' => ['Ω', 'Ώ', 'Ŵ', 'W'],
+            'X' => ['Χ', 'Ξ', 'X'],
+            'Y' => ['Ý', 'Ỳ', 'Ỷ', 'Ỹ', 'Ỵ', 'Ÿ', 'Ῠ', 'Ῡ', 'Ὺ', 'Ύ', 'Ы', 'Й', 'Υ', 'Ϋ', 'Ŷ', 'Y'],
+            'Z' => ['Ź', 'Ž', 'Ż', 'З', 'Ζ', 'Z'],
+            'AE' => ['Æ', 'Ǽ'],
+            'Ch' => ['Ч'],
+            'Dj' => ['Ђ'],
+            'Dz' => ['Џ'],
+            'Gx' => ['Ĝ'],
+            'Hx' => ['Ĥ'],
+            'Ij' => ['IJ'],
+            'Jx' => ['Ĵ'],
+            'Kh' => ['Х'],
+            'Lj' => ['Љ'],
+            'Nj' => ['Њ'],
+            'Oe' => ['Œ'],
+            'Ps' => ['Ψ'],
+            'Sh' => ['Ш'],
+            'Shch' => ['Щ'],
+            'Ss' => ['ẞ'],
+            'Th' => ['Þ'],
+            'Ts' => ['Ц'],
+            'Ya' => ['Я'],
+            'Yu' => ['Ю'],
+            'Zh' => ['Ж'],
+            ' ' => ["\xC2\xA0", "\xE2\x80\x80", "\xE2\x80\x81", "\xE2\x80\x82", "\xE2\x80\x83", "\xE2\x80\x84", "\xE2\x80\x85", "\xE2\x80\x86", "\xE2\x80\x87", "\xE2\x80\x88", "\xE2\x80\x89", "\xE2\x80\x8A", "\xE2\x80\xAF", "\xE2\x81\x9F", "\xE3\x80\x80", "\xEF\xBE\xA0"],
+        ];
+    }
+
+    /**
+     * Returns the language specific replacements for the ascii method.
+     *
+     * Note: Adapted from Stringy\Stringy.
+     *
+     * @see https://github.com/danielstjules/Stringy/blob/3.1.0/LICENSE.txt
+     */
+    protected static function languageSpecificCharsArray(string $language): ?array
+    {
+        static $languageSpecific;
+        if (!isset($languageSpecific)) {
+            $languageSpecific = [
+                'bg' => [
+                    ['х', 'Х', 'щ', 'Щ', 'ъ', 'Ъ', 'ь', 'Ь'],
+                    ['h', 'H', 'sht', 'SHT', 'a', 'А', 'y', 'Y'],
+                ],
+                'de' => [
+                    ['ä',  'ö',  'ü',  'Ä',  'Ö',  'Ü'],
+                    ['ae', 'oe', 'ue', 'AE', 'OE', 'UE'],
+                ],
+            ];
+        }
+
+        return isset($languageSpecific[$language]) ? $languageSpecific[$language] : null;
+    }
+}

+ 142 - 0
addons/epay/library/Yansongda/Supports/Traits/Accessable.php

@@ -0,0 +1,142 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Yansongda\Supports\Traits;
+
+trait Accessable
+{
+    /**
+     * __get.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return mixed
+     */
+    public function __get(string $key)
+    {
+        return $this->get($key);
+    }
+
+    /**
+     * __set.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param mixed $value
+     *
+     * @return mixed
+     */
+    public function __set(string $key, $value)
+    {
+        return $this->set($key, $value);
+    }
+
+    /**
+     * get.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param mixed $default
+     *
+     * @return mixed
+     */
+    public function get(?string $key = null, $default = null)
+    {
+        if (is_null($key)) {
+            return method_exists($this, 'toArray') ? $this->toArray() : $default;
+        }
+
+        $method = 'get';
+        foreach (explode('_', $key) as $item) {
+            $method .= ucfirst($item);
+        }
+
+        if (method_exists($this, $method)) {
+            return $this->{$method}();
+        }
+
+        return $default;
+    }
+
+    /**
+     * set.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param mixed $value
+     *
+     * @return $this
+     */
+    public function set(string $key, $value)
+    {
+        $method = 'set';
+        foreach (explode('_', $key) as $item) {
+            $method .= ucfirst($item);
+        }
+
+        if (method_exists($this, $method)) {
+            return $this->{$method}($value);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Whether a offset exists.
+     *
+     * @see https://php.net/manual/en/arrayaccess.offsetexists.php
+     *
+     * @param mixed $offset an offset to check for
+     *
+     * @return bool true on success or false on failure.
+     *
+     * The return value will be casted to boolean if non-boolean was returned.
+     */
+    public function offsetExists($offset)
+    {
+        return !is_null($this->get($offset));
+    }
+
+    /**
+     * Offset to retrieve.
+     *
+     * @see https://php.net/manual/en/arrayaccess.offsetget.php
+     *
+     * @param mixed $offset the offset to retrieve
+     *
+     * @return mixed can return all value types
+     */
+    public function offsetGet($offset)
+    {
+        return $this->get($offset);
+    }
+
+    /**
+     * Offset to set.
+     *
+     * @see https://php.net/manual/en/arrayaccess.offsetset.php
+     *
+     * @param mixed $offset the offset to assign the value to
+     * @param mixed $value  the value to set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value)
+    {
+        $this->set($offset, $value);
+    }
+
+    /**
+     * Offset to unset.
+     *
+     * @see https://php.net/manual/en/arrayaccess.offsetunset.php
+     *
+     * @param mixed $offset the offset to unset
+     *
+     * @return void
+     */
+    public function offsetUnset($offset)
+    {
+    }
+}

+ 32 - 0
addons/epay/library/Yansongda/Supports/Traits/Arrayable.php

@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Yansongda\Supports\Traits;
+
+use ReflectionClass;
+use Yansongda\Supports\Str;
+
+trait Arrayable
+{
+    /**
+     * toArray.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @throws \ReflectionException
+     */
+    public function toArray(): array
+    {
+        $result = [];
+
+        foreach ((new ReflectionClass($this))->getProperties() as $item) {
+            $k = $item->getName();
+            $method = 'get'.Str::studly($k);
+
+            $result[Str::snake($k)] = method_exists($this, $method) ? $this->{$method}() : $this->{$k};
+        }
+
+        return $result;
+    }
+}

+ 229 - 0
addons/epay/library/Yansongda/Supports/Traits/HasHttpRequest.php

@@ -0,0 +1,229 @@
+<?php
+
+namespace Yansongda\Supports\Traits;
+
+use GuzzleHttp\Client;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Trait HasHttpRequest.
+ *
+ * @property string $baseUri
+ * @property float  $timeout
+ * @property float  $connectTimeout
+ */
+trait HasHttpRequest
+{
+    /**
+     * Http client.
+     *
+     * @var Client|null
+     */
+    protected $httpClient = null;
+
+    /**
+     * Http client options.
+     *
+     * @var array
+     */
+    protected $httpOptions = [];
+
+    /**
+     * Send a GET request.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return array|string
+     */
+    public function get(string $endpoint, array $query = [], array $headers = [])
+    {
+        return $this->request('get', $endpoint, [
+            'headers' => $headers,
+            'query' => $query,
+        ]);
+    }
+
+    /**
+     * Send a POST request.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string|array $data
+     *
+     * @return array|string
+     */
+    public function post(string $endpoint, $data, array $options = [])
+    {
+        if (!is_array($data)) {
+            $options['body'] = $data;
+        } else {
+            $options['form_params'] = $data;
+        }
+
+        return $this->request('post', $endpoint, $options);
+    }
+
+    /**
+     * Send request.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return array|string
+     */
+    public function request(string $method, string $endpoint, array $options = [])
+    {
+        return $this->unwrapResponse($this->getHttpClient()->{$method}($endpoint, $options));
+    }
+
+    /**
+     * Set http client.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return $this
+     */
+    public function setHttpClient(Client $client): self
+    {
+        $this->httpClient = $client;
+
+        return $this;
+    }
+
+    /**
+     * Return http client.
+     */
+    public function getHttpClient(): Client
+    {
+        if (is_null($this->httpClient)) {
+            $this->httpClient = $this->getDefaultHttpClient();
+        }
+
+        return $this->httpClient;
+    }
+
+    /**
+     * Get default http client.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function getDefaultHttpClient(): Client
+    {
+        return new Client($this->getOptions());
+    }
+
+    /**
+     * setBaseUri.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return $this
+     */
+    public function setBaseUri(string $url): self
+    {
+        if (property_exists($this, 'baseUri')) {
+            $parsedUrl = parse_url($url);
+
+            $this->baseUri = ($parsedUrl['scheme'] ?? 'http').'://'.
+                $parsedUrl['host'].(isset($parsedUrl['port']) ? (':'.$parsedUrl['port']) : '');
+        }
+
+        return $this;
+    }
+
+    /**
+     * getBaseUri.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function getBaseUri(): string
+    {
+        return property_exists($this, 'baseUri') ? $this->baseUri : '';
+    }
+
+    public function getTimeout(): float
+    {
+        return property_exists($this, 'timeout') ? $this->timeout : 5.0;
+    }
+
+    public function setTimeout(float $timeout): self
+    {
+        if (property_exists($this, 'timeout')) {
+            $this->timeout = $timeout;
+        }
+
+        return $this;
+    }
+
+    public function getConnectTimeout(): float
+    {
+        return property_exists($this, 'connectTimeout') ? $this->connectTimeout : 3.0;
+    }
+
+    public function setConnectTimeout(float $connectTimeout): self
+    {
+        if (property_exists($this, 'connectTimeout')) {
+            $this->connectTimeout = $connectTimeout;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get default options.
+     *
+     * @author yansongda <me@yansongda.cn>
+     */
+    public function getOptions(): array
+    {
+        return array_merge([
+            'base_uri' => $this->getBaseUri(),
+            'timeout' => $this->getTimeout(),
+            'connect_timeout' => $this->getConnectTimeout(),
+        ], $this->getHttpOptions());
+    }
+
+    /**
+     * setOptions.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return $this
+     */
+    public function setOptions(array $options): self
+    {
+        return $this->setHttpOptions($options);
+    }
+
+    public function getHttpOptions(): array
+    {
+        return $this->httpOptions;
+    }
+
+    public function setHttpOptions(array $httpOptions): self
+    {
+        $this->httpOptions = $httpOptions;
+
+        return $this;
+    }
+
+    /**
+     * Convert response.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return array|string
+     */
+    public function unwrapResponse(ResponseInterface $response)
+    {
+        $contentType = $response->getHeaderLine('Content-Type');
+        $contents = $response->getBody()->getContents();
+
+        if (false !== stripos($contentType, 'json') || stripos($contentType, 'javascript')) {
+            return json_decode($contents, true);
+        } elseif (false !== stripos($contentType, 'xml')) {
+            return json_decode(json_encode(simplexml_load_string($contents, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true);
+        }
+
+        return $contents;
+    }
+}

+ 85 - 0
addons/epay/library/Yansongda/Supports/Traits/Serializable.php

@@ -0,0 +1,85 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Yansongda\Supports\Traits;
+
+use RuntimeException;
+
+trait Serializable
+{
+    /**
+     * toJson.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @return string
+     */
+    public function toJson()
+    {
+        return $this->serialize();
+    }
+
+    /**
+     * Specify data which should be serialized to JSON.
+     *
+     * @see   https://php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed data which can be serialized by <b>json_encode</b>,
+     *               which is a value of any type other than a resource
+     *
+     * @since 5.4.0
+     */
+    public function jsonSerialize()
+    {
+        if (method_exists($this, 'toArray')) {
+            return $this->toArray();
+        }
+
+        return [];
+    }
+
+    /**
+     * String representation of object.
+     *
+     * @see   https://php.net/manual/en/serializable.serialize.php
+     *
+     * @return string the string representation of the object or null
+     *
+     * @since 5.1.0
+     */
+    public function serialize()
+    {
+        if (method_exists($this, 'toArray')) {
+            return json_encode($this->toArray());
+        }
+
+        return json_encode([]);
+    }
+
+    /**
+     * Constructs the object.
+     *
+     * @see   https://php.net/manual/en/serializable.unserialize.php
+     *
+     * @param string $serialized <p>
+     *                           The string representation of the object.
+     *                           </p>
+     *
+     * @since 5.1.0
+     */
+    public function unserialize($serialized)
+    {
+        $data = json_decode($serialized, true);
+
+        if (JSON_ERROR_NONE !== json_last_error()) {
+            throw new RuntimeException('Invalid Json Format');
+        }
+
+        foreach ($data as $key => $item) {
+            if (method_exists($this, 'set')) {
+                $this->set($key, $item);
+            }
+        }
+    }
+}

+ 147 - 0
addons/epay/library/Yansongda/Supports/Traits/ShouldThrottle.php

@@ -0,0 +1,147 @@
+<?php
+
+namespace Yansongda\Supports\Traits;
+
+use Predis\Client;
+
+/**
+ * Trait ShouldThrottle.
+ *
+ * @property Client $redis
+ */
+trait ShouldThrottle
+{
+    /**
+     * _throttle.
+     *
+     * @var array
+     */
+    protected $_throttle = [
+        'limit' => 60,
+        'period' => 60,
+        'count' => 0,
+        'reset_time' => 0,
+    ];
+
+    /**
+     * isThrottled.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $key
+     * @param int    $limit
+     * @param int    $period
+     * @param bool   $auto_add
+     *
+     * @return bool
+     */
+    public function isThrottled($key, $limit = 60, $period = 60, $auto_add = false)
+    {
+        if (-1 === $limit) {
+            return false;
+        }
+
+        $now = microtime(true) * 1000;
+
+        $this->redis->zremrangebyscore($key, 0, $now - $period * 1000);
+
+        $this->_throttle = [
+            'limit' => $limit,
+            'period' => $period,
+            'count' => $this->getThrottleCounts($key, $period),
+            'reset_time' => $this->getThrottleResetTime($key, $now),
+        ];
+
+        if ($this->_throttle['count'] < $limit) {
+            if ($auto_add) {
+                $this->throttleAdd($key, $period);
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * 限流 + 1.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $key
+     * @param int    $period
+     */
+    public function throttleAdd($key, $period = 60)
+    {
+        $now = microtime(true) * 1000;
+
+        $this->redis->zadd($key, [$now => $now]);
+        $this->redis->expire($key, $period * 2);
+    }
+
+    /**
+     * getResetTime.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param $key
+     * @param $now
+     *
+     * @return int
+     */
+    public function getThrottleResetTime($key, $now)
+    {
+        $data = $this->redis->zrangebyscore(
+            $key,
+            $now - $this->_throttle['period'] * 1000,
+            $now,
+            ['limit' => [0, 1]]
+        );
+
+        if (0 === count($data)) {
+            return $this->_throttle['reset_time'] = time() + $this->_throttle['period'];
+        }
+
+        return intval($data[0] / 1000) + $this->_throttle['period'];
+    }
+
+    /**
+     * 获取限流相关信息.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string|null $key
+     * @param mixed|null  $default
+     *
+     * @return array|null
+     */
+    public function getThrottleInfo($key = null, $default = null)
+    {
+        if (is_null($key)) {
+            return $this->_throttle;
+        }
+
+        if (isset($this->_throttle[$key])) {
+            return $this->_throttle[$key];
+        }
+
+        return $default;
+    }
+
+    /**
+     * 获取已使用次数.
+     *
+     * @author yansongda <me@yansongda.cn>
+     *
+     * @param string $key
+     * @param int    $period
+     *
+     * @return string
+     */
+    public function getThrottleCounts($key, $period = 60)
+    {
+        $now = microtime(true) * 1000;
+
+        return $this->redis->zcount($key, $now - $period * 1000, $now);
+    }
+}

+ 47 - 0
addons/epay/view/api/alipay.html

@@ -0,0 +1,47 @@
+<div class="container">
+    <h2 class="scanpay-title">
+        <img src="__ADDON__/images/logo-alipay.png" alt="" height="32" class="pull-left" style="margin-right:5px;"> 支付宝支付
+        <div class="scanpay-time">
+            请在 <span>60</span> 秒内完成支付
+        </div>
+    </h2>
+
+    <div class="scanpay scanpay-alipay">
+        <div class="row">
+            <div class="col-xs-12 col-sm-12">
+                <div class="row">
+                    <div class="col-xs-12 col-sm-5">
+                        <div class="scanpay-body">
+                            <div class="scanpay-order clearfix">
+                                <p>订单标题:<em>{$orderData.title}</em></p>
+                                <p>订单编号:<em>{$orderData.orderid}</em></p>
+                                <p>订单价格:<em class="scanpay-price">¥{$orderData.amount}</em> 元</p>
+                            </div>
+                            <div class="scanpay-qrcode">
+                                <div class="qrcode" data-text="{$payData.qr_code}"></div>
+                                <div class="expired hidden"></div>
+                                <div class="paid hidden"></div>
+                            </div>
+                            <div class="scanpay-tips">
+                                <p>请使用支付宝扫一扫<br>扫描二维码进行支付</p>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="col-sm-1"></div>
+                    <div class="col-sm-6 hidden-xs">
+                        <div class="scanpay-screenshot">
+                            <img src="__ADDON__/images/screenshot-alipay.png" class="img-responsive" alt=""/>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+</div>
+
+<!--@formatter:off-->
+<script>
+    var queryParams = {"paytype":"alipay", "orderid":"{$orderData.orderid}", "returnurl":"{$orderData.returnurl}"};
+</script>
+<!--@formatter:on-->

+ 90 - 0
addons/epay/view/api/wechat.html

@@ -0,0 +1,90 @@
+{if $type=='jsapi'}
+<div class="container">
+    <div class="row" style="margin-top:20px;">
+        <div class="col-xs-12">
+            <button type="button" class="btn btn-success btn-lg btn-block">正在发起微信支付</button>
+            <button type="button" class="btn btn-default btn-lg btn-block" onclick="location.href='{$orderData.returnurl}'">如果页面未自动跳转</button>
+        </div>
+    </div>
+</div>
+<!--@formatter:off-->
+<script>
+    function onBridgeReady() {
+        WeixinJSBridge.invoke('getBrandWCPayRequest', {$payData|json_encode}, function(res) {
+            if (res.err_msg == "get_brand_wcpay_request:ok") {
+                layer.msg('支付成功!');
+            } else if (res.err_msg == "get_brand_wcpay_request:cancel") {
+                layer.msg('您取消了支付');
+            } else if (res.err_msg == "get_brand_wcpay_request:fail") {
+                layer.msg('支付失败');
+            }else{
+                layer.msg(typeof res.err_msg!='undefined' ? res.err_msg : (typeof res.errMsg !=='undefined' ? res.errMsg : "未知支付状态"));
+            }
+            setTimeout(function () {
+                location.href = '{$orderData.returnurl}';
+            }, 1500);
+        });
+    }
+
+    if (typeof WeixinJSBridge == "undefined") {
+        if (document.addEventListener) {
+            document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
+        } else if (document.attachEvent) {
+            document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
+            document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
+        }
+    } else {
+        onBridgeReady();
+    }
+</script>
+<!--@formatter:on-->
+
+{elseif $type=='pc' /}
+<div class="container">
+    <h2 class="scanpay-title">
+        <img src="__ADDON__/images/logo-wechat.png" alt="" height="32" class="pull-left" style="margin-right:5px;"> 微信支付
+        <div class="scanpay-time">
+            请在 <span>60</span> 秒内完成支付
+        </div>
+    </h2>
+
+    <div class="scanpay scanpay-wechat">
+        <div class="row">
+            <div class="col-xs-12 col-sm-12">
+                <div class="row">
+                    <div class="col-xs-12 col-sm-5">
+                        <div class="scanpay-body">
+                            <div class="scanpay-order clearfix">
+                                <p>订单标题:<em>{$orderData.title}</em></p>
+                                <p>订单编号:<em>{$orderData.orderid}</em></p>
+                                <p>订单价格:<em class="scanpay-price">¥{$orderData.amount}</em> 元</p>
+                            </div>
+                            <div class="scanpay-qrcode">
+                                <div class="qrcode" data-text="{$payData.code_url}"></div>
+                                <div class="expired hidden"></div>
+                                <div class="paid hidden"></div>
+                            </div>
+                            <div class="scanpay-tips">
+                                <p>请使用微信扫一扫<br>扫描二维码进行支付</p>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="col-sm-1"></div>
+                    <div class="col-sm-6 hidden-xs">
+                        <div class="scanpay-screenshot">
+                            <img src="__ADDON__/images/screenshot-wechat.png" class="img-responsive" alt=""/>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+</div>
+
+<!--@formatter:off-->
+<script>
+    var queryParams = {"paytype":"wechat", "orderid":"{$orderData.orderid}", "returnurl":"{$orderData.returnurl}"};
+</script>
+<!--@formatter:on-->
+{/if}

File diff suppressed because it is too large
+ 17 - 0
addons/epay/view/index/index.html


+ 104 - 0
addons/epay/view/layout/default.html

@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta name="description" content="">
+    <meta name="author" content="">
+
+    <title>{$title} - {$site.name}</title>
+
+    <link href="__CDN__/assets/libs/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
+    <link href="__ADDON__/css/common.css?v={$site.version}" rel="stylesheet">
+    <link href="__CDN__/assets/libs/font-awesome/css/font-awesome.min.css" rel="stylesheet">
+
+    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
+    <!--[if lt IE 9]>
+    <script src="https://cdn.staticfile.org/html5shiv/3.7.3/html5shiv.min.js"></script>
+    <script src="https://cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script>
+    <![endif]-->
+
+</head>
+
+<body>
+
+<!-- Navigation -->
+<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
+    <div class="container">
+        <!-- Brand and toggle get grouped for better mobile display -->
+        <div class="navbar-header">
+            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
+                <span class="sr-only">Toggle navigation</span>
+                <span class="icon-bar"></span>
+                <span class="icon-bar"></span>
+                <span class="icon-bar"></span>
+            </button>
+            <a class="navbar-brand" href="{:addon_url('epay/index/index')}">{$site.name}</a>
+        </div>
+        <!-- Collect the nav links, forms, and other content for toggling -->
+        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
+            <ul class="nav navbar-nav navbar-right">
+                <li>
+                </li>
+                {if $user}
+                <li class="dropdown">
+                    <a href="#" class="dropdown-toggle" data-toggle="dropdown">欢迎你! {$user.nickname|htmlentities}<b class="caret"></b></a>
+                    <ul class="dropdown-menu">
+                        <li>
+                            <a href="{:url('index/user/index')}">会员中心</a>
+                        </li>
+                        <li>
+                            <a href="{:url('index/user/profile')}">个人资料</a>
+                        </li>
+                        <li>
+                            <a href="{:url('index/user/logout')}">退出登录</a>
+                        </li>
+                    </ul>
+                </li>
+                {else /}
+                <li class="dropdown">
+                    <a href="{:url('index/user/index')}" class="dropdown-toggle" data-toggle="dropdown">会员中心 <b class="caret"></b></a>
+                    <ul class="dropdown-menu">
+                        <li>
+                            <a href="{:url('index/user/login')}">登录</a>
+                        </li>
+                        <li>
+                            <a href="{:url('index/user/register')}">注册</a>
+                        </li>
+                    </ul>
+                </li>
+                {/if}
+            </ul>
+        </div>
+        <!-- /.navbar-collapse -->
+    </div>
+    <!-- /.container -->
+</nav>
+
+{__CONTENT__}
+
+<div class="container">
+    <!-- Footer -->
+    <footer>
+        <div class="row">
+            <div class="col-lg-12">
+                <hr>
+                <p>Copyright &copy; {$site.name} 2017-2022</p>
+            </div>
+        </div>
+    </footer>
+
+</div>
+<!-- /.container -->
+
+<script src="__CDN__/assets/libs/jquery/dist/jquery.min.js"></script>
+<script src="__CDN__/assets/libs/bootstrap/dist/js/bootstrap.min.js"></script>
+<script src="__CDN__/assets/libs/fastadmin-layer/dist/layer.js?v={$site.version}"></script>
+<script src="__ADDON__/js/jquery.qrcode.min.js?v={$site.version}"></script>
+<script src="__ADDON__/js/common.js?v={$site.version}"></script>
+
+</body>
+</html>

+ 39 - 0
application/admin/controller/Epay.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use think\Config;
+
+class Epay extends Backend
+{
+    protected $noNeedRight = ['upload'];
+
+    /**
+     * 上传本地证书
+     * @return void
+     */
+    public function upload()
+    {
+        Config::set('default_return_type', 'json');
+
+        $certname = $this->request->post('certname', '');
+        $certPathArr = [
+            'cert_client'         => '/addons/epay/certs/apiclient_cert.pem', //微信支付api
+            'cert_key'            => '/addons/epay/certs/apiclient_key.pem', //微信支付api
+            'app_cert_public_key' => '/addons/epay/certs/appCertPublicKey.crt',//应用公钥证书路径
+            'alipay_root_cert'    => '/addons/epay/certs/alipayRootCert.crt', //支付宝根证书路径
+            'ali_public_key'      => '/addons/epay/certs/alipayCertPublicKey.crt', //支付宝公钥证书路径
+        ];
+        if (!isset($certPathArr[$certname])) {
+            $this->error("证书错误");
+        }
+        $url = $certPathArr[$certname];
+        $file = $this->request->file('file');
+        if (!$file) {
+            $this->error("未上传文件");
+        }
+        $file->move(dirname(ROOT_PATH . $url), basename(ROOT_PATH . $url), true);
+        $this->success(__('上传成功'), '', ['url' => $url]);
+    }
+}

+ 178 - 0
public/assets/addons/epay/css/common.css

@@ -0,0 +1,178 @@
+html,
+body {
+  height: 100%;
+}
+body {
+  padding-top: 50px;
+  /* Required padding for .navbar-fixed-top. Remove if using .navbar-static-top. Change if height of navigation changes. */
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+}
+.img-addon {
+  margin-bottom: 10px;
+  width: 100%;
+}
+.img-hover:hover {
+  opacity: 0.8;
+}
+.display-1 {
+  font-size: 44px;
+}
+.display-4 {
+  font-size: 24px;
+  line-height: 32px;
+}
+/* Home Page Carousel */
+header.carousel {
+  height: 50%;
+}
+header.carousel .item,
+header.carousel .item.active,
+header.carousel .carousel-inner {
+  height: 100%;
+}
+header.carousel .fill {
+  width: 100%;
+  height: 100%;
+}
+.error-404 {
+  font-size: 100px;
+}
+/* Pricing Page Styles */
+.price {
+  display: block;
+  font-size: 50px;
+  line-height: 50px;
+}
+.price sup {
+  top: -20px;
+  left: 2px;
+  font-size: 20px;
+}
+.period {
+  display: block;
+  font-style: italic;
+}
+/* Footer Styles */
+/* Responsive Styles */
+@media (max-width: 991px) {
+  .customer-img,
+  .img-related {
+    margin-bottom: 30px;
+  }
+}
+@media (max-width: 767px) {
+  .img-addon {
+    margin-bottom: 15px;
+  }
+  header.carousel .carousel {
+    height: 70%;
+  }
+}
+.carousel-body {
+  position: absolute;
+  width: 100%;
+  top: 25%;
+  text-align: center;
+  color: #fff;
+}
+.addonlist a > p {
+  margin-bottom: 15px;
+}
+/* PC扫码支付 */
+.scanpay {
+  margin-top: 20px;
+}
+.scanpay-title {
+  margin: 30px 0 15px 0;
+  padding-bottom: 15px;
+  border-bottom: 1px solid #eee;
+  position: relative;
+}
+.scanpay-qrcode {
+  margin-bottom: 20px;
+  position: relative;
+}
+.scanpay-qrcode img {
+  width: 100%;
+  border: 1px solid #eee;
+}
+.scanpay-qrcode .expired {
+  position: absolute;
+  top: 0;
+  left: 0;
+  height: 100%;
+  width: 100%;
+  opacity: .95;
+  background: #fff url(../images/expired.png) center center no-repeat;
+}
+.scanpay-qrcode .paid {
+  position: absolute;
+  top: 0;
+  left: 0;
+  height: 100%;
+  width: 100%;
+  opacity: .95;
+  background: #fff url(../images/paid.png) center center no-repeat;
+}
+.scanpay-screenshot {
+  padding: 0;
+}
+.scanpay-screenshot img {
+  width: 100%;
+}
+.scanpay-tips {
+  height: 60px;
+  padding: 8px 0 8px 125px;
+  background: #00c800 url(../images/scan.png) 50px 12px no-repeat;
+  background-size: 36px 36px;
+}
+.scanpay-tips p {
+  margin: 0;
+  font-size: 14px;
+  line-height: 22px;
+  color: #fff;
+  font-weight: 700;
+}
+.scanpay-time {
+  font-size: 14px;
+  margin-bottom: 15px;
+  position: absolute;
+  top: 15px;
+  right: 10px;
+  font-weight: normal;
+  display: none;
+}
+.scanpay-time span {
+  color: red;
+}
+.scanpay-order {
+  margin-bottom: 5px;
+}
+.scanpay-order em {
+  font-style: normal;
+  color: #666;
+}
+.scanpay-order em.scanpay-price {
+  color: #ff3333;
+  font-weight: bold;
+}
+.scanpay-alipay .scanpay-tips {
+  background-color: #4290e8;
+}
+@media (max-width: 767px) {
+  .scanpay {
+    margin-top: 20px;
+  }
+}
+@media (max-height: 855px) and (min-width: 767px) {
+  .scanpay {
+    width: calc(130vh);
+    min-width: 760px;
+  }
+}
+.qrcode canvas,
+.qrcode img {
+  width: 100%;
+}

BIN
public/assets/addons/epay/images/alipay.png


BIN
public/assets/addons/epay/images/expired.png


BIN
public/assets/addons/epay/images/logo-alipay.png


BIN
public/assets/addons/epay/images/logo-wechat.png


BIN
public/assets/addons/epay/images/paid.png


BIN
public/assets/addons/epay/images/scan.png


BIN
public/assets/addons/epay/images/screenshot-alipay.png


BIN
public/assets/addons/epay/images/screenshot-wechat.png


BIN
public/assets/addons/epay/images/wechat.png


+ 69 - 0
public/assets/addons/epay/js/common.js

@@ -0,0 +1,69 @@
+$(function () {
+
+    if ($('.carousel').length > 0) {
+        $('.carousel').carousel({
+            interval: 5000
+        });
+    }
+
+    if ($(".btn-experience").length > 0) {
+        $(".btn-experience").on("click", function () {
+            location.href = "/addons/epay/index/experience?amount=" + $("input[name=amount]").val() + "&type=" + $(this).data("type") + "&method=" + $("#method").val();
+        });
+    }
+
+    if ($(".qrcode").length > 0) {
+        $(".qrcode").qrcode({width: 250, height: 250, text: $(".qrcode").data("text")});
+    }
+
+    var si, xhr;
+    if (typeof queryParams != 'undefined') {
+        var queryResult = function () {
+            xhr && xhr.abort();
+            xhr = $.ajax({
+                url: "",
+                type: "post",
+                data: queryParams,
+                dataType: 'json',
+                success: function (ret) {
+                    if (ret.code == 1) {
+                        var data = ret.data;
+                        if (typeof data.status != 'undefined') {
+                            var status = data.status;
+                            if (status == 'SUCCESS' || status == 'TRADE_SUCCESS') {
+                                $(".scanpay-qrcode .paid").removeClass("hidden");
+                                $(".scanpay-tips p").html("支付成功!<br><span>3</span>秒后将自动跳转...");
+
+                                var sin = setInterval(function () {
+                                    $(".scanpay-tips p span").text(parseInt($(".scanpay-tips p span").text()) - 1);
+                                }, 1000);
+
+                                setTimeout(function () {
+                                    clearInterval(sin);
+                                    location.href = queryParams.returnurl;
+                                }, 3000);
+
+                                clearInterval(si);
+                            } else if (status == 'REFUND' || status == 'TRADE_CLOSED') {
+                                $(".scanpay-tips p").html("请求失败!<br>请返回重新发起支付");
+                                clearInterval(si);
+                            } else if (status == 'NOTPAY' || status == 'TRADE_NOT_EXIST') {
+                            } else if (status == 'CLOSED' || status == 'TRADE_CLOSED') {
+                                $(".scanpay-tips p").html("订单已关闭!<br>请返回重新发起支付");
+                                clearInterval(si);
+                            } else if (status == 'USERPAYING' || status == 'WAIT_BUYER_PAY') {
+                            } else if (status == 'PAYERROR') {
+                                clearInterval(si);
+                            }
+                        }
+                    }
+                }
+            });
+        };
+        si = setInterval(function () {
+            queryResult();
+        }, 3000);
+        queryResult();
+    }
+
+});

+ 34 - 0
public/assets/addons/epay/js/jquery.qrcode.min.js

@@ -0,0 +1,34 @@
+/**
+ * jQuery-qrcode
+ * @url https://github.com/jeromeetienne/jquery-qrcode
+ * @licence MIT
+ */
+
+(function(r){r.fn.qrcode=function(h){var s;function u(a){this.mode=s;this.data=a}function o(a,c){this.typeNumber=a;this.errorCorrectLevel=c;this.modules=null;this.moduleCount=0;this.dataCache=null;this.dataList=[]}function q(a,c){if(void 0==a.length)throw Error(a.length+"/"+c);for(var d=0;d<a.length&&0==a[d];)d++;this.num=Array(a.length-d+c);for(var b=0;b<a.length-d;b++)this.num[b]=a[b+d]}function p(a,c){this.totalCount=a;this.dataCount=c}function t(){this.buffer=[];this.length=0}u.prototype={getLength:function(){return this.data.length},
+    write:function(a){for(var c=0;c<this.data.length;c++)a.put(this.data.charCodeAt(c),8)}};o.prototype={addData:function(a){this.dataList.push(new u(a));this.dataCache=null},isDark:function(a,c){if(0>a||this.moduleCount<=a||0>c||this.moduleCount<=c)throw Error(a+","+c);return this.modules[a][c]},getModuleCount:function(){return this.moduleCount},make:function(){if(1>this.typeNumber){for(var a=1,a=1;40>a;a++){for(var c=p.getRSBlocks(a,this.errorCorrectLevel),d=new t,b=0,e=0;e<c.length;e++)b+=c[e].dataCount;
+        for(e=0;e<this.dataList.length;e++)c=this.dataList[e],d.put(c.mode,4),d.put(c.getLength(),j.getLengthInBits(c.mode,a)),c.write(d);if(d.getLengthInBits()<=8*b)break}this.typeNumber=a}this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17;this.modules=Array(this.moduleCount);for(var d=0;d<this.moduleCount;d++){this.modules[d]=Array(this.moduleCount);for(var b=0;b<this.moduleCount;b++)this.modules[d][b]=null}this.setupPositionProbePattern(0,0);this.setupPositionProbePattern(this.moduleCount-
+        7,0);this.setupPositionProbePattern(0,this.moduleCount-7);this.setupPositionAdjustPattern();this.setupTimingPattern();this.setupTypeInfo(a,c);7<=this.typeNumber&&this.setupTypeNumber(a);null==this.dataCache&&(this.dataCache=o.createData(this.typeNumber,this.errorCorrectLevel,this.dataList));this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,c){for(var d=-1;7>=d;d++)if(!(-1>=a+d||this.moduleCount<=a+d))for(var b=-1;7>=b;b++)-1>=c+b||this.moduleCount<=c+b||(this.modules[a+d][c+b]=
+        0<=d&&6>=d&&(0==b||6==b)||0<=b&&6>=b&&(0==d||6==d)||2<=d&&4>=d&&2<=b&&4>=b?!0:!1)},getBestMaskPattern:function(){for(var a=0,c=0,d=0;8>d;d++){this.makeImpl(!0,d);var b=j.getLostPoint(this);if(0==d||a>b)a=b,c=d}return c},createMovieClip:function(a,c,d){a=a.createEmptyMovieClip(c,d);this.make();for(c=0;c<this.modules.length;c++)for(var d=1*c,b=0;b<this.modules[c].length;b++){var e=1*b;this.modules[c][b]&&(a.beginFill(0,100),a.moveTo(e,d),a.lineTo(e+1,d),a.lineTo(e+1,d+1),a.lineTo(e,d+1),a.endFill())}return a},
+    setupTimingPattern:function(){for(var a=8;a<this.moduleCount-8;a++)null==this.modules[a][6]&&(this.modules[a][6]=0==a%2);for(a=8;a<this.moduleCount-8;a++)null==this.modules[6][a]&&(this.modules[6][a]=0==a%2)},setupPositionAdjustPattern:function(){for(var a=j.getPatternPosition(this.typeNumber),c=0;c<a.length;c++)for(var d=0;d<a.length;d++){var b=a[c],e=a[d];if(null==this.modules[b][e])for(var f=-2;2>=f;f++)for(var i=-2;2>=i;i++)this.modules[b+f][e+i]=-2==f||2==f||-2==i||2==i||0==f&&0==i?!0:!1}},setupTypeNumber:function(a){for(var c=
+        j.getBCHTypeNumber(this.typeNumber),d=0;18>d;d++){var b=!a&&1==(c>>d&1);this.modules[Math.floor(d/3)][d%3+this.moduleCount-8-3]=b}for(d=0;18>d;d++)b=!a&&1==(c>>d&1),this.modules[d%3+this.moduleCount-8-3][Math.floor(d/3)]=b},setupTypeInfo:function(a,c){for(var d=j.getBCHTypeInfo(this.errorCorrectLevel<<3|c),b=0;15>b;b++){var e=!a&&1==(d>>b&1);6>b?this.modules[b][8]=e:8>b?this.modules[b+1][8]=e:this.modules[this.moduleCount-15+b][8]=e}for(b=0;15>b;b++)e=!a&&1==(d>>b&1),8>b?this.modules[8][this.moduleCount-
+    b-1]=e:9>b?this.modules[8][15-b-1+1]=e:this.modules[8][15-b-1]=e;this.modules[this.moduleCount-8][8]=!a},mapData:function(a,c){for(var d=-1,b=this.moduleCount-1,e=7,f=0,i=this.moduleCount-1;0<i;i-=2)for(6==i&&i--;;){for(var g=0;2>g;g++)if(null==this.modules[b][i-g]){var n=!1;f<a.length&&(n=1==(a[f]>>>e&1));j.getMask(c,b,i-g)&&(n=!n);this.modules[b][i-g]=n;e--; -1==e&&(f++,e=7)}b+=d;if(0>b||this.moduleCount<=b){b-=d;d=-d;break}}}};o.PAD0=236;o.PAD1=17;o.createData=function(a,c,d){for(var c=p.getRSBlocks(a,
+    c),b=new t,e=0;e<d.length;e++){var f=d[e];b.put(f.mode,4);b.put(f.getLength(),j.getLengthInBits(f.mode,a));f.write(b)}for(e=a=0;e<c.length;e++)a+=c[e].dataCount;if(b.getLengthInBits()>8*a)throw Error("code length overflow. ("+b.getLengthInBits()+">"+8*a+")");for(b.getLengthInBits()+4<=8*a&&b.put(0,4);0!=b.getLengthInBits()%8;)b.putBit(!1);for(;!(b.getLengthInBits()>=8*a);){b.put(o.PAD0,8);if(b.getLengthInBits()>=8*a)break;b.put(o.PAD1,8)}return o.createBytes(b,c)};o.createBytes=function(a,c){for(var d=
+    0,b=0,e=0,f=Array(c.length),i=Array(c.length),g=0;g<c.length;g++){var n=c[g].dataCount,h=c[g].totalCount-n,b=Math.max(b,n),e=Math.max(e,h);f[g]=Array(n);for(var k=0;k<f[g].length;k++)f[g][k]=255&a.buffer[k+d];d+=n;k=j.getErrorCorrectPolynomial(h);n=(new q(f[g],k.getLength()-1)).mod(k);i[g]=Array(k.getLength()-1);for(k=0;k<i[g].length;k++)h=k+n.getLength()-i[g].length,i[g][k]=0<=h?n.get(h):0}for(k=g=0;k<c.length;k++)g+=c[k].totalCount;d=Array(g);for(k=n=0;k<b;k++)for(g=0;g<c.length;g++)k<f[g].length&&
+(d[n++]=f[g][k]);for(k=0;k<e;k++)for(g=0;g<c.length;g++)k<i[g].length&&(d[n++]=i[g][k]);return d};s=4;for(var j={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,
+        78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:1335,G18:7973,G15_MASK:21522,getBCHTypeInfo:function(a){for(var c=a<<10;0<=j.getBCHDigit(c)-j.getBCHDigit(j.G15);)c^=j.G15<<j.getBCHDigit(c)-j.getBCHDigit(j.G15);return(a<<10|c)^j.G15_MASK},getBCHTypeNumber:function(a){for(var c=a<<12;0<=j.getBCHDigit(c)-
+    j.getBCHDigit(j.G18);)c^=j.G18<<j.getBCHDigit(c)-j.getBCHDigit(j.G18);return a<<12|c},getBCHDigit:function(a){for(var c=0;0!=a;)c++,a>>>=1;return c},getPatternPosition:function(a){return j.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,c,d){switch(a){case 0:return 0==(c+d)%2;case 1:return 0==c%2;case 2:return 0==d%3;case 3:return 0==(c+d)%3;case 4:return 0==(Math.floor(c/2)+Math.floor(d/3))%2;case 5:return 0==c*d%2+c*d%3;case 6:return 0==(c*d%2+c*d%3)%2;case 7:return 0==(c*d%3+(c+d)%2)%2;default:throw Error("bad maskPattern:"+
+        a);}},getErrorCorrectPolynomial:function(a){for(var c=new q([1],0),d=0;d<a;d++)c=c.multiply(new q([1,l.gexp(d)],0));return c},getLengthInBits:function(a,c){if(1<=c&&10>c)switch(a){case 1:return 10;case 2:return 9;case s:return 8;case 8:return 8;default:throw Error("mode:"+a);}else if(27>c)switch(a){case 1:return 12;case 2:return 11;case s:return 16;case 8:return 10;default:throw Error("mode:"+a);}else if(41>c)switch(a){case 1:return 14;case 2:return 13;case s:return 16;case 8:return 12;default:throw Error("mode:"+
+        a);}else throw Error("type:"+c);},getLostPoint:function(a){for(var c=a.getModuleCount(),d=0,b=0;b<c;b++)for(var e=0;e<c;e++){for(var f=0,i=a.isDark(b,e),g=-1;1>=g;g++)if(!(0>b+g||c<=b+g))for(var h=-1;1>=h;h++)0>e+h||c<=e+h||0==g&&0==h||i==a.isDark(b+g,e+h)&&f++;5<f&&(d+=3+f-5)}for(b=0;b<c-1;b++)for(e=0;e<c-1;e++)if(f=0,a.isDark(b,e)&&f++,a.isDark(b+1,e)&&f++,a.isDark(b,e+1)&&f++,a.isDark(b+1,e+1)&&f++,0==f||4==f)d+=3;for(b=0;b<c;b++)for(e=0;e<c-6;e++)a.isDark(b,e)&&!a.isDark(b,e+1)&&a.isDark(b,e+
+        2)&&a.isDark(b,e+3)&&a.isDark(b,e+4)&&!a.isDark(b,e+5)&&a.isDark(b,e+6)&&(d+=40);for(e=0;e<c;e++)for(b=0;b<c-6;b++)a.isDark(b,e)&&!a.isDark(b+1,e)&&a.isDark(b+2,e)&&a.isDark(b+3,e)&&a.isDark(b+4,e)&&!a.isDark(b+5,e)&&a.isDark(b+6,e)&&(d+=40);for(e=f=0;e<c;e++)for(b=0;b<c;b++)a.isDark(b,e)&&f++;a=Math.abs(100*f/c/c-50)/5;return d+10*a}},l={glog:function(a){if(1>a)throw Error("glog("+a+")");return l.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;256<=a;)a-=255;return l.EXP_TABLE[a]},EXP_TABLE:Array(256),
+    LOG_TABLE:Array(256)},m=0;8>m;m++)l.EXP_TABLE[m]=1<<m;for(m=8;256>m;m++)l.EXP_TABLE[m]=l.EXP_TABLE[m-4]^l.EXP_TABLE[m-5]^l.EXP_TABLE[m-6]^l.EXP_TABLE[m-8];for(m=0;255>m;m++)l.LOG_TABLE[l.EXP_TABLE[m]]=m;q.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var c=Array(this.getLength()+a.getLength()-1),d=0;d<this.getLength();d++)for(var b=0;b<a.getLength();b++)c[d+b]^=l.gexp(l.glog(this.get(d))+l.glog(a.get(b)));return new q(c,0)},mod:function(a){if(0>
+        this.getLength()-a.getLength())return this;for(var c=l.glog(this.get(0))-l.glog(a.get(0)),d=Array(this.getLength()),b=0;b<this.getLength();b++)d[b]=this.get(b);for(b=0;b<a.getLength();b++)d[b]^=l.gexp(l.glog(a.get(b))+c);return(new q(d,0)).mod(a)}};p.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],
+    [4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,
+        116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,
+        43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,
+        3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,
+        55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,
+        45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]];p.getRSBlocks=function(a,c){var d=p.getRsBlockTable(a,c);if(void 0==d)throw Error("bad rs block @ typeNumber:"+a+"/errorCorrectLevel:"+c);for(var b=d.length/3,e=[],f=0;f<b;f++)for(var h=d[3*f+0],g=d[3*f+1],j=d[3*f+2],l=0;l<h;l++)e.push(new p(g,j));return e};p.getRsBlockTable=function(a,c){switch(c){case 1:return p.RS_BLOCK_TABLE[4*(a-1)+0];case 0:return p.RS_BLOCK_TABLE[4*(a-1)+1];case 3:return p.RS_BLOCK_TABLE[4*
+(a-1)+2];case 2:return p.RS_BLOCK_TABLE[4*(a-1)+3]}};t.prototype={get:function(a){return 1==(this.buffer[Math.floor(a/8)]>>>7-a%8&1)},put:function(a,c){for(var d=0;d<c;d++)this.putBit(1==(a>>>c-d-1&1))},getLengthInBits:function(){return this.length},putBit:function(a){var c=Math.floor(this.length/8);this.buffer.length<=c&&this.buffer.push(0);a&&(this.buffer[c]|=128>>>this.length%8);this.length++}};"string"===typeof h&&(h={text:h});h=r.extend({},{render:"canvas",width:256,height:256,typeNumber:-1,
+    correctLevel:2,background:"#ffffff",foreground:"#000000"},h);return this.each(function(){var a;if("canvas"==h.render){a=new o(h.typeNumber,h.correctLevel);a.addData(h.text);a.make();var c=document.createElement("canvas");c.width=h.width;c.height=h.height;for(var d=c.getContext("2d"),b=h.width/a.getModuleCount(),e=h.height/a.getModuleCount(),f=0;f<a.getModuleCount();f++)for(var i=0;i<a.getModuleCount();i++){d.fillStyle=a.isDark(f,i)?h.foreground:h.background;var g=Math.ceil((i+1)*b)-Math.floor(i*b),
+    j=Math.ceil((f+1)*b)-Math.floor(f*b);d.fillRect(Math.round(i*b),Math.round(f*e),g,j)}}else{a=new o(h.typeNumber,h.correctLevel);a.addData(h.text);a.make();c=r("<table></table>").css("width",h.width+"px").css("height",h.height+"px").css("border","0px").css("border-collapse","collapse").css("background-color",h.background);d=h.width/a.getModuleCount();b=h.height/a.getModuleCount();for(e=0;e<a.getModuleCount();e++){f=r("<tr></tr>").css("height",b+"px").appendTo(c);for(i=0;i<a.getModuleCount();i++)r("<td></td>").css("width",
+    d+"px").css("background-color",a.isDark(e,i)?h.foreground:h.background).appendTo(f)}}a=c;jQuery(a).appendTo(this)})}})(jQuery);

+ 224 - 0
public/assets/addons/epay/less/common.less

@@ -0,0 +1,224 @@
+html,
+body {
+    height: 100%;
+}
+
+body {
+    padding-top: 50px; /* Required padding for .navbar-fixed-top. Remove if using .navbar-static-top. Change if height of navigation changes. */
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+    font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+
+}
+
+.img-addon {
+    margin-bottom: 10px;
+    width: 100%;
+}
+
+.img-hover:hover {
+    opacity: 0.8;
+}
+
+.display-1 {
+    font-size: 44px;
+}
+
+.display-4 {
+    font-size: 24px;
+    line-height: 32px;
+}
+
+/* Home Page Carousel */
+
+header.carousel {
+    height: 50%;
+}
+
+header.carousel .item,
+header.carousel .item.active,
+header.carousel .carousel-inner {
+    height: 100%;
+}
+
+header.carousel .fill {
+    width: 100%;
+    height: 100%;
+}
+
+.error-404 {
+    font-size: 100px;
+}
+
+/* Pricing Page Styles */
+
+.price {
+    display: block;
+    font-size: 50px;
+    line-height: 50px;
+}
+
+.price sup {
+    top: -20px;
+    left: 2px;
+    font-size: 20px;
+}
+
+.period {
+    display: block;
+    font-style: italic;
+}
+
+/* Footer Styles */
+
+footer {
+}
+
+/* Responsive Styles */
+
+@media (max-width: 991px) {
+    .customer-img,
+    .img-related {
+        margin-bottom: 30px;
+    }
+}
+
+@media (max-width: 767px) {
+    .img-addon {
+        margin-bottom: 15px;
+    }
+
+    header.carousel .carousel {
+        height: 70%;
+    }
+}
+
+.carousel-body {
+    position: absolute;
+    width: 100%;
+    top: 25%;
+    text-align: center;
+    color: #fff;
+}
+
+.addonlist a > p {
+    margin-bottom: 15px;
+}
+
+/* PC扫码支付 */
+
+.scanpay {
+    margin-top: 20px;
+}
+.scanpay-title {
+    margin: 30px 0 15px 0;
+    padding-bottom: 15px;
+    border-bottom: 1px solid #eee;
+    position: relative;
+}
+
+.scanpay-qrcode {
+    margin-bottom: 20px;
+    position: relative;
+
+    img {
+        width: 100%;
+        border: 1px solid #eee;
+    }
+
+    .expired {
+        position: absolute;
+        top: 0;
+        left: 0;
+        height: 100%;
+        width: 100%;
+        opacity: .95;
+        background: #fff url(../images/expired.png) center center no-repeat;
+    }
+
+    .paid {
+        position: absolute;
+        top: 0;
+        left: 0;
+        height: 100%;
+        width: 100%;
+        opacity: .95;
+        background: #fff url(../images/paid.png) center center no-repeat;
+    }
+}
+
+
+.scanpay-screenshot {
+    padding: 0;
+
+    img {
+        width: 100%;
+    }
+}
+
+.scanpay-tips {
+    height: 60px;
+    padding: 8px 0 8px 125px;
+    background: #00c800 url(../images/scan.png) 50px 12px no-repeat;
+    background-size: 36px 36px;
+
+    p {
+        margin: 0;
+        font-size: 14px;
+        line-height: 22px;
+        color: #fff;
+        font-weight: 700
+    }
+}
+
+.scanpay-time {
+    font-size: 14px;
+    margin-bottom: 15px;
+    position: absolute;
+    top: 15px;
+    right: 10px;
+    font-weight: normal;
+    display: none;
+
+    span {
+        color: red;
+    }
+
+}
+
+.scanpay-order {
+    margin-bottom: 5px;
+
+    em {
+        font-style: normal;
+        color: #666;
+
+        &.scanpay-price {
+            color: #ff3333;
+            font-weight: bold;
+        }
+    }
+}
+
+.scanpay-alipay {
+    .scanpay-tips {
+        background-color: #4290e8;
+    }
+}
+
+@media (max-width: 767px) {
+    .scanpay {
+        margin-top: 20px;
+    }
+}
+
+@media (max-height: 855px) and (min-width: 767px) {
+    .scanpay {
+        width: calc(~ '130vh');
+        min-width: 760px;
+    }
+}
+
+.qrcode canvas, .qrcode img {
+    width: 100%;
+}

Some files were not shown because too many files changed in this diff