Browse Source

epay插件

lizhen_gitee 3 years ago
parent
commit
02c3b71a54
99 changed files with 10364 additions and 1 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. 366 0
      addons/epay/config.html
  9. 67 0
      addons/epay/config.php
  10. 243 0
      addons/epay/controller/Api.php
  11. 111 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. 1856 0
      addons/epay/library/QRCode.php
  16. 58 0
      addons/epay/library/RedirectResponse.php
  17. 26 0
      addons/epay/library/Response.php
  18. 301 0
      addons/epay/library/Service.php
  19. 110 0
      addons/epay/library/Wechat.php
  20. 83 0
      addons/epay/library/Yansongda/Pay/Contracts/GatewayApplicationInterface.php
  21. 20 0
      addons/epay/library/Yansongda/Pay/Contracts/GatewayInterface.php
  22. 98 0
      addons/epay/library/Yansongda/Pay/Events.php
  23. 31 0
      addons/epay/library/Yansongda/Pay/Events/ApiRequested.php
  24. 31 0
      addons/epay/library/Yansongda/Pay/Events/ApiRequesting.php
  25. 40 0
      addons/epay/library/Yansongda/Pay/Events/Event.php
  26. 33 0
      addons/epay/library/Yansongda/Pay/Events/MethodCalled.php
  27. 31 0
      addons/epay/library/Yansongda/Pay/Events/PayStarted.php
  28. 23 0
      addons/epay/library/Yansongda/Pay/Events/PayStarting.php
  29. 25 0
      addons/epay/library/Yansongda/Pay/Events/RequestReceived.php
  30. 25 0
      addons/epay/library/Yansongda/Pay/Events/SignFailed.php
  31. 19 0
      addons/epay/library/Yansongda/Pay/Exceptions/BusinessException.php
  32. 44 0
      addons/epay/library/Yansongda/Pay/Exceptions/Exception.php
  33. 20 0
      addons/epay/library/Yansongda/Pay/Exceptions/GatewayException.php
  34. 19 0
      addons/epay/library/Yansongda/Pay/Exceptions/InvalidArgumentException.php
  35. 19 0
      addons/epay/library/Yansongda/Pay/Exceptions/InvalidConfigException.php
  36. 19 0
      addons/epay/library/Yansongda/Pay/Exceptions/InvalidGatewayException.php
  37. 19 0
      addons/epay/library/Yansongda/Pay/Exceptions/InvalidSignException.php
  38. 422 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay.php
  39. 38 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/AppGateway.php
  40. 40 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/Gateway.php
  41. 46 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/MiniGateway.php
  42. 47 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/PosGateway.php
  43. 21 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/RefundGateway.php
  44. 41 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/ScanGateway.php
  45. 452 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/Support.php
  46. 49 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/TransferGateway.php
  47. 26 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/WapGateway.php
  48. 104 0
      addons/epay/library/Yansongda/Pay/Gateways/Alipay/WebGateway.php
  49. 366 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat.php
  50. 62 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/AppGateway.php
  51. 88 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/Gateway.php
  52. 57 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/GroupRedpackGateway.php
  53. 35 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/MiniappGateway.php
  54. 59 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/MpGateway.php
  55. 44 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/PosGateway.php
  56. 61 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/RedpackGateway.php
  57. 50 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/RefundGateway.php
  58. 44 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/ScanGateway.php
  59. 449 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/Support.php
  60. 80 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/TransferGateway.php
  61. 47 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/WapGateway.php
  62. 86 0
      addons/epay/library/Yansongda/Pay/Gateways/Wechat/WebGateway.php
  63. 20 0
      addons/epay/library/Yansongda/Pay/LICENSE
  64. 114 0
      addons/epay/library/Yansongda/Pay/Listeners/KernelLogSubscriber.php
  65. 49 0
      addons/epay/library/Yansongda/Pay/Log.php
  66. 131 0
      addons/epay/library/Yansongda/Pay/Pay.php
  67. 605 0
      addons/epay/library/Yansongda/Supports/Arr.php
  68. 363 0
      addons/epay/library/Yansongda/Supports/Collection.php
  69. 7 0
      addons/epay/library/Yansongda/Supports/Config.php
  70. 20 0
      addons/epay/library/Yansongda/Supports/LICENSE
  71. 91 0
      addons/epay/library/Yansongda/Supports/Log.php
  72. 240 0
      addons/epay/library/Yansongda/Supports/Logger.php
  73. 36 0
      addons/epay/library/Yansongda/Supports/Logger/StdoutHandler.php
  74. 570 0
      addons/epay/library/Yansongda/Supports/Str.php
  75. 142 0
      addons/epay/library/Yansongda/Supports/Traits/Accessable.php
  76. 32 0
      addons/epay/library/Yansongda/Supports/Traits/Arrayable.php
  77. 229 0
      addons/epay/library/Yansongda/Supports/Traits/HasHttpRequest.php
  78. 85 0
      addons/epay/library/Yansongda/Supports/Traits/Serializable.php
  79. 147 0
      addons/epay/library/Yansongda/Supports/Traits/ShouldThrottle.php
  80. 47 0
      addons/epay/view/api/alipay.html
  81. 90 0
      addons/epay/view/api/wechat.html
  82. 12 0
      addons/epay/view/index/index.html
  83. 103 0
      addons/epay/view/layout/default.html
  84. 39 0
      application/admin/controller/Epay.php
  85. 5 1
      application/extra/addons.php
  86. 180 0
      public/assets/addons/epay/css/common.css
  87. 20 0
      public/assets/addons/epay/css/epay.css
  88. BIN
      public/assets/addons/epay/images/alipay.png
  89. BIN
      public/assets/addons/epay/images/expired.png
  90. BIN
      public/assets/addons/epay/images/logo-alipay.png
  91. BIN
      public/assets/addons/epay/images/logo-wechat.png
  92. BIN
      public/assets/addons/epay/images/paid.png
  93. BIN
      public/assets/addons/epay/images/scan.png
  94. BIN
      public/assets/addons/epay/images/screenshot-alipay.png
  95. BIN
      public/assets/addons/epay/images/screenshot-wechat.png
  96. BIN
      public/assets/addons/epay/images/wechat.png
  97. 65 0
      public/assets/addons/epay/js/common.js
  98. 229 0
      public/assets/addons/epay/less/common.less
  99. 28 0
      public/assets/addons/epay/less/epay.less

+ 1 - 0
addons/epay/.addonrc

@@ -0,0 +1 @@
+{"license":"regular","licenseto":"38239","licensekey":"p1vu6GkD0ch3lgz8 TpBekP\/wYE7mITx1zJYPlQ==","files":["application\\admin\\controller\\Epay.php","public\\assets\\addons\\epay\\css\\common.css","public\\assets\\addons\\epay\\css\\epay.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\\less\\common.less","public\\assets\\addons\\epay\\less\\epay.less"]}

+ 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


+ 366 - 0
addons/epay/config.html

@@ -0,0 +1,366 @@
+<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 appid</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 appid</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>子商户公众号的appid</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>子商户小程序的appid</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 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">
+                                        {:Form::radios('row[alipay][mode]',['normal'=>'正式环境','dev'=>'沙箱环境'],$item.value.mode??'normal')}
+                                    </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>支付宝公钥路径(ali_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="javascript:" data-toggle="tooltip" data-title="如果要使用转账、提现功能,则必须使用公钥证书"> <i class="fa fa-info-circle"></i> 公钥和公钥证书说明</a> <a href="https://opensupport.alipay.com/support/helpcenter/207/201602471154" 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/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>支付宝根证书路径(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/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 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-success 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");
+            });
+        });
+    });
+</script>

+ 67 - 0
addons/epay/config.php

@@ -0,0 +1,67 @@
+<?php
+
+return [
+    [
+        'name' => 'wechat',
+        'title' => '微信',
+        'type' => 'array',
+        'content' => [],
+        'value' => [
+            'appid' => '',
+            'app_id' => '',
+            'app_secret' => '',
+            'miniapp_id' => '',
+            'mch_id' => '1510099271',
+            'key' => 'changlaikejimiyao20180508jiushia',
+            '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' => '',
+    ],
+    [
+        'name' => '__tips__',
+        'title' => '温馨提示',
+        'type' => 'array',
+        'content' => [],
+        'value' => '请注意微信支付证书路径位于/addons/epay/certs目录下,请替换成你自己的证书<br>appid:APP的appid<br>app_id:公众号的appid<br>app_secret:公众号的secret<br>miniapp_id:小程序ID<br>mch_id:微信商户ID<br>key:微信商户支付的密钥',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '微信参数配置',
+        'ok' => '',
+        'extend' => '',
+    ],
+];

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

@@ -0,0 +1,243 @@
+<?php
+
+namespace addons\epay\controller;
+
+use addons\epay\library\QRCode;
+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);
+                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) {
+            //发起公众号(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);
+                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');
+        if (!Service::checkNotify($type)) {
+            echo '签名错误';
+            return;
+        }
+
+        //你可以在这里你的业务处理逻辑,比如处理你的订单状态、给会员加余额等等功能
+        //下面这句必须要执行,且在此之前不能有任何输出
+        echo "success";
+        return;
+    }
+
+    /**
+     * 支付成功返回
+     */
+    public function returnx()
+    {
+        $type = $this->request->param('type');
+        if (Service::checkReturn($type)) {
+            echo '签名错误';
+            return;
+        }
+
+        //你可以在这里定义你的提示信息,但切记不可在此编写逻辑
+        $this->success("恭喜你!支付成功!", addon_url("epay/index/index"));
+
+        return;
+    }
+
+    /**
+     * 生成二维码
+     */
+    public function qrcode()
+    {
+        $text = $this->request->get('text', 'hello world');
+
+        //如果有安装二维码插件,则调用插件的生成方法
+        if (class_exists("\addons\qrcode\library\Service") && get_addon_info('qrcode')['state']) {
+            $qrCode = \addons\qrcode\library\Service::qrcode(['text' => $text]);
+            $response = Response::create()->header("Content-Type", "image/png");
+
+            header('Content-Type: ' . $qrCode->getContentType());
+            $response->content($qrCode->writeString());
+            return $response;
+        } else {
+            $qr = QRCode::getMinimumQRCode($text);
+            $im = $qr->createImage(8, 5);
+            header("Content-type: image/png");
+            imagepng($im);
+            imagedestroy($im);
+            return;
+        }
+    }
+
+}

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

@@ -0,0 +1,111 @@
+<?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) {
+        }
+        echo $pay->success();
+    }
+
+    /**
+     * 支付返回,仅供开发测试
+     */
+    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.5
+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;
+    }
+
+}

+ 1856 - 0
addons/epay/library/QRCode.php

@@ -0,0 +1,1856 @@
+<?php
+
+namespace addons\epay\library;
+
+//---------------------------------------------------------------
+// QRCode for PHP5
+//
+// Copyright (c) 2009 Kazuhiko Arase
+//
+// URL: http://www.d-project.com/
+//
+// Licensed under the MIT license:
+//   http://www.opensource.org/licenses/mit-license.php
+//
+// The word "QR Code" is registered trademark of
+// DENSO WAVE INCORPORATED
+//   http://www.denso-wave.com/qrcode/faqpatent-e.html
+//
+//---------------------------------------------------------------------
+
+//---------------------------------------------------------------
+// QRCode
+//---------------------------------------------------------------
+
+define("QR_PAD0", 0xEC);
+define("QR_PAD1", 0x11);
+
+class QRCode
+{
+
+    var $typeNumber;
+
+    var $modules;
+
+    var $moduleCount;
+
+    var $errorCorrectLevel;
+
+    var $qrDataList;
+
+    function __construct()
+    {
+        $this->typeNumber = 1;
+        $this->errorCorrectLevel = QR_ERROR_CORRECT_LEVEL_H;
+        $this->qrDataList = array();
+    }
+
+    function getTypeNumber()
+    {
+        return $this->typeNumber;
+    }
+
+    function setTypeNumber($typeNumber)
+    {
+        $this->typeNumber = $typeNumber;
+    }
+
+    function getErrorCorrectLevel()
+    {
+        return $this->errorCorrectLevel;
+    }
+
+    function setErrorCorrectLevel($errorCorrectLevel)
+    {
+        $this->errorCorrectLevel = $errorCorrectLevel;
+    }
+
+    function addData($data, $mode = 0)
+    {
+
+        if ($mode == 0) {
+            $mode = QRUtil::getMode($data);
+        }
+
+        switch ($mode) {
+
+            case QR_MODE_NUMBER :
+                $this->addDataImpl(new QRNumber($data));
+                break;
+
+            case QR_MODE_ALPHA_NUM :
+                $this->addDataImpl(new QRAlphaNum($data));
+                break;
+
+            case QR_MODE_8BIT_BYTE :
+                $this->addDataImpl(new QR8BitByte($data));
+                break;
+
+            case QR_MODE_KANJI :
+                $this->addDataImpl(new QRKanji($data));
+                break;
+
+            default :
+                trigger_error("mode:$mode", E_USER_ERROR);
+        }
+    }
+
+    function clearData()
+    {
+        $this->qrDataList = array();
+    }
+
+    function addDataImpl($qrData)
+    {
+        $this->qrDataList[] = $qrData;
+    }
+
+    function getDataCount()
+    {
+        return count($this->qrDataList);
+    }
+
+    function getData($index)
+    {
+        return $this->qrDataList[$index];
+    }
+
+    function isDark($row, $col)
+    {
+        if ($this->modules[$row][$col] !== null) {
+            return $this->modules[$row][$col];
+        } else {
+            return false;
+        }
+    }
+
+    function getModuleCount()
+    {
+        return $this->moduleCount;
+    }
+
+    // used for converting fg/bg colors (e.g. #0000ff = 0x0000FF)
+    // added 2015.07.27 ~ DoktorJ
+    function hex2rgb($hex = 0x0)
+    {
+        return array(
+            'r' => floor($hex / 65536),
+            'g' => floor($hex / 256) % 256,
+            'b' => $hex % 256
+        );
+    }
+
+    function make()
+    {
+        $this->makeImpl(false, $this->getBestMaskPattern());
+    }
+
+    function getBestMaskPattern()
+    {
+
+        $minLostPoint = 0;
+        $pattern = 0;
+
+        for ($i = 0; $i < 8; $i++) {
+
+            $this->makeImpl(true, $i);
+
+            $lostPoint = QRUtil::getLostPoint($this);
+
+            if ($i == 0 || $minLostPoint > $lostPoint) {
+                $minLostPoint = $lostPoint;
+                $pattern = $i;
+            }
+        }
+
+        return $pattern;
+    }
+
+    function createNullArray($length)
+    {
+        $nullArray = array();
+        for ($i = 0; $i < $length; $i++) {
+            $nullArray[] = null;
+        }
+        return $nullArray;
+    }
+
+    function makeImpl($test, $maskPattern)
+    {
+
+        $this->moduleCount = $this->typeNumber * 4 + 17;
+
+        $this->modules = array();
+        for ($i = 0; $i < $this->moduleCount; $i++) {
+            $this->modules[] = QRCode::createNullArray($this->moduleCount);
+        }
+
+        $this->setupPositionProbePattern(0, 0);
+        $this->setupPositionProbePattern($this->moduleCount - 7, 0);
+        $this->setupPositionProbePattern(0, $this->moduleCount - 7);
+
+        $this->setupPositionAdjustPattern();
+        $this->setupTimingPattern();
+
+        $this->setupTypeInfo($test, $maskPattern);
+
+        if ($this->typeNumber >= 7) {
+            $this->setupTypeNumber($test);
+        }
+
+        $dataArray = $this->qrDataList;
+
+        $data = QRCode::createData($this->typeNumber, $this->errorCorrectLevel, $dataArray);
+
+        $this->mapData($data, $maskPattern);
+    }
+
+    function mapData(&$data, $maskPattern)
+    {
+
+        $inc = -1;
+        $row = $this->moduleCount - 1;
+        $bitIndex = 7;
+        $byteIndex = 0;
+
+        for ($col = $this->moduleCount - 1; $col > 0; $col -= 2) {
+
+            if ($col == 6) $col--;
+
+            while (true) {
+
+                for ($c = 0; $c < 2; $c++) {
+
+                    if ($this->modules[$row][$col - $c] === null) {
+
+                        $dark = false;
+
+                        if ($byteIndex < count($data)) {
+                            $dark = ((($data[$byteIndex] >> $bitIndex) & 1) == 1);
+                        }
+
+                        if (QRUtil::getMask($maskPattern, $row, $col - $c)) {
+                            $dark = !$dark;
+                        }
+
+                        $this->modules[$row][$col - $c] = $dark;
+                        $bitIndex--;
+
+                        if ($bitIndex == -1) {
+                            $byteIndex++;
+                            $bitIndex = 7;
+                        }
+                    }
+                }
+
+                $row += $inc;
+
+                if ($row < 0 || $this->moduleCount <= $row) {
+                    $row -= $inc;
+                    $inc = -$inc;
+                    break;
+                }
+            }
+        }
+    }
+
+    function setupPositionAdjustPattern()
+    {
+
+        $pos = QRUtil::getPatternPosition($this->typeNumber);
+
+        for ($i = 0; $i < count($pos); $i++) {
+
+            for ($j = 0; $j < count($pos); $j++) {
+
+                $row = $pos[$i];
+                $col = $pos[$j];
+
+                if ($this->modules[$row][$col] !== null) {
+                    continue;
+                }
+
+                for ($r = -2; $r <= 2; $r++) {
+
+                    for ($c = -2; $c <= 2; $c++) {
+                        $this->modules[$row + $r][$col + $c] =
+                            $r == -2 || $r == 2 || $c == -2 || $c == 2 || ($r == 0 && $c == 0);
+                    }
+                }
+            }
+        }
+    }
+
+    function setupPositionProbePattern($row, $col)
+    {
+
+        for ($r = -1; $r <= 7; $r++) {
+
+            for ($c = -1; $c <= 7; $c++) {
+
+                if ($row + $r <= -1 || $this->moduleCount <= $row + $r
+                    || $col + $c <= -1 || $this->moduleCount <= $col + $c) {
+                    continue;
+                }
+
+                $this->modules[$row + $r][$col + $c] =
+                    (0 <= $r && $r <= 6 && ($c == 0 || $c == 6))
+                    || (0 <= $c && $c <= 6 && ($r == 0 || $r == 6))
+                    || (2 <= $r && $r <= 4 && 2 <= $c && $c <= 4);
+            }
+        }
+    }
+
+    function setupTimingPattern()
+    {
+
+        for ($i = 8; $i < $this->moduleCount - 8; $i++) {
+
+            if ($this->modules[$i][6] !== null || $this->modules[6][$i] !== null) {
+                continue;
+            }
+
+            $this->modules[$i][6] = ($i % 2 == 0);
+            $this->modules[6][$i] = ($i % 2 == 0);
+        }
+    }
+
+    function setupTypeNumber($test)
+    {
+
+        $bits = QRUtil::getBCHTypeNumber($this->typeNumber);
+
+        for ($i = 0; $i < 18; $i++) {
+            $mod = (!$test && (($bits >> $i) & 1) == 1);
+            $this->modules[(int)floor($i / 3)][$i % 3 + $this->moduleCount - 8 - 3] = $mod;
+            $this->modules[$i % 3 + $this->moduleCount - 8 - 3][floor($i / 3)] = $mod;
+        }
+    }
+
+    function setupTypeInfo($test, $maskPattern)
+    {
+
+        $data = ($this->errorCorrectLevel << 3) | $maskPattern;
+        $bits = QRUtil::getBCHTypeInfo($data);
+
+        for ($i = 0; $i < 15; $i++) {
+
+            $mod = (!$test && (($bits >> $i) & 1) == 1);
+
+            if ($i < 6) {
+                $this->modules[$i][8] = $mod;
+            } else if ($i < 8) {
+                $this->modules[$i + 1][8] = $mod;
+            } else {
+                $this->modules[$this->moduleCount - 15 + $i][8] = $mod;
+            }
+
+            if ($i < 8) {
+                $this->modules[8][$this->moduleCount - $i - 1] = $mod;
+            } else if ($i < 9) {
+                $this->modules[8][15 - $i - 1 + 1] = $mod;
+            } else {
+                $this->modules[8][15 - $i - 1] = $mod;
+            }
+        }
+
+        $this->modules[$this->moduleCount - 8][8] = !$test;
+    }
+
+    function createData($typeNumber, $errorCorrectLevel, $dataArray)
+    {
+
+        $rsBlocks = QRRSBlock::getRSBlocks($typeNumber, $errorCorrectLevel);
+
+        $buffer = new QRBitBuffer();
+
+        for ($i = 0; $i < count($dataArray); $i++) {
+            /** @var \QRData $data */
+            $data = $dataArray[$i];
+            $buffer->put($data->getMode(), 4);
+            $buffer->put($data->getLength(), $data->getLengthInBits($typeNumber));
+            $data->write($buffer);
+        }
+
+        $totalDataCount = 0;
+        for ($i = 0; $i < count($rsBlocks); $i++) {
+            $totalDataCount += $rsBlocks[$i]->getDataCount();
+        }
+
+        if ($buffer->getLengthInBits() > $totalDataCount * 8) {
+            trigger_error("code length overflow. ("
+                . $buffer->getLengthInBits()
+                . ">"
+                . $totalDataCount * 8
+                . ")", E_USER_ERROR);
+        }
+
+        // end code.
+        if ($buffer->getLengthInBits() + 4 <= $totalDataCount * 8) {
+            $buffer->put(0, 4);
+        }
+
+        // padding
+        while ($buffer->getLengthInBits() % 8 != 0) {
+            $buffer->putBit(false);
+        }
+
+        // padding
+        while (true) {
+
+            if ($buffer->getLengthInBits() >= $totalDataCount * 8) {
+                break;
+            }
+            $buffer->put(QR_PAD0, 8);
+
+            if ($buffer->getLengthInBits() >= $totalDataCount * 8) {
+                break;
+            }
+            $buffer->put(QR_PAD1, 8);
+        }
+
+        return QRCode::createBytes($buffer, $rsBlocks);
+    }
+
+    /**
+     * @param \QRBitBuffer $buffer
+     * @param \QRRSBlock[] $rsBlocks
+     *
+     * @return array
+     */
+    function createBytes(&$buffer, &$rsBlocks)
+    {
+
+        $offset = 0;
+
+        $maxDcCount = 0;
+        $maxEcCount = 0;
+
+        $dcdata = QRCode::createNullArray(count($rsBlocks));
+        $ecdata = QRCode::createNullArray(count($rsBlocks));
+
+        $rsBlockCount = count($rsBlocks);
+        for ($r = 0; $r < $rsBlockCount; $r++) {
+
+            $dcCount = $rsBlocks[$r]->getDataCount();
+            $ecCount = $rsBlocks[$r]->getTotalCount() - $dcCount;
+
+            $maxDcCount = max($maxDcCount, $dcCount);
+            $maxEcCount = max($maxEcCount, $ecCount);
+
+            $dcdata[$r] = QRCode::createNullArray($dcCount);
+            $dcDataCount = count($dcdata[$r]);
+            for ($i = 0; $i < $dcDataCount; $i++) {
+                $bdata = $buffer->getBuffer();
+                $dcdata[$r][$i] = 0xff & $bdata[$i + $offset];
+            }
+            $offset += $dcCount;
+
+            $rsPoly = QRUtil::getErrorCorrectPolynomial($ecCount);
+            $rawPoly = new QRPolynomial($dcdata[$r], $rsPoly->getLength() - 1);
+
+            $modPoly = $rawPoly->mod($rsPoly);
+            $ecdata[$r] = QRCode::createNullArray($rsPoly->getLength() - 1);
+
+            $ecDataCount = count($ecdata[$r]);
+            for ($i = 0; $i < $ecDataCount; $i++) {
+                $modIndex = $i + $modPoly->getLength() - count($ecdata[$r]);
+                $ecdata[$r][$i] = ($modIndex >= 0) ? $modPoly->get($modIndex) : 0;
+            }
+        }
+
+        $totalCodeCount = 0;
+        for ($i = 0; $i < $rsBlockCount; $i++) {
+            $totalCodeCount += $rsBlocks[$i]->getTotalCount();
+        }
+
+        $data = QRCode::createNullArray($totalCodeCount);
+
+        $index = 0;
+
+        for ($i = 0; $i < $maxDcCount; $i++) {
+            for ($r = 0; $r < $rsBlockCount; $r++) {
+                if ($i < count($dcdata[$r])) {
+                    $data[$index++] = $dcdata[$r][$i];
+                }
+            }
+        }
+
+        for ($i = 0; $i < $maxEcCount; $i++) {
+            for ($r = 0; $r < $rsBlockCount; $r++) {
+                if ($i < count($ecdata[$r])) {
+                    $data[$index++] = $ecdata[$r][$i];
+                }
+            }
+        }
+
+        return $data;
+    }
+
+    static function getMinimumQRCode($data, $errorCorrectLevel = QR_ERROR_CORRECT_LEVEL_L)
+    {
+
+        $mode = QRUtil::getMode($data);
+
+        $qr = new QRCode();
+        $qr->setErrorCorrectLevel($errorCorrectLevel);
+        $qr->addData($data, $mode);
+
+        $qrData = $qr->getData(0);
+        $length = $qrData->getLength();
+
+        for ($typeNumber = 1; $typeNumber <= 40; $typeNumber++) {
+            if ($length <= QRUtil::getMaxLength($typeNumber, $mode, $errorCorrectLevel)) {
+                $qr->setTypeNumber($typeNumber);
+                break;
+            }
+        }
+
+        $qr->make();
+
+        return $qr;
+    }
+
+    // added $fg (foreground), $bg (background), and $bgtrans (use transparent bg) parameters
+    // also added some simple error checking on parameters
+    // updated 2015.07.27 ~ DoktorJ
+    function createImage($size = 2, $margin = 2, $fg = 0x000000, $bg = 0xFFFFFF, $bgtrans = false)
+    {
+
+        // size/margin EC
+        if (!is_numeric($size)) $size = 2;
+        if (!is_numeric($margin)) $margin = 2;
+        if ($size < 1) $size = 1;
+        if ($margin < 0) $margin = 0;
+
+        $image_size = $this->getModuleCount() * $size + $margin * 2;
+
+        $image = imagecreatetruecolor($image_size, $image_size);
+
+        // fg/bg EC
+        if ($fg < 0 || $fg > 0xFFFFFF) $fg = 0x0;
+        if ($bg < 0 || $bg > 0xFFFFFF) $bg = 0xFFFFFF;
+
+        // convert hexadecimal RGB to arrays for imagecolorallocate
+        $fgrgb = $this->hex2rgb($fg);
+        $bgrgb = $this->hex2rgb($bg);
+
+        // replace $black and $white with $fgc and $bgc
+        $fgc = imagecolorallocate($image, $fgrgb['r'], $fgrgb['g'], $fgrgb['b']);
+        $bgc = imagecolorallocate($image, $bgrgb['r'], $bgrgb['g'], $bgrgb['b']);
+        if ($bgtrans) imagecolortransparent($image, $bgc);
+
+        // update $white to $bgc
+        imagefilledrectangle($image, 0, 0, $image_size, $image_size, $bgc);
+
+        for ($r = 0; $r < $this->getModuleCount(); $r++) {
+            for ($c = 0; $c < $this->getModuleCount(); $c++) {
+                if ($this->isDark($r, $c)) {
+
+                    // update $black to $fgc
+                    imagefilledrectangle($image,
+                        $margin + $c * $size,
+                        $margin + $r * $size,
+                        $margin + ($c + 1) * $size - 1,
+                        $margin + ($r + 1) * $size - 1,
+                        $fgc);
+                }
+            }
+        }
+
+        return $image;
+    }
+
+    function printHTML($size = "2px")
+    {
+
+        $style = "border-style:none;border-collapse:collapse;margin:0px;padding:0px;";
+
+        print("<table style='$style'>");
+
+        for ($r = 0; $r < $this->getModuleCount(); $r++) {
+
+            print("<tr style='$style'>");
+
+            for ($c = 0; $c < $this->getModuleCount(); $c++) {
+                $color = $this->isDark($r, $c) ? "#000000" : "#ffffff";
+                print("<td style='$style;width:$size;height:$size;background-color:$color'></td>");
+            }
+
+            print("</tr>");
+        }
+
+        print("</table>");
+    }
+}
+
+//---------------------------------------------------------------
+// QRUtil
+//---------------------------------------------------------------
+
+define("QR_G15", (1 << 10) | (1 << 8) | (1 << 5)
+    | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0));
+
+define("QR_G18", (1 << 12) | (1 << 11) | (1 << 10)
+    | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0));
+
+define("QR_G15_MASK", (1 << 14) | (1 << 12) | (1 << 10)
+    | (1 << 4) | (1 << 1));
+
+class QRUtil
+{
+
+    static $QR_MAX_LENGTH = array(
+        array(array(41, 25, 17, 10), array(34, 20, 14, 8), array(27, 16, 11, 7), array(17, 10, 7, 4)),
+        array(array(77, 47, 32, 20), array(63, 38, 26, 16), array(48, 29, 20, 12), array(34, 20, 14, 8)),
+        array(array(127, 77, 53, 32), array(101, 61, 42, 26), array(77, 47, 32, 20), array(58, 35, 24, 15)),
+        array(array(187, 114, 78, 48), array(149, 90, 62, 38), array(111, 67, 46, 28), array(82, 50, 34, 21)),
+        array(array(255, 154, 106, 65), array(202, 122, 84, 52), array(144, 87, 60, 37), array(106, 64, 44, 27)),
+        array(array(322, 195, 134, 82), array(255, 154, 106, 65), array(178, 108, 74, 45), array(139, 84, 58, 36)),
+        array(array(370, 224, 154, 95), array(293, 178, 122, 75), array(207, 125, 86, 53), array(154, 93, 64, 39)),
+        array(array(461, 279, 192, 118), array(365, 221, 152, 93), array(259, 157, 108, 66), array(202, 122, 84, 52)),
+        array(array(552, 335, 230, 141), array(432, 262, 180, 111), array(312, 189, 130, 80), array(235, 143, 98, 60)),
+        array(array(652, 395, 271, 167), array(513, 311, 213, 131), array(364, 221, 151, 93), array(288, 174, 119, 74))
+    );
+
+    static $QR_PATTERN_POSITION_TABLE = array(
+        array(),
+        array(6, 18),
+        array(6, 22),
+        array(6, 26),
+        array(6, 30),
+        array(6, 34),
+        array(6, 22, 38),
+        array(6, 24, 42),
+        array(6, 26, 46),
+        array(6, 28, 50),
+        array(6, 30, 54),
+        array(6, 32, 58),
+        array(6, 34, 62),
+        array(6, 26, 46, 66),
+        array(6, 26, 48, 70),
+        array(6, 26, 50, 74),
+        array(6, 30, 54, 78),
+        array(6, 30, 56, 82),
+        array(6, 30, 58, 86),
+        array(6, 34, 62, 90),
+        array(6, 28, 50, 72, 94),
+        array(6, 26, 50, 74, 98),
+        array(6, 30, 54, 78, 102),
+        array(6, 28, 54, 80, 106),
+        array(6, 32, 58, 84, 110),
+        array(6, 30, 58, 86, 114),
+        array(6, 34, 62, 90, 118),
+        array(6, 26, 50, 74, 98, 122),
+        array(6, 30, 54, 78, 102, 126),
+        array(6, 26, 52, 78, 104, 130),
+        array(6, 30, 56, 82, 108, 134),
+        array(6, 34, 60, 86, 112, 138),
+        array(6, 30, 58, 86, 114, 142),
+        array(6, 34, 62, 90, 118, 146),
+        array(6, 30, 54, 78, 102, 126, 150),
+        array(6, 24, 50, 76, 102, 128, 154),
+        array(6, 28, 54, 80, 106, 132, 158),
+        array(6, 32, 58, 84, 110, 136, 162),
+        array(6, 26, 54, 82, 110, 138, 166),
+        array(6, 30, 58, 86, 114, 142, 170)
+    );
+
+    static function getPatternPosition($typeNumber)
+    {
+        return self::$QR_PATTERN_POSITION_TABLE[$typeNumber - 1];
+    }
+
+    static function getMaxLength($typeNumber, $mode, $errorCorrectLevel)
+    {
+
+        $t = $typeNumber - 1;
+        $e = 0;
+        $m = 0;
+
+        switch ($errorCorrectLevel) {
+            case QR_ERROR_CORRECT_LEVEL_L :
+                $e = 0;
+                break;
+            case QR_ERROR_CORRECT_LEVEL_M :
+                $e = 1;
+                break;
+            case QR_ERROR_CORRECT_LEVEL_Q :
+                $e = 2;
+                break;
+            case QR_ERROR_CORRECT_LEVEL_H :
+                $e = 3;
+                break;
+            default :
+                trigger_error("e:$errorCorrectLevel", E_USER_ERROR);
+        }
+
+        switch ($mode) {
+            case QR_MODE_NUMBER    :
+                $m = 0;
+                break;
+            case QR_MODE_ALPHA_NUM :
+                $m = 1;
+                break;
+            case QR_MODE_8BIT_BYTE :
+                $m = 2;
+                break;
+            case QR_MODE_KANJI     :
+                $m = 3;
+                break;
+            default :
+                trigger_error("m:$mode", E_USER_ERROR);
+        }
+
+        return self::$QR_MAX_LENGTH[$t][$e][$m];
+    }
+
+    static function getErrorCorrectPolynomial($errorCorrectLength)
+    {
+
+        $a = new QRPolynomial(array(1));
+
+        for ($i = 0; $i < $errorCorrectLength; $i++) {
+            $a = $a->multiply(new QRPolynomial(array(1, QRMath::gexp($i))));
+        }
+
+        return $a;
+    }
+
+    static function getMask($maskPattern, $i, $j)
+    {
+
+        switch ($maskPattern) {
+
+            case QR_MASK_PATTERN000 :
+                return ($i + $j) % 2 == 0;
+            case QR_MASK_PATTERN001 :
+                return $i % 2 == 0;
+            case QR_MASK_PATTERN010 :
+                return $j % 3 == 0;
+            case QR_MASK_PATTERN011 :
+                return ($i + $j) % 3 == 0;
+            case QR_MASK_PATTERN100 :
+                return (floor($i / 2) + floor($j / 3)) % 2 == 0;
+            case QR_MASK_PATTERN101 :
+                return ($i * $j) % 2 + ($i * $j) % 3 == 0;
+            case QR_MASK_PATTERN110 :
+                return (($i * $j) % 2 + ($i * $j) % 3) % 2 == 0;
+            case QR_MASK_PATTERN111 :
+                return (($i * $j) % 3 + ($i + $j) % 2) % 2 == 0;
+
+            default :
+                trigger_error("mask:$maskPattern", E_USER_ERROR);
+        }
+    }
+
+    /**
+     * @param \QRCode $qrCode
+     *
+     * @return float|int
+     */
+    static function getLostPoint($qrCode)
+    {
+
+        $moduleCount = $qrCode->getModuleCount();
+
+        $lostPoint = 0;
+
+
+        // LEVEL1
+
+        for ($row = 0; $row < $moduleCount; $row++) {
+
+            for ($col = 0; $col < $moduleCount; $col++) {
+
+                $sameCount = 0;
+                $dark = $qrCode->isDark($row, $col);
+
+                for ($r = -1; $r <= 1; $r++) {
+
+                    if ($row + $r < 0 || $moduleCount <= $row + $r) {
+                        continue;
+                    }
+
+                    for ($c = -1; $c <= 1; $c++) {
+
+                        if (($col + $c < 0 || $moduleCount <= $col + $c) || ($r == 0 && $c == 0)) {
+                            continue;
+                        }
+
+                        if ($dark == $qrCode->isDark($row + $r, $col + $c)) {
+                            $sameCount++;
+                        }
+                    }
+                }
+
+                if ($sameCount > 5) {
+                    $lostPoint += (3 + $sameCount - 5);
+                }
+            }
+        }
+
+        // LEVEL2
+
+        for ($row = 0; $row < $moduleCount - 1; $row++) {
+            for ($col = 0; $col < $moduleCount - 1; $col++) {
+                $count = 0;
+                if ($qrCode->isDark($row, $col)) $count++;
+                if ($qrCode->isDark($row + 1, $col)) $count++;
+                if ($qrCode->isDark($row, $col + 1)) $count++;
+                if ($qrCode->isDark($row + 1, $col + 1)) $count++;
+                if ($count == 0 || $count == 4) {
+                    $lostPoint += 3;
+                }
+            }
+        }
+
+        // LEVEL3
+
+        for ($row = 0; $row < $moduleCount; $row++) {
+            for ($col = 0; $col < $moduleCount - 6; $col++) {
+                if ($qrCode->isDark($row, $col)
+                    && !$qrCode->isDark($row, $col + 1)
+                    && $qrCode->isDark($row, $col + 2)
+                    && $qrCode->isDark($row, $col + 3)
+                    && $qrCode->isDark($row, $col + 4)
+                    && !$qrCode->isDark($row, $col + 5)
+                    && $qrCode->isDark($row, $col + 6)) {
+                    $lostPoint += 40;
+                }
+            }
+        }
+
+        for ($col = 0; $col < $moduleCount; $col++) {
+            for ($row = 0; $row < $moduleCount - 6; $row++) {
+                if ($qrCode->isDark($row, $col)
+                    && !$qrCode->isDark($row + 1, $col)
+                    && $qrCode->isDark($row + 2, $col)
+                    && $qrCode->isDark($row + 3, $col)
+                    && $qrCode->isDark($row + 4, $col)
+                    && !$qrCode->isDark($row + 5, $col)
+                    && $qrCode->isDark($row + 6, $col)) {
+                    $lostPoint += 40;
+                }
+            }
+        }
+
+        // LEVEL4
+
+        $darkCount = 0;
+
+        for ($col = 0; $col < $moduleCount; $col++) {
+            for ($row = 0; $row < $moduleCount; $row++) {
+                if ($qrCode->isDark($row, $col)) {
+                    $darkCount++;
+                }
+            }
+        }
+
+        $ratio = abs(100 * $darkCount / $moduleCount / $moduleCount - 50) / 5;
+        $lostPoint += $ratio * 10;
+
+        return $lostPoint;
+    }
+
+    static function getMode($s)
+    {
+        if (QRUtil::isAlphaNum($s)) {
+            if (QRUtil::isNumber($s)) {
+                return QR_MODE_NUMBER;
+            }
+            return QR_MODE_ALPHA_NUM;
+        } else if (QRUtil::isKanji($s)) {
+            return QR_MODE_KANJI;
+        } else {
+            return QR_MODE_8BIT_BYTE;
+        }
+    }
+
+    static function isNumber($s)
+    {
+        for ($i = 0; $i < strlen($s); $i++) {
+            $c = ord($s[$i]);
+            if (!(QRUtil::toCharCode('0') <= $c && $c <= QRUtil::toCharCode('9'))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    static function isAlphaNum($s)
+    {
+        for ($i = 0; $i < strlen($s); $i++) {
+            $c = ord($s[$i]);
+            if (!(QRUtil::toCharCode('0') <= $c && $c <= QRUtil::toCharCode('9'))
+                && !(QRUtil::toCharCode('A') <= $c && $c <= QRUtil::toCharCode('Z'))
+                && strpos(" $%*+-./:", $s[$i]) === false) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    static function isKanji($s)
+    {
+
+        $data = $s;
+
+        $i = 0;
+
+        while ($i + 1 < strlen($data)) {
+
+            $c = ((0xff & ord($data[$i])) << 8) | (0xff & ord($data[$i + 1]));
+
+            if (!(0x8140 <= $c && $c <= 0x9FFC) && !(0xE040 <= $c && $c <= 0xEBBF)) {
+                return false;
+            }
+
+            $i += 2;
+        }
+
+        if ($i < strlen($data)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    static function toCharCode($s)
+    {
+        return ord($s[0]);
+    }
+
+    static function getBCHTypeInfo($data)
+    {
+        $d = $data << 10;
+        while (QRUtil::getBCHDigit($d) - QRUtil::getBCHDigit(QR_G15) >= 0) {
+            $d ^= (QR_G15 << (QRUtil::getBCHDigit($d) - QRUtil::getBCHDigit(QR_G15)));
+        }
+        return (($data << 10) | $d) ^ QR_G15_MASK;
+    }
+
+    static function getBCHTypeNumber($data)
+    {
+        $d = $data << 12;
+        while (QRUtil::getBCHDigit($d) - QRUtil::getBCHDigit(QR_G18) >= 0) {
+            $d ^= (QR_G18 << (QRUtil::getBCHDigit($d) - QRUtil::getBCHDigit(QR_G18)));
+        }
+        return ($data << 12) | $d;
+    }
+
+    static function getBCHDigit($data)
+    {
+
+        $digit = 0;
+
+        while ($data != 0) {
+            $digit++;
+            $data >>= 1;
+        }
+
+        return $digit;
+    }
+}
+
+//---------------------------------------------------------------
+// QRRSBlock
+//---------------------------------------------------------------
+
+class QRRSBlock
+{
+
+    var $totalCount;
+    var $dataCount;
+
+    static $QR_RS_BLOCK_TABLE = array(
+
+        // L
+        // M
+        // Q
+        // H
+
+        // 1
+        array(1, 26, 19),
+        array(1, 26, 16),
+        array(1, 26, 13),
+        array(1, 26, 9),
+
+        // 2
+        array(1, 44, 34),
+        array(1, 44, 28),
+        array(1, 44, 22),
+        array(1, 44, 16),
+
+        // 3
+        array(1, 70, 55),
+        array(1, 70, 44),
+        array(2, 35, 17),
+        array(2, 35, 13),
+
+        // 4
+        array(1, 100, 80),
+        array(2, 50, 32),
+        array(2, 50, 24),
+        array(4, 25, 9),
+
+        // 5
+        array(1, 134, 108),
+        array(2, 67, 43),
+        array(2, 33, 15, 2, 34, 16),
+        array(2, 33, 11, 2, 34, 12),
+
+        // 6
+        array(2, 86, 68),
+        array(4, 43, 27),
+        array(4, 43, 19),
+        array(4, 43, 15),
+
+        // 7
+        array(2, 98, 78),
+        array(4, 49, 31),
+        array(2, 32, 14, 4, 33, 15),
+        array(4, 39, 13, 1, 40, 14),
+
+        // 8
+        array(2, 121, 97),
+        array(2, 60, 38, 2, 61, 39),
+        array(4, 40, 18, 2, 41, 19),
+        array(4, 40, 14, 2, 41, 15),
+
+        // 9
+        array(2, 146, 116),
+        array(3, 58, 36, 2, 59, 37),
+        array(4, 36, 16, 4, 37, 17),
+        array(4, 36, 12, 4, 37, 13),
+
+        // 10
+        array(2, 86, 68, 2, 87, 69),
+        array(4, 69, 43, 1, 70, 44),
+        array(6, 43, 19, 2, 44, 20),
+        array(6, 43, 15, 2, 44, 16),
+
+        // 11
+        array(4, 101, 81),
+        array(1, 80, 50, 4, 81, 51),
+        array(4, 50, 22, 4, 51, 23),
+        array(3, 36, 12, 8, 37, 13),
+
+        // 12
+        array(2, 116, 92, 2, 117, 93),
+        array(6, 58, 36, 2, 59, 37),
+        array(4, 46, 20, 6, 47, 21),
+        array(7, 42, 14, 4, 43, 15),
+
+        // 13
+        array(4, 133, 107),
+        array(8, 59, 37, 1, 60, 38),
+        array(8, 44, 20, 4, 45, 21),
+        array(12, 33, 11, 4, 34, 12),
+
+        // 14
+        array(3, 145, 115, 1, 146, 116),
+        array(4, 64, 40, 5, 65, 41),
+        array(11, 36, 16, 5, 37, 17),
+        array(11, 36, 12, 5, 37, 13),
+
+        // 15
+        array(5, 109, 87, 1, 110, 88),
+        array(5, 65, 41, 5, 66, 42),
+        array(5, 54, 24, 7, 55, 25),
+        array(11, 36, 12, 7, 37, 13),
+
+        // 16
+        array(5, 122, 98, 1, 123, 99),
+        array(7, 73, 45, 3, 74, 46),
+        array(15, 43, 19, 2, 44, 20),
+        array(3, 45, 15, 13, 46, 16),
+
+        // 17
+        array(1, 135, 107, 5, 136, 108),
+        array(10, 74, 46, 1, 75, 47),
+        array(1, 50, 22, 15, 51, 23),
+        array(2, 42, 14, 17, 43, 15),
+
+        // 18
+        array(5, 150, 120, 1, 151, 121),
+        array(9, 69, 43, 4, 70, 44),
+        array(17, 50, 22, 1, 51, 23),
+        array(2, 42, 14, 19, 43, 15),
+
+        // 19
+        array(3, 141, 113, 4, 142, 114),
+        array(3, 70, 44, 11, 71, 45),
+        array(17, 47, 21, 4, 48, 22),
+        array(9, 39, 13, 16, 40, 14),
+
+        // 20
+        array(3, 135, 107, 5, 136, 108),
+        array(3, 67, 41, 13, 68, 42),
+        array(15, 54, 24, 5, 55, 25),
+        array(15, 43, 15, 10, 44, 16),
+
+        // 21
+        array(4, 144, 116, 4, 145, 117),
+        array(17, 68, 42),
+        array(17, 50, 22, 6, 51, 23),
+        array(19, 46, 16, 6, 47, 17),
+
+        // 22
+        array(2, 139, 111, 7, 140, 112),
+        array(17, 74, 46),
+        array(7, 54, 24, 16, 55, 25),
+        array(34, 37, 13),
+
+        // 23
+        array(4, 151, 121, 5, 152, 122),
+        array(4, 75, 47, 14, 76, 48),
+        array(11, 54, 24, 14, 55, 25),
+        array(16, 45, 15, 14, 46, 16),
+
+        // 24
+        array(6, 147, 117, 4, 148, 118),
+        array(6, 73, 45, 14, 74, 46),
+        array(11, 54, 24, 16, 55, 25),
+        array(30, 46, 16, 2, 47, 17),
+
+        // 25
+        array(8, 132, 106, 4, 133, 107),
+        array(8, 75, 47, 13, 76, 48),
+        array(7, 54, 24, 22, 55, 25),
+        array(22, 45, 15, 13, 46, 16),
+
+        // 26
+        array(10, 142, 114, 2, 143, 115),
+        array(19, 74, 46, 4, 75, 47),
+        array(28, 50, 22, 6, 51, 23),
+        array(33, 46, 16, 4, 47, 17),
+
+        // 27
+        array(8, 152, 122, 4, 153, 123),
+        array(22, 73, 45, 3, 74, 46),
+        array(8, 53, 23, 26, 54, 24),
+        array(12, 45, 15, 28, 46, 16),
+
+        // 28
+        array(3, 147, 117, 10, 148, 118),
+        array(3, 73, 45, 23, 74, 46),
+        array(4, 54, 24, 31, 55, 25),
+        array(11, 45, 15, 31, 46, 16),
+
+        // 29
+        array(7, 146, 116, 7, 147, 117),
+        array(21, 73, 45, 7, 74, 46),
+        array(1, 53, 23, 37, 54, 24),
+        array(19, 45, 15, 26, 46, 16),
+
+        // 30
+        array(5, 145, 115, 10, 146, 116),
+        array(19, 75, 47, 10, 76, 48),
+        array(15, 54, 24, 25, 55, 25),
+        array(23, 45, 15, 25, 46, 16),
+
+        // 31
+        array(13, 145, 115, 3, 146, 116),
+        array(2, 74, 46, 29, 75, 47),
+        array(42, 54, 24, 1, 55, 25),
+        array(23, 45, 15, 28, 46, 16),
+
+        // 32
+        array(17, 145, 115),
+        array(10, 74, 46, 23, 75, 47),
+        array(10, 54, 24, 35, 55, 25),
+        array(19, 45, 15, 35, 46, 16),
+
+        // 33
+        array(17, 145, 115, 1, 146, 116),
+        array(14, 74, 46, 21, 75, 47),
+        array(29, 54, 24, 19, 55, 25),
+        array(11, 45, 15, 46, 46, 16),
+
+        // 34
+        array(13, 145, 115, 6, 146, 116),
+        array(14, 74, 46, 23, 75, 47),
+        array(44, 54, 24, 7, 55, 25),
+        array(59, 46, 16, 1, 47, 17),
+
+        // 35
+        array(12, 151, 121, 7, 152, 122),
+        array(12, 75, 47, 26, 76, 48),
+        array(39, 54, 24, 14, 55, 25),
+        array(22, 45, 15, 41, 46, 16),
+
+        // 36
+        array(6, 151, 121, 14, 152, 122),
+        array(6, 75, 47, 34, 76, 48),
+        array(46, 54, 24, 10, 55, 25),
+        array(2, 45, 15, 64, 46, 16),
+
+        // 37
+        array(17, 152, 122, 4, 153, 123),
+        array(29, 74, 46, 14, 75, 47),
+        array(49, 54, 24, 10, 55, 25),
+        array(24, 45, 15, 46, 46, 16),
+
+        // 38
+        array(4, 152, 122, 18, 153, 123),
+        array(13, 74, 46, 32, 75, 47),
+        array(48, 54, 24, 14, 55, 25),
+        array(42, 45, 15, 32, 46, 16),
+
+        // 39
+        array(20, 147, 117, 4, 148, 118),
+        array(40, 75, 47, 7, 76, 48),
+        array(43, 54, 24, 22, 55, 25),
+        array(10, 45, 15, 67, 46, 16),
+
+        // 40
+        array(19, 148, 118, 6, 149, 119),
+        array(18, 75, 47, 31, 76, 48),
+        array(34, 54, 24, 34, 55, 25),
+        array(20, 45, 15, 61, 46, 16)
+
+    );
+
+    function __construct($totalCount, $dataCount)
+    {
+        $this->totalCount = $totalCount;
+        $this->dataCount = $dataCount;
+    }
+
+    function getDataCount()
+    {
+        return $this->dataCount;
+    }
+
+    function getTotalCount()
+    {
+        return $this->totalCount;
+    }
+
+    static function getRSBlocks($typeNumber, $errorCorrectLevel)
+    {
+
+        $rsBlock = QRRSBlock::getRsBlockTable($typeNumber, $errorCorrectLevel);
+        $length = count($rsBlock) / 3;
+
+        $list = array();
+
+        for ($i = 0; $i < $length; $i++) {
+
+            $count = $rsBlock[$i * 3 + 0];
+            $totalCount = $rsBlock[$i * 3 + 1];
+            $dataCount = $rsBlock[$i * 3 + 2];
+
+            for ($j = 0; $j < $count; $j++) {
+                $list[] = new QRRSBlock($totalCount, $dataCount);
+            }
+        }
+
+        return $list;
+    }
+
+    static function getRsBlockTable($typeNumber, $errorCorrectLevel)
+    {
+
+        switch ($errorCorrectLevel) {
+            case QR_ERROR_CORRECT_LEVEL_L :
+                return self::$QR_RS_BLOCK_TABLE[($typeNumber - 1) * 4 + 0];
+            case QR_ERROR_CORRECT_LEVEL_M :
+                return self::$QR_RS_BLOCK_TABLE[($typeNumber - 1) * 4 + 1];
+            case QR_ERROR_CORRECT_LEVEL_Q :
+                return self::$QR_RS_BLOCK_TABLE[($typeNumber - 1) * 4 + 2];
+            case QR_ERROR_CORRECT_LEVEL_H :
+                return self::$QR_RS_BLOCK_TABLE[($typeNumber - 1) * 4 + 3];
+            default :
+                trigger_error("tn:$typeNumber/ecl:$errorCorrectLevel", E_USER_ERROR);
+        }
+    }
+}
+
+//---------------------------------------------------------------
+// QRNumber
+//---------------------------------------------------------------
+
+class QRNumber extends QRData
+{
+
+    function __construct($data)
+    {
+        parent::__construct(QR_MODE_NUMBER, $data);
+    }
+
+    function write(&$buffer)
+    {
+
+        $data = $this->getData();
+
+        $i = 0;
+
+        while ($i + 2 < strlen($data)) {
+            $num = QRNumber::parseInt(substr($data, $i, 3));
+            $buffer->put($num, 10);
+            $i += 3;
+        }
+
+        if ($i < strlen($data)) {
+
+            if (strlen($data) - $i == 1) {
+                $num = QRNumber::parseInt(substr($data, $i, $i + 1));
+                $buffer->put($num, 4);
+            } else if (strlen($data) - $i == 2) {
+                $num = QRNumber::parseInt(substr($data, $i, $i + 2));
+                $buffer->put($num, 7);
+            }
+        }
+    }
+
+    static function parseInt($s)
+    {
+
+        $num = 0;
+        for ($i = 0; $i < strlen($s); $i++) {
+            $num = $num * 10 + QRNumber::parseIntAt(ord($s[$i]));
+        }
+        return $num;
+    }
+
+    static function parseIntAt($c)
+    {
+
+        if (QRUtil::toCharCode('0') <= $c && $c <= QRUtil::toCharCode('9')) {
+            return $c - QRUtil::toCharCode('0');
+        }
+
+        trigger_error("illegal char : $c", E_USER_ERROR);
+    }
+}
+
+//---------------------------------------------------------------
+// QRKanji
+//---------------------------------------------------------------
+
+class QRKanji extends QRData
+{
+
+    function __construct($data)
+    {
+        parent::__construct(QR_MODE_KANJI, $data);
+    }
+
+    function write(&$buffer)
+    {
+
+        $data = $this->getData();
+
+        $i = 0;
+
+        while ($i + 1 < strlen($data)) {
+
+            $c = ((0xff & ord($data[$i])) << 8) | (0xff & ord($data[$i + 1]));
+
+            if (0x8140 <= $c && $c <= 0x9FFC) {
+                $c -= 0x8140;
+            } else if (0xE040 <= $c && $c <= 0xEBBF) {
+                $c -= 0xC140;
+            } else {
+                trigger_error("illegal char at " . ($i + 1) . "/$c", E_USER_ERROR);
+            }
+
+            $c = (($c >> 8) & 0xff) * 0xC0 + ($c & 0xff);
+
+            $buffer->put($c, 13);
+
+            $i += 2;
+        }
+
+        if ($i < strlen($data)) {
+            trigger_error("illegal char at " . ($i + 1), E_USER_ERROR);
+        }
+    }
+
+    function getLength()
+    {
+        return floor(strlen($this->getData()) / 2);
+    }
+}
+
+//---------------------------------------------------------------
+// QRAlphaNum
+//---------------------------------------------------------------
+
+class QRAlphaNum extends QRData
+{
+
+    function __construct($data)
+    {
+        parent::__construct(QR_MODE_ALPHA_NUM, $data);
+    }
+
+    function write(&$buffer)
+    {
+
+        $i = 0;
+        $c = $this->getData();
+
+        while ($i + 1 < strlen($c)) {
+            $buffer->put(QRAlphaNum::getCode(ord($c[$i])) * 45
+                + QRAlphaNum::getCode(ord($c[$i + 1])), 11);
+            $i += 2;
+        }
+
+        if ($i < strlen($c)) {
+            $buffer->put(QRAlphaNum::getCode(ord($c[$i])), 6);
+        }
+    }
+
+    static function getCode($c)
+    {
+
+        if (QRUtil::toCharCode('0') <= $c
+            && $c <= QRUtil::toCharCode('9')) {
+            return $c - QRUtil::toCharCode('0');
+        } else if (QRUtil::toCharCode('A') <= $c
+            && $c <= QRUtil::toCharCode('Z')) {
+            return $c - QRUtil::toCharCode('A') + 10;
+        } else {
+            switch ($c) {
+                case QRUtil::toCharCode(' ') :
+                    return 36;
+                case QRUtil::toCharCode('$') :
+                    return 37;
+                case QRUtil::toCharCode('%') :
+                    return 38;
+                case QRUtil::toCharCode('*') :
+                    return 39;
+                case QRUtil::toCharCode('+') :
+                    return 40;
+                case QRUtil::toCharCode('-') :
+                    return 41;
+                case QRUtil::toCharCode('.') :
+                    return 42;
+                case QRUtil::toCharCode('/') :
+                    return 43;
+                case QRUtil::toCharCode(':') :
+                    return 44;
+                default :
+                    trigger_error("illegal char : $c", E_USER_ERROR);
+            }
+        }
+
+    }
+}
+
+//---------------------------------------------------------------
+// QR8BitByte
+//---------------------------------------------------------------
+
+class QR8BitByte extends QRData
+{
+
+    function __construct($data)
+    {
+        parent::__construct(QR_MODE_8BIT_BYTE, $data);
+    }
+
+    function write(&$buffer)
+    {
+
+        $data = $this->getData();
+        for ($i = 0; $i < strlen($data); $i++) {
+            $buffer->put(ord($data[$i]), 8);
+        }
+    }
+
+}
+
+//---------------------------------------------------------------
+// QRData
+//---------------------------------------------------------------
+
+abstract class QRData
+{
+
+    var $mode;
+
+    var $data;
+
+    function __construct($mode, $data)
+    {
+        $this->mode = $mode;
+        $this->data = $data;
+    }
+
+    function getMode()
+    {
+        return $this->mode;
+    }
+
+    function getData()
+    {
+        return $this->data;
+    }
+
+    /**
+     * @return int
+     */
+    function getLength()
+    {
+        return strlen($this->getData());
+    }
+
+    /**
+     * @param \QRBitBuffer $buffer
+     */
+    abstract function write(&$buffer);
+
+    function getLengthInBits($type)
+    {
+
+        if (1 <= $type && $type < 10) {
+
+            // 1 - 9
+
+            switch ($this->mode) {
+                case QR_MODE_NUMBER     :
+                    return 10;
+                case QR_MODE_ALPHA_NUM     :
+                    return 9;
+                case QR_MODE_8BIT_BYTE    :
+                    return 8;
+                case QR_MODE_KANJI      :
+                    return 8;
+                default :
+                    trigger_error("mode:$this->mode", E_USER_ERROR);
+            }
+
+        } else if ($type < 27) {
+
+            // 10 - 26
+
+            switch ($this->mode) {
+                case QR_MODE_NUMBER     :
+                    return 12;
+                case QR_MODE_ALPHA_NUM     :
+                    return 11;
+                case QR_MODE_8BIT_BYTE    :
+                    return 16;
+                case QR_MODE_KANJI      :
+                    return 10;
+                default :
+                    trigger_error("mode:$this->mode", E_USER_ERROR);
+            }
+
+        } else if ($type < 41) {
+
+            // 27 - 40
+
+            switch ($this->mode) {
+                case QR_MODE_NUMBER     :
+                    return 14;
+                case QR_MODE_ALPHA_NUM    :
+                    return 13;
+                case QR_MODE_8BIT_BYTE    :
+                    return 16;
+                case QR_MODE_KANJI      :
+                    return 12;
+                default :
+                    trigger_error("mode:$this->mode", E_USER_ERROR);
+            }
+
+        } else {
+            trigger_error("mode:$this->mode", E_USER_ERROR);
+        }
+    }
+
+}
+
+//---------------------------------------------------------------
+// QRMath
+//---------------------------------------------------------------
+
+class QRMath
+{
+
+    static $QR_MATH_EXP_TABLE = null;
+    static $QR_MATH_LOG_TABLE = null;
+
+    static function init()
+    {
+
+        self::$QR_MATH_EXP_TABLE = QRMath::createNumArray(256);
+
+        for ($i = 0; $i < 8; $i++) {
+            self::$QR_MATH_EXP_TABLE[$i] = 1 << $i;
+        }
+
+        for ($i = 8; $i < 256; $i++) {
+            self::$QR_MATH_EXP_TABLE[$i] = self::$QR_MATH_EXP_TABLE[$i - 4]
+                ^ self::$QR_MATH_EXP_TABLE[$i - 5]
+                ^ self::$QR_MATH_EXP_TABLE[$i - 6]
+                ^ self::$QR_MATH_EXP_TABLE[$i - 8];
+        }
+
+        self::$QR_MATH_LOG_TABLE = QRMath::createNumArray(256);
+
+        for ($i = 0; $i < 255; $i++) {
+            self::$QR_MATH_LOG_TABLE[self::$QR_MATH_EXP_TABLE[$i]] = $i;
+        }
+    }
+
+    static function createNumArray($length)
+    {
+        $num_array = array();
+        for ($i = 0; $i < $length; $i++) {
+            $num_array[] = 0;
+        }
+        return $num_array;
+    }
+
+    static function glog($n)
+    {
+
+        if ($n < 1) {
+            trigger_error("log($n)", E_USER_ERROR);
+        }
+
+        return self::$QR_MATH_LOG_TABLE[$n];
+    }
+
+    static function gexp($n)
+    {
+
+        while ($n < 0) {
+            $n += 255;
+        }
+
+        while ($n >= 256) {
+            $n -= 255;
+        }
+
+        return self::$QR_MATH_EXP_TABLE[$n];
+    }
+}
+
+// init static table
+QRMath::init();
+
+//---------------------------------------------------------------
+// QRPolynomial
+//---------------------------------------------------------------
+
+class QRPolynomial
+{
+
+    var $num;
+
+    function __construct($num, $shift = 0)
+    {
+
+        $offset = 0;
+
+        while ($offset < count($num) && $num[$offset] == 0) {
+            $offset++;
+        }
+
+        $this->num = QRMath::createNumArray(count($num) - $offset + $shift);
+        for ($i = 0; $i < count($num) - $offset; $i++) {
+            $this->num[$i] = $num[$i + $offset];
+        }
+    }
+
+    function get($index)
+    {
+        return $this->num[$index];
+    }
+
+    function getLength()
+    {
+        return count($this->num);
+    }
+
+    // PHP5
+    function __toString()
+    {
+        return $this->toString();
+    }
+
+    function toString()
+    {
+
+        $buffer = "";
+
+        for ($i = 0; $i < $this->getLength(); $i++) {
+            if ($i > 0) {
+                $buffer .= ",";
+            }
+            $buffer .= $this->get($i);
+        }
+
+        return $buffer;
+    }
+
+    function toLogString()
+    {
+
+        $buffer = "";
+
+        for ($i = 0; $i < $this->getLength(); $i++) {
+            if ($i > 0) {
+                $buffer .= ",";
+            }
+            $buffer .= QRMath::glog($this->get($i));
+        }
+
+        return $buffer;
+    }
+
+    /**
+     * @param \QRPolynomial $e
+     *
+     * @return \QRPolynomial
+     */
+    function multiply($e)
+    {
+
+        $num = QRMath::createNumArray($this->getLength() + $e->getLength() - 1);
+
+        for ($i = 0; $i < $this->getLength(); $i++) {
+            $vi = QRMath::glog($this->get($i));
+
+            for ($j = 0; $j < $e->getLength(); $j++) {
+                $num[$i + $j] ^= QRMath::gexp($vi + QRMath::glog($e->get($j)));
+            }
+        }
+
+        return new QRPolynomial($num);
+    }
+
+    /**
+     * @param \QRPolynomial $e
+     *
+     * @return $this|\QRPolynomial
+     */
+    function mod($e)
+    {
+
+        if ($this->getLength() - $e->getLength() < 0) {
+            return $this;
+        }
+
+        $ratio = QRMath::glog($this->get(0)) - QRMath::glog($e->get(0));
+
+        $num = QRMath::createNumArray($this->getLength());
+        for ($i = 0; $i < $this->getLength(); $i++) {
+            $num[$i] = $this->get($i);
+        }
+
+        for ($i = 0; $i < $e->getLength(); $i++) {
+            $num[$i] ^= QRMath::gexp(QRMath::glog($e->get($i)) + $ratio);
+        }
+
+        $newPolynomial = new QRPolynomial($num);
+        return $newPolynomial->mod($e);
+    }
+}
+
+//---------------------------------------------------------------
+// Mode
+//---------------------------------------------------------------
+
+define("QR_MODE_NUMBER", 1 << 0);
+define("QR_MODE_ALPHA_NUM", 1 << 1);
+define("QR_MODE_8BIT_BYTE", 1 << 2);
+define("QR_MODE_KANJI", 1 << 3);
+
+//---------------------------------------------------------------
+// MaskPattern
+//---------------------------------------------------------------
+
+define("QR_MASK_PATTERN000", 0);
+define("QR_MASK_PATTERN001", 1);
+define("QR_MASK_PATTERN010", 2);
+define("QR_MASK_PATTERN011", 3);
+define("QR_MASK_PATTERN100", 4);
+define("QR_MASK_PATTERN101", 5);
+define("QR_MASK_PATTERN110", 6);
+define("QR_MASK_PATTERN111", 7);
+
+//---------------------------------------------------------------
+// ErrorCorrectLevel
+
+// 7%.
+define("QR_ERROR_CORRECT_LEVEL_L", 1);
+// 15%.
+define("QR_ERROR_CORRECT_LEVEL_M", 0);
+// 25%.
+define("QR_ERROR_CORRECT_LEVEL_Q", 3);
+// 30%.
+define("QR_ERROR_CORRECT_LEVEL_H", 2);
+
+
+//---------------------------------------------------------------
+// QRBitBuffer
+//---------------------------------------------------------------
+
+class QRBitBuffer
+{
+
+    var $buffer;
+    var $length;
+
+    function __construct()
+    {
+        $this->buffer = array();
+        $this->length = 0;
+    }
+
+    function getBuffer()
+    {
+        return $this->buffer;
+    }
+
+    function getLengthInBits()
+    {
+        return $this->length;
+    }
+
+    function __toString()
+    {
+        $buffer = "";
+        for ($i = 0; $i < $this->getLengthInBits(); $i++) {
+            $buffer .= $this->get($i) ? '1' : '0';
+        }
+        return $buffer;
+    }
+
+    function get($index)
+    {
+        $bufIndex = (int)floor($index / 8);
+        return (($this->buffer[$bufIndex] >> (7 - $index % 8)) & 1) == 1;
+    }
+
+    function put($num, $length)
+    {
+
+        for ($i = 0; $i < $length; $i++) {
+            $this->putBit((($num >> ($length - $i - 1)) & 1) == 1);
+        }
+    }
+
+    function putBit($bit)
+    {
+
+        $bufIndex = (int)floor($this->length / 8);
+        if (count($this->buffer) <= $bufIndex) {
+            $this->buffer[] = 0;
+        }
+
+        if ($bit) {
+            $this->buffer[$bufIndex] |= (0x80 >> ($this->length % 8));
+        }
+
+        $this->length++;
+    }
+}
+

+ 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);
+    }
+}

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

@@ -0,0 +1,301 @@
+<?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;
+                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':
+                //    //电脑支付,跳转到自定义展示页面(FastAdmin独有)
+                //    $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 '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/sandboxnew/',
+        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';
+    }
+}

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

@@ -0,0 +1,449 @@
+<?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,
+            ] : []
+        );
+        $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 ('pay/getsignkey' === $endpoint ||
+            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('pay/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">
+                                <img src="{:addon_url('epay/api/qrcode',[],false)}?text={$payData.qr_code}">
+                                <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">
+                                <img src="{:addon_url('epay/api/qrcode',[],false)}?text={$payData.code_url}">
+                                <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
+ 12 - 0
addons/epay/view/index/index.html


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

@@ -0,0 +1,103 @@
+<!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" 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}<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-2020</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"></script>
+<script src="__ADDON__/js/common.js"></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]);
+    }
+}

+ 5 - 1
application/extra/addons.php

@@ -2,7 +2,11 @@
 
 return [
     'autoload' => false,
-    'hooks' => [],
+    'hooks' => [
+        'app_init' => [
+            'epay',
+        ],
+    ],
     'route' => [
         '/example$' => 'example/index/index',
         '/example/d/[:name]' => 'example/demo/index',

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

@@ -0,0 +1,180 @@
+/*!
+ * Start Bootstrap - Modern Business (http://startbootstrap.com/)
+ * Copyright 2013-2016 Start Bootstrap
+ * Licensed under MIT (https://github.com/BlackrockDigital/startbootstrap/blob/gh-pages/LICENSE)
+ */
+/* Global Styles */
+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;
+  }
+}

+ 20 - 0
public/assets/addons/epay/css/epay.css

@@ -0,0 +1,20 @@
+@import url("../../../css/bootstrap.min.css");
+@import url("../../../libs/font-awesome/css/font-awesome.min.css");
+html,
+body {
+  height: 100%;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+  font-weight: 400;
+  overflow-x: hidden;
+  overflow-y: auto;
+  background: #f4f6f8;
+  font-size: 14px;
+  color: #616161;
+}
+.container {
+  max-width: 850px;
+  margin: 0 auto;
+  padding: 50px;
+}

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


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

@@ -0,0 +1,65 @@
+$(function () {
+
+    if ($('.carousel').length > 0) {
+        $('.carousel').carousel({
+            interval: 5000 //changes the speed
+        });
+    }
+
+    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();
+        });
+    }
+
+    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();
+    }
+
+});

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

@@ -0,0 +1,229 @@
+/*!
+ * Start Bootstrap - Modern Business (http://startbootstrap.com/)
+ * Copyright 2013-2016 Start Bootstrap
+ * Licensed under MIT (https://github.com/BlackrockDigital/startbootstrap/blob/gh-pages/LICENSE)
+ */
+
+/* Global Styles */
+
+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;
+    }
+}

+ 28 - 0
public/assets/addons/epay/less/epay.less

@@ -0,0 +1,28 @@
+@import (reference) "../../../../public/assets/less/bootstrap-less/mixins.less";
+@import (reference) "../../../../public/assets/less/bootstrap-less/variables.less";
+@import (reference) "../../../../public/assets/less/fastadmin/mixins.less";
+@import (reference) "../../../../public/assets/less/fastadmin/variables.less";
+@import "../../../../public/assets/less/lesshat.less";
+@import url("../../../css/bootstrap.min.css");
+@import url("../../../libs/font-awesome/css/font-awesome.min.css");
+
+html,
+body {
+  height: 100%;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+  font-weight: 400;
+  overflow-x: hidden;
+  overflow-y: auto;
+  background: #f4f6f8;
+  font-size: 14px;
+  color: #616161;
+
+}
+
+.container {
+  max-width: 850px;
+  margin: 0 auto;
+  padding:50px;
+}

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