作者 郭文星

'2023-5-25后台代码跟新'

正在显示 61 个修改的文件 包含 4815 行增加0 行删除

要显示太多修改。

为保证性能只显示 61 of 61+ 个文件。

  1 +{"files":["application\\admin\\controller\\Epay.php","public\\assets\\addons\\epay\\css\\common.css","public\\assets\\addons\\epay\\images\\alipay.png","public\\assets\\addons\\epay\\images\\expired.png","public\\assets\\addons\\epay\\images\\logo-alipay.png","public\\assets\\addons\\epay\\images\\logo-wechat.png","public\\assets\\addons\\epay\\images\\paid.png","public\\assets\\addons\\epay\\images\\scan.png","public\\assets\\addons\\epay\\images\\screenshot-alipay.png","public\\assets\\addons\\epay\\images\\screenshot-wechat.png","public\\assets\\addons\\epay\\images\\wechat.png","public\\assets\\addons\\epay\\js\\common.js","public\\assets\\addons\\epay\\js\\jquery.qrcode.min.js","public\\assets\\addons\\epay\\less\\common.less"],"license":"regular","licenseto":"15629","licensekey":"ArY9zvN8lSfKu4Pw cYmPbH3AGCnkWBKBpdP6OA==","domains":["cardverification.com"],"licensecodes":[],"validations":["867a7ee97b89c3a1f31235c70da23ecf"]}
  1 +<?php
  2 +
  3 +namespace addons\epay;
  4 +
  5 +use think\Addons;
  6 +use think\Config;
  7 +use think\Loader;
  8 +
  9 +/**
  10 + * 微信支付宝整合插件
  11 + */
  12 +class Epay extends Addons
  13 +{
  14 +
  15 + /**
  16 + * 插件安装方法
  17 + * @return bool
  18 + */
  19 + public function install()
  20 + {
  21 +
  22 + return true;
  23 + }
  24 +
  25 + /**
  26 + * 插件卸载方法
  27 + * @return bool
  28 + */
  29 + public function uninstall()
  30 + {
  31 +
  32 + return true;
  33 + }
  34 +
  35 + /**
  36 + * 插件启用方法
  37 + * @return bool
  38 + */
  39 + public function enable()
  40 + {
  41 +
  42 + return true;
  43 + }
  44 +
  45 + /**
  46 + * 插件禁用方法
  47 + * @return bool
  48 + */
  49 + public function disable()
  50 + {
  51 +
  52 + return true;
  53 + }
  54 +
  55 + /**
  56 + * 添加命名空间
  57 + */
  58 + public function appInit()
  59 + {
  60 + //添加命名空间
  61 + if (!class_exists('\Yansongda\Pay\Pay')) {
  62 + Loader::addNamespace('Yansongda\Pay', ADDON_PATH . 'epay' . DS . 'library' . DS . 'Yansongda' . DS . 'Pay' . DS);
  63 + }
  64 + if (!class_exists('\Yansongda\Supports\Logger')) {
  65 + Loader::addNamespace('Yansongda\Supports', ADDON_PATH . 'epay' . DS . 'library' . DS . 'Yansongda' . DS . 'Supports' . DS);
  66 + }
  67 + }
  68 +
  69 +}
  1 +<form id="config-form" class="edit-form form-horizontal" role="form" data-toggle="validator" method="POST" action="">
  2 +
  3 + <div class="panel panel-default panel-intro">
  4 + <div class="panel-heading">
  5 + <ul class="nav nav-tabs nav-group">
  6 + <li class="active"><a href="#wechat" data-toggle="tab">微信支付</a></li>
  7 + <li><a href="#alipay" data-toggle="tab">支付宝</a></li>
  8 + </ul>
  9 + </div>
  10 +
  11 + <div class="panel-body">
  12 + <div id="myTabContent" class="tab-content">
  13 + {foreach $addon.config as $item}
  14 + {if $item.name=='wechat'}
  15 + <div class="tab-pane fade active in" id="wechat">
  16 + <table class="table table-striped table-config">
  17 + <tbody>
  18 + <tr>
  19 + <td width="15%">APP的app_id</td>
  20 + <td>
  21 + <div class="row">
  22 + <div class="col-sm-8 col-xs-12">
  23 + <input type="text" name="row[wechat][appid]" value="{$item.value.appid|default=''}" class="form-control" data-rule="" data-tip="APP应用中支付时使用"/>
  24 + </div>
  25 + <div class="col-sm-4"></div>
  26 + </div>
  27 + </td>
  28 + </tr>
  29 + <tr>
  30 + <td>公众号的app_id</td>
  31 + <td>
  32 + <div class="row">
  33 + <div class="col-sm-8 col-xs-12">
  34 + <input type="text" name="row[wechat][app_id]" value="{$item.value.app_id|default=''}" class="form-control" data-rule="" data-tip="公众号中支付时使用"/>
  35 + </div>
  36 + <div class="col-sm-4"></div>
  37 + </div>
  38 + </td>
  39 + </tr>
  40 + <tr>
  41 + <td>公众号的app_secret</td>
  42 + <td>
  43 + <div class="row">
  44 + <div class="col-sm-8 col-xs-12">
  45 + <input type="text" name="row[wechat][app_secret]" value="{$item.value.app_secret|default=''}" class="form-control" data-rule="" data-tip="仅在需要获取Openid时使用"/>
  46 + </div>
  47 + <div class="col-sm-4"></div>
  48 + </div>
  49 + </td>
  50 + </tr>
  51 + <tr>
  52 + <td>小程序的app_id</td>
  53 + <td>
  54 + <div class="row">
  55 + <div class="col-sm-8 col-xs-12">
  56 + <input type="text" name="row[wechat][miniapp_id]" value="{$item.value.miniapp_id|default=''}" class="form-control" data-rule="" data-tip="仅在小程序支付时使用"/>
  57 + </div>
  58 + <div class="col-sm-4"></div>
  59 + </div>
  60 + </td>
  61 + </tr>
  62 + <tr>
  63 + <td>微信支付商户号ID</td>
  64 + <td>
  65 + <div class="row">
  66 + <div class="col-sm-8 col-xs-12">
  67 + <input type="text" name="row[wechat][mch_id]" value="{$item.value.mch_id|default=''}" class="form-control" data-rule="" data-tip=""/>
  68 + </div>
  69 + <div class="col-sm-4"></div>
  70 + </div>
  71 + </td>
  72 + </tr>
  73 + <tr>
  74 + <td>微信支付商户的密钥</td>
  75 + <td>
  76 + <div class="row">
  77 + <div class="col-sm-8 col-xs-12">
  78 + <input type="text" name="row[wechat][key]" value="{$item.value.key|default=''}" class="form-control" data-rule="" data-tip=""/>
  79 + </div>
  80 + <div class="col-sm-4"></div>
  81 + </div>
  82 + </td>
  83 + </tr>
  84 + <tr>
  85 + <td>支付模式</td>
  86 + <td>
  87 + <div class="row">
  88 + <div class="col-sm-8 col-xs-12">
  89 + {:Form::radios('row[wechat][mode]',['normal'=>'正式环境','dev'=>'沙箱环境','service'=>'服务商模式'],$item.value.mode??'normal')}
  90 + </div>
  91 + <div class="col-sm-4"></div>
  92 + </div>
  93 + </td>
  94 + </tr>
  95 + <tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}">
  96 + <td>子商户商户号ID</td>
  97 + <td>
  98 + <div class="row">
  99 + <div class="col-sm-8 col-xs-12">
  100 + <input type="text" name="row[wechat][sub_mch_id]" value="{$item.value.sub_mch_id|default=''}" class="form-control" data-rule="" data-tip="如果未用到子商户,请勿填写"/>
  101 + </div>
  102 + <div class="col-sm-4"></div>
  103 + </div>
  104 + </td>
  105 + </tr>
  106 + <tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}">
  107 + <td>子商户APP的app_id</td>
  108 + <td>
  109 + <div class="row">
  110 + <div class="col-sm-8 col-xs-12">
  111 + <input type="text" name="row[wechat][sub_appid]" value="{$item.value.sub_appid|default=''}" class="form-control" data-rule="" data-tip="如果未用到子商户,请勿填写"/>
  112 + </div>
  113 + <div class="col-sm-4"></div>
  114 + </div>
  115 + </td>
  116 + </tr>
  117 + <tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}">
  118 + <td>子商户公众号的app_id</td>
  119 + <td>
  120 + <div class="row">
  121 + <div class="col-sm-8 col-xs-12">
  122 + <input type="text" name="row[wechat][sub_app_id]" value="{$item.value.sub_app_id|default=''}" class="form-control" data-rule="" data-tip="如果未用到子商户,请勿填写"/>
  123 + </div>
  124 + <div class="col-sm-4"></div>
  125 + </div>
  126 + </td>
  127 + </tr>
  128 + <tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}">
  129 + <td>子商户小程序的app_id</td>
  130 + <td>
  131 + <div class="row">
  132 + <div class="col-sm-8 col-xs-12">
  133 + <input type="text" name="row[wechat][sub_miniapp_id]" value="{$item.value.sub_miniapp_id|default=''}" class="form-control" data-rule="" data-tip="如果未用到子商户,请勿填写"/>
  134 + </div>
  135 + <div class="col-sm-4"></div>
  136 + </div>
  137 + </td>
  138 + </tr>
  139 + <tr>
  140 + <td>回调通知地址</td>
  141 + <td>
  142 + <div class="row">
  143 + <div class="col-sm-8 col-xs-12">
  144 + <input type="text" name="row[wechat][notify_url]" value="{$item.value.notify_url|default=''}" class="form-control" data-rule="" data-tip="请勿随意修改,实际以逻辑代码中请求的为准"/>
  145 + </div>
  146 + <div class="col-sm-4"></div>
  147 + </div>
  148 + </td>
  149 + </tr>
  150 + <tr>
  151 + <td>微信支付API证书cert</td>
  152 + <td>
  153 + <div class="row">
  154 + <div class="col-sm-8 col-xs-12">
  155 + <div class="input-group">
  156 + <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="可选, 仅在退款、红包等情况时需要用到">
  157 + <div class="input-group-addon no-border no-padding">
  158 + <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>
  159 + </div>
  160 + <span class="msg-box n-right" for="c-cert_client"></span>
  161 + </div>
  162 + <div style="margin-top:5px;"><a href="https://pay.weixin.qq.com" target="_blank"><i class="fa fa-question-circle"></i> 如何获取微信支付API证书?</a></div>
  163 + </div>
  164 + <div class="col-sm-4"></div>
  165 + </div>
  166 + </td>
  167 + </tr>
  168 + <tr>
  169 + <td>微信支付API证书key</td>
  170 + <td>
  171 + <div class="row">
  172 + <div class="col-sm-8 col-xs-12">
  173 + <div class="input-group">
  174 + <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="可选, 仅在退款、红包等情况时需要用到">
  175 + <div class="input-group-addon no-border no-padding">
  176 + <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>
  177 + </div>
  178 + <span class="msg-box n-right" for="c-cert_key"></span>
  179 + </div>
  180 + <div style="margin-top:5px;"><a href="https://pay.weixin.qq.com" target="_blank"><i class="fa fa-question-circle"></i> 如何获取微信支付API证书?</a></div>
  181 + </div>
  182 + <div class="col-sm-4"></div>
  183 + </div>
  184 + </td>
  185 + </tr>
  186 +
  187 + <tr>
  188 + <td>记录日志</td>
  189 + <td>
  190 + <div class="row">
  191 + <div class="col-sm-8 col-xs-12">
  192 + {:Form::radios('row[wechat][log]',['1'=>'开启','0'=>'关闭'],$item.value.log)}
  193 + </div>
  194 + <div class="col-sm-4"></div>
  195 + </div>
  196 + </td>
  197 + </tr>
  198 + </tbody>
  199 + </table>
  200 + </div>
  201 + {elseif $item.name=='alipay'}
  202 + <div class="tab-pane fade" id="alipay">
  203 + <table class="table table-striped table-config">
  204 + <tbody>
  205 + <tr>
  206 + <td>支付模式</td>
  207 + <td>
  208 + <div class="row">
  209 + <div class="col-sm-12 col-xs-12">
  210 + {:Form::radios('row[alipay][mode]',['normal'=>'正式环境','dev'=>'沙箱环境'],$item.value.mode??'normal')}
  211 +
  212 + <div style="margin-top:5px;" data-mode="dev" class="text-muted {if ($item.value.mode??'')==='normal'}hidden{/if}">
  213 + <i class="fa fa-info-circle"></i> 如果使用沙箱环境,务必使用沙箱的app_id和沙箱证书,以及使用沙箱账号进行测试。<br>
  214 + 沙箱环境:<a href="https://openhome.alipay.com/develop/sandbox/app" target="_blank">https://openhome.alipay.com/develop/sandbox/app</a>
  215 + </div>
  216 + </div>
  217 + </div>
  218 + </td>
  219 + </tr>
  220 + <tr>
  221 + <td width="15%">应用ID(app_id)</td>
  222 + <td>
  223 + <div class="row">
  224 + <div class="col-sm-8 col-xs-12">
  225 + <input type="text" name="row[alipay][app_id]" value="{$item.value.app_id|default=''}" class="form-control" data-rule="" data-tip=""/>
  226 + </div>
  227 + <div class="col-sm-4"></div>
  228 + </div>
  229 + </td>
  230 + </tr>
  231 + <tr>
  232 + <td>回调通知地址</td>
  233 + <td>
  234 + <div class="row">
  235 + <div class="col-sm-8 col-xs-12">
  236 + <input type="text" name="row[alipay][notify_url]" value="{$item.value.notify_url|default=''}" class="form-control" data-rule="" data-tip="请勿随意修改,实际以逻辑代码中请求的为准"/>
  237 + </div>
  238 + <div class="col-sm-4"></div>
  239 + </div>
  240 + </td>
  241 + </tr>
  242 + <tr>
  243 + <td>支付跳转地址</td>
  244 + <td>
  245 + <div class="row">
  246 + <div class="col-sm-8 col-xs-12">
  247 + <input type="text" name="row[alipay][return_url]" value="{$item.value.return_url|default=''}" class="form-control" data-rule="" data-tip="请勿随意修改,实际以逻辑代码中请求的为准"/>
  248 + </div>
  249 + <div class="col-sm-4"></div>
  250 + </div>
  251 + </td>
  252 + </tr>
  253 + <tr>
  254 + <td>应用私钥(private_key)</td>
  255 + <td>
  256 + <div class="row">
  257 + <div class="col-sm-8 col-xs-12">
  258 + <input type="text" name="row[alipay][private_key]" value="{$item.value.private_key|default=''}" class="form-control" data-rule=""/>
  259 + <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>
  260 + </div>
  261 + <div class="col-sm-4"></div>
  262 + </div>
  263 + </td>
  264 + </tr>
  265 + <tr>
  266 + <td>签名方式</td>
  267 + <td>
  268 + <div>
  269 + <div class="radio">
  270 + <label for="row[alipay][signtype]-publickey"><input id="row[alipay][signtype]-publickey" name="row[alipay][signtype]" {if isset($item.value.signtype)&&$item.value.signtype=='publickey'}checked{/if} type="radio" value="publickey"> 公钥</label>
  271 + <label for="row[alipay][signtype]-cert"><input id="row[alipay][signtype]-cert" {if isset($item.value.signtype)&&$item.value.signtype=='cert'}checked{/if} name="row[alipay][signtype]" type="radio" value="cert"> 公钥证书</label>
  272 + </div>
  273 + </div>
  274 + <div style="margin-top:5px;" class="text-muted">
  275 + <i class="fa fa-info-circle"></i> 如果要使用转账、提现功能,则必须使用公钥证书
  276 + </div>
  277 + </td>
  278 + </tr>
  279 + <tr>
  280 + <td>支付宝公钥路径(alipay_public_key)</td>
  281 + <td>
  282 + <div class="row">
  283 + <div class="col-sm-8 col-xs-12">
  284 + <div class="input-group">
  285 + <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="公钥请直接粘贴,公钥证书请点击右侧的上传">
  286 + <div class="input-group-addon no-border no-padding">
  287 + <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>
  288 + </div>
  289 + <span class="msg-box n-right" for="c-ali_public_key"></span>
  290 + </div>
  291 + <div style="margin-top:5px;"><a href="https://opensupport.alipay.com/support/helpcenter/271/201602474998" target="_blank"><i class="fa fa-question-circle"></i> 如何获取支付宝公钥证书?</a></div>
  292 + </div>
  293 + <div class="col-sm-4"></div>
  294 + </div>
  295 + </td>
  296 + </tr>
  297 + <tr>
  298 + <td>应用公钥证书路径(app_cert_public_key)</td>
  299 + <td>
  300 + <div class="row">
  301 + <div class="col-sm-8 col-xs-12">
  302 + <div class="input-group">
  303 + <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}">
  304 + <div class="input-group-addon no-border no-padding">
  305 + <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>
  306 + </div>
  307 + <span class="msg-box n-right" for="c-app_cert_public_key"></span>
  308 + </div>
  309 + <div style="margin-top:5px;"><a href="https://opensupport.alipay.com/support/helpcenter/271/201602474998" target="_blank"><i class="fa fa-question-circle"></i> 如何获取应用公钥证书?</a></div>
  310 + </div>
  311 + <div class="col-sm-4"></div>
  312 + </div>
  313 + </td>
  314 + </tr>
  315 + <tr>
  316 + <td>支付宝根证书路径(alipay_root_cert)</td>
  317 + <td>
  318 + <div class="row">
  319 + <div class="col-sm-8 col-xs-12">
  320 + <div class="input-group">
  321 + <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}">
  322 + <div class="input-group-addon no-border no-padding">
  323 + <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>
  324 + </div>
  325 + <span class="msg-box n-right" for="c-alipay_root_cert"></span>
  326 + </div>
  327 + <div style="margin-top:5px;"><a href="https://opensupport.alipay.com/support/helpcenter/271/201602474998" target="_blank"><i class="fa fa-question-circle"></i> 如何获取支付宝证书?</a></div>
  328 + </div>
  329 + <div class="col-sm-4"></div>
  330 + </div>
  331 + </td>
  332 + </tr>
  333 +
  334 + <tr>
  335 + <td>记录日志</td>
  336 + <td>
  337 + <div class="row">
  338 + <div class="col-sm-8 col-xs-12">
  339 + {:Form::radios('row[alipay][log]',['1'=>'开启','0'=>'关闭'],$item.value.log)}
  340 + </div>
  341 + <div class="col-sm-4"></div>
  342 + </div>
  343 + </td>
  344 + </tr>
  345 +
  346 + <tr>
  347 + <td>PC端使用扫码支付</td>
  348 + <td>
  349 + <div class="row">
  350 + <div class="col-sm-8 col-xs-12">
  351 + {:Form::radios('row[alipay][scanpay]',['1'=>'开启','0'=>'关闭'],$item.value.scanpay??0)}
  352 + </div>
  353 + <div class="col-sm-4"></div>
  354 + </div>
  355 + </td>
  356 + </tr>
  357 + </tbody>
  358 + </table>
  359 + </div>
  360 + {/if}
  361 + {/foreach}
  362 + <div class="form-group layer-footer">
  363 + <label class="control-label col-xs-12 col-sm-2"></label>
  364 + <div class="col-xs-12 col-sm-8">
  365 + <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
  366 + <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
  367 + </div>
  368 + </div>
  369 + </div>
  370 + </div>
  371 + </div>
  372 +</form>
  373 +<script>
  374 + document.querySelectorAll("input[name='row[wechat][mode]']").forEach(function (i, j) {
  375 + i.addEventListener("click", function () {
  376 + document.querySelectorAll("#wechat table tr[data-type]").forEach(function (m, n) {
  377 + m.classList.add("hidden");
  378 + });
  379 + document.querySelectorAll("#wechat table tr[data-type='" + this.value + "']").forEach(function (m, n) {
  380 + m.classList.remove("hidden");
  381 + });
  382 + });
  383 + });
  384 + document.querySelectorAll("input[name='row[alipay][mode]']").forEach(function (i, j) {
  385 + i.addEventListener("click", function () {
  386 + document.querySelectorAll("#alipay [data-mode]").forEach(function (m, n) {
  387 + m.classList.add("hidden");
  388 + });
  389 + document.querySelectorAll("#alipay [data-mode='" + this.value + "']").forEach(function (m, n) {
  390 + m.classList.remove("hidden");
  391 + });
  392 + });
  393 + });
  394 +</script>
  1 +<?php
  2 +
  3 +return [
  4 + [
  5 + 'name' => 'wechat',
  6 + 'title' => '微信',
  7 + 'type' => 'array',
  8 + 'content' => [],
  9 + 'value' => [
  10 + 'appid' => '',
  11 + 'app_id' => '',
  12 + 'app_secret' => '',
  13 + 'miniapp_id' => '',
  14 + 'mch_id' => '',
  15 + 'key' => '',
  16 + 'mode' => 'normal',
  17 + 'sub_mch_id' => '',
  18 + 'sub_appid' => '',
  19 + 'sub_app_id' => '',
  20 + 'sub_miniapp_id' => '',
  21 + 'notify_url' => '/addons/epay/api/notifyx/type/wechat',
  22 + 'cert_client' => '/addons/epay/certs/apiclient_cert.pem',
  23 + 'cert_key' => '/addons/epay/certs/apiclient_key.pem',
  24 + 'log' => '1',
  25 + ],
  26 + 'rule' => '',
  27 + 'msg' => '',
  28 + 'tip' => '微信参数配置',
  29 + 'ok' => '',
  30 + 'extend' => '',
  31 + ],
  32 + [
  33 + 'name' => 'alipay',
  34 + 'title' => '支付宝',
  35 + 'type' => 'array',
  36 + 'content' => [],
  37 + 'value' => [
  38 + 'app_id' => '',
  39 + 'mode' => 'normal',
  40 + 'notify_url' => '/addons/epay/api/notifyx/type/alipay',
  41 + 'return_url' => '/addons/epay/api/returnx/type/alipay',
  42 + 'private_key' => '',
  43 + 'ali_public_key' => '',
  44 + 'app_cert_public_key' => '',
  45 + 'alipay_root_cert' => '',
  46 + 'log' => '1',
  47 + 'scanpay' => '0',
  48 + ],
  49 + 'rule' => 'required',
  50 + 'msg' => '',
  51 + 'tip' => '支付宝参数配置',
  52 + 'ok' => '',
  53 + 'extend' => '',
  54 + ]
  55 +];
  1 +<?php
  2 +
  3 +namespace addons\epay\controller;
  4 +
  5 +use addons\epay\library\Service;
  6 +use addons\epay\library\Wechat;
  7 +use addons\third\model\Third;
  8 +use app\common\library\Auth;
  9 +use think\addons\Controller;
  10 +use think\Response;
  11 +use think\Session;
  12 +use Yansongda\Pay\Exceptions\GatewayException;
  13 +use Yansongda\Pay\Pay;
  14 +
  15 +/**
  16 + * API接口控制器
  17 + *
  18 + * @package addons\epay\controller
  19 + */
  20 +class Api extends Controller
  21 +{
  22 +
  23 + protected $layout = 'default';
  24 + protected $config = [];
  25 +
  26 + /**
  27 + * 默认方法
  28 + */
  29 + public function index()
  30 + {
  31 + return;
  32 + }
  33 +
  34 + /**
  35 + * 外部提交
  36 + */
  37 + public function submit()
  38 + {
  39 + $this->request->filter('trim');
  40 + $out_trade_no = $this->request->request("out_trade_no");
  41 + $title = $this->request->request("title");
  42 + $amount = $this->request->request('amount');
  43 + $type = $this->request->request('type');
  44 + $method = $this->request->request('method', 'web');
  45 + $openid = $this->request->request('openid', '');
  46 + $auth_code = $this->request->request('auth_code', '');
  47 + $notifyurl = $this->request->request('notifyurl', '');
  48 + $returnurl = $this->request->request('returnurl', '');
  49 +
  50 + if (!$amount || $amount < 0) {
  51 + $this->error("支付金额必须大于0");
  52 + }
  53 +
  54 + if (!$type || !in_array($type, ['alipay', 'wechat'])) {
  55 + $this->error("支付类型错误");
  56 + }
  57 +
  58 + $params = [
  59 + 'type' => $type,
  60 + 'out_trade_no' => $out_trade_no,
  61 + 'title' => $title,
  62 + 'amount' => $amount,
  63 + 'method' => $method,
  64 + 'openid' => $openid,
  65 + 'auth_code' => $auth_code,
  66 + 'notifyurl' => $notifyurl,
  67 + 'returnurl' => $returnurl,
  68 + ];
  69 + return Service::submitOrder($params);
  70 + }
  71 +
  72 + /**
  73 + * 微信支付(公众号支付&PC扫码支付)
  74 + * @return string
  75 + */
  76 + public function wechat()
  77 + {
  78 + $config = Service::getConfig('wechat');
  79 +
  80 + $isWechat = stripos($this->request->server('HTTP_USER_AGENT'), 'MicroMessenger') !== false;
  81 + $isMobile = $this->request->isMobile();
  82 + $this->view->assign("isWechat", $isWechat);
  83 + $this->view->assign("isMobile", $isMobile);
  84 +
  85 + //发起PC支付(Scan支付)(PC扫码模式)
  86 + if ($this->request->isAjax()) {
  87 + $pay = Pay::wechat($config);
  88 + $orderid = $this->request->post("orderid");
  89 + try {
  90 + $result = $pay->find($orderid, 'scan');
  91 + if ($result['return_code'] == 'SUCCESS' && $result['result_code'] == 'SUCCESS') {
  92 + $this->success("", "", ['status' => $result['trade_state']]);
  93 + } else {
  94 + $this->error("查询失败");
  95 + }
  96 + } catch (GatewayException $e) {
  97 + $this->error("查询失败");
  98 + }
  99 + }
  100 +
  101 + $orderData = Session::get("wechatorderdata");
  102 + if (!$orderData) {
  103 + $this->error("请求参数错误");
  104 + }
  105 + if ($isWechat && $isMobile) {
  106 + //发起公众号(jsapi支付),openid必须
  107 +
  108 + //如果没有openid,则自动去获取openid
  109 + if (!isset($orderData['openid']) || !$orderData['openid']) {
  110 + $orderData['openid'] = Service::getOpenid();
  111 + }
  112 +
  113 + $orderData['method'] = 'mp';
  114 + $type = 'jsapi';
  115 + $payData = Service::submitOrder($orderData);
  116 + if (!isset($payData['paySign'])) {
  117 + $this->error("创建订单失败,请返回重试", "");
  118 + }
  119 + } else {
  120 + $orderData['method'] = 'scan';
  121 + $type = 'pc';
  122 + $payData = Service::submitOrder($orderData);
  123 + if (!isset($payData['code_url'])) {
  124 + $this->error("创建订单失败,请返回重试", "");
  125 + }
  126 + }
  127 + $this->view->assign("orderData", $orderData);
  128 + $this->view->assign("payData", $payData);
  129 + $this->view->assign("type", $type);
  130 +
  131 + $this->view->assign("title", "微信支付");
  132 + return $this->view->fetch();
  133 + }
  134 +
  135 + /**
  136 + * 支付宝支付(PC扫码支付)
  137 + * @return string
  138 + */
  139 + public function alipay()
  140 + {
  141 + $config = Service::getConfig('alipay');
  142 +
  143 + $isWechat = stripos($this->request->server('HTTP_USER_AGENT'), 'MicroMessenger') !== false;
  144 + $isMobile = $this->request->isMobile();
  145 + $this->view->assign("isWechat", $isWechat);
  146 + $this->view->assign("isMobile", $isMobile);
  147 +
  148 + if ($this->request->isAjax()) {
  149 + $orderid = $this->request->post("orderid");
  150 + $pay = Pay::alipay($config);
  151 + try {
  152 + $result = $pay->find($orderid, 'scan');
  153 + if ($result['code'] == '10000' && $result['trade_status'] == 'TRADE_SUCCESS') {
  154 + $this->success("", "", ['status' => $result['trade_status']]);
  155 + } else {
  156 + $this->error("查询失败");
  157 + }
  158 + } catch (GatewayException $e) {
  159 + $this->error("查询失败");
  160 + }
  161 + }
  162 +
  163 + //发起PC支付(Scan支付)(PC扫码模式)
  164 + $orderData = Session::get("alipayorderdata");
  165 + if (!$orderData) {
  166 + $this->error("请求参数错误");
  167 + }
  168 +
  169 + $orderData['method'] = 'scan';
  170 + $payData = Service::submitOrder($orderData);
  171 + if (!isset($payData['qr_code'])) {
  172 + $this->error("创建订单失败,请返回重试");
  173 + }
  174 +
  175 + $type = 'pc';
  176 + $this->view->assign("orderData", $orderData);
  177 + $this->view->assign("payData", $payData);
  178 + $this->view->assign("type", $type);
  179 + $this->view->assign("title", "支付宝支付");
  180 + return $this->view->fetch();
  181 + }
  182 +
  183 + /**
  184 + * 支付成功回调
  185 + */
  186 + public function notifyx()
  187 + {
  188 + $type = $this->request->param('type');
  189 + $pay = \addons\epay\library\Service::checkNotify($type);
  190 + if (!$pay) {
  191 + echo '签名错误';
  192 + return;
  193 + }
  194 + $data = $pay->verify();
  195 +
  196 + //你可以在这里你的业务处理逻辑,比如处理你的订单状态、给会员加余额等等功能
  197 + //下面这句必须要执行,且在此之前不能有任何输出
  198 + return $pay->success()->send();
  199 + }
  200 +
  201 + /**
  202 + * 支付成功返回
  203 + */
  204 + public function returnx()
  205 + {
  206 + $type = $this->request->param('type');
  207 + if (Service::checkReturn($type)) {
  208 + echo '签名错误';
  209 + return;
  210 + }
  211 +
  212 + //你可以在这里定义你的提示信息,但切记不可在此编写逻辑
  213 + $this->success("恭喜你!支付成功!", addon_url("epay/index/index"));
  214 +
  215 + return;
  216 + }
  217 +
  218 +}
  1 +<?php
  2 +
  3 +namespace addons\epay\controller;
  4 +
  5 +use addons\epay\library\Service;
  6 +use fast\Random;
  7 +use think\addons\Controller;
  8 +use Exception;
  9 +
  10 +/**
  11 + * 微信支付宝整合插件首页
  12 + *
  13 + * 此控制器仅用于开发展示说明和测试,请自行添加一个新的控制器进行处理返回和回调事件,同时删除此控制器文件
  14 + *
  15 + * Class Index
  16 + * @package addons\epay\controller
  17 + */
  18 +class Index extends Controller
  19 +{
  20 +
  21 + protected $layout = 'default';
  22 +
  23 + protected $config = [];
  24 +
  25 + public function _initialize()
  26 + {
  27 + parent::_initialize();
  28 + if (!config("app_debug")) {
  29 + $this->error("仅在开发环境下查看");
  30 + }
  31 + }
  32 +
  33 + public function index()
  34 + {
  35 + $this->view->assign("title", "微信支付宝整合");
  36 + return $this->view->fetch();
  37 + }
  38 +
  39 + /**
  40 + * 体验,仅供开发测试
  41 + */
  42 + public function experience()
  43 + {
  44 + $amount = $this->request->request('amount');
  45 + $type = $this->request->request('type');
  46 + $method = $this->request->request('method');
  47 +
  48 + if (!$amount || $amount < 0) {
  49 + $this->error("支付金额必须大于0");
  50 + }
  51 +
  52 + if (!$type || !in_array($type, ['alipay', 'wechat'])) {
  53 + $this->error("支付类型不能为空");
  54 + }
  55 +
  56 + //订单号
  57 + $out_trade_no = date("YmdHis") . mt_rand(100000, 999999);
  58 +
  59 + //订单标题
  60 + $title = '测试订单';
  61 +
  62 + //回调链接
  63 + $notifyurl = $this->request->root(true) . '/addons/epay/index/notifyx/paytype/' . $type;
  64 + $returnurl = $this->request->root(true) . '/addons/epay/index/returnx/paytype/' . $type . '/out_trade_no/' . $out_trade_no;
  65 +
  66 + $response = Service::submitOrder($amount, $out_trade_no, $type, $title, $notifyurl, $returnurl, $method);
  67 +
  68 + return $response;
  69 + }
  70 +
  71 + /**
  72 + * 支付成功,仅供开发测试
  73 + */
  74 + public function notifyx()
  75 + {
  76 + $paytype = $this->request->param('paytype');
  77 + $pay = Service::checkNotify($paytype);
  78 + if (!$pay) {
  79 + echo '签名错误';
  80 + return;
  81 + }
  82 + $data = $pay->verify();
  83 + try {
  84 + $payamount = $paytype == 'alipay' ? $data['total_amount'] : $data['total_fee'] / 100;
  85 + $out_trade_no = $data['out_trade_no'];
  86 +
  87 + //你可以在此编写订单逻辑
  88 + } catch (Exception $e) {
  89 + }
  90 +
  91 + //下面这句必须要执行,且在此之前不能有任何输出
  92 + return $pay->success()->send();
  93 + }
  94 +
  95 + /**
  96 + * 支付返回,仅供开发测试
  97 + */
  98 + public function returnx()
  99 + {
  100 + $paytype = $this->request->param('paytype');
  101 + $out_trade_no = $this->request->param('out_trade_no');
  102 + $pay = Service::checkReturn($paytype);
  103 + if (!$pay) {
  104 + $this->error('签名错误', '');
  105 + }
  106 +
  107 + //你可以在这里通过out_trade_no去验证订单状态
  108 + //但是不可以在此编写订单逻辑!!!
  109 +
  110 + $this->success("请返回网站查看支付结果", addon_url("epay/index/index"));
  111 + }
  112 +
  113 +}
  1 +name = epay
  2 +title = 微信支付宝整合
  3 +intro = 可用于快速整合企业微信、支付宝支付功能
  4 +author = FastAdmin
  5 +website = https://www.fastadmin.net
  6 +version = 1.2.10
  7 +state = 1
  8 +url = /addons/epay
  9 +license = regular
  10 +licenseto = 15629
  1 +<?php
  2 +
  3 +namespace addons\epay\library;
  4 +
  5 +class Collection extends \Yansongda\Supports\Collection
  6 +{
  7 +
  8 + /**
  9 + * 创建 Collection 实例
  10 + * @access public
  11 + * @param array $items 数据
  12 + * @return static
  13 + */
  14 + public static function make($items = [])
  15 + {
  16 + return new static($items);
  17 + }
  18 +}
  1 +<?php
  2 +
  3 +namespace addons\epay\library;
  4 +
  5 +use think\Exception;
  6 +
  7 +class OrderException extends Exception
  8 +{
  9 + public function __construct($message = "", $code = 0, $data = [])
  10 + {
  11 + $this->message = $message;
  12 + $this->code = $code;
  13 + $this->data = $data;
  14 + }
  15 +
  16 +}
  1 +<?php
  2 +
  3 +namespace addons\epay\library;
  4 +
  5 +class RedirectResponse extends \Symfony\Component\HttpFoundation\RedirectResponse implements \JsonSerializable, \Serializable
  6 +{
  7 + public function __toString()
  8 + {
  9 + return $this->getContent();
  10 + }
  11 +
  12 + public function setTargetUrl($url)
  13 + {
  14 + if ('' === ($url ?? '')) {
  15 + throw new \InvalidArgumentException('无法跳转到空页面');
  16 + }
  17 +
  18 + $this->targetUrl = $url;
  19 +
  20 + $this->setContent(
  21 + sprintf('<!DOCTYPE html>
  22 +<html>
  23 + <head>
  24 + <meta charset="UTF-8" />
  25 + <meta http-equiv="refresh" content="0;url=\'%1$s\'" />
  26 +
  27 + <title>正在跳转支付 %1$s</title>
  28 + </head>
  29 + <body>
  30 + <div id="redirect" style="display:none;">正在跳转支付 <a href="%1$s">%1$s</a></div>
  31 + <script type="text/javascript">
  32 + setTimeout(function(){
  33 + document.getElementById("redirect").style.display = "block";
  34 + }, 1000);
  35 + </script>
  36 + </body>
  37 +</html>', htmlspecialchars($url, \ENT_QUOTES, 'UTF-8')));
  38 +
  39 + $this->headers->set('Location', $url);
  40 +
  41 + return $this;
  42 + }
  43 +
  44 + public function jsonSerialize()
  45 + {
  46 + return $this->getContent();
  47 + }
  48 +
  49 + public function serialize()
  50 + {
  51 + return serialize($this->content);
  52 + }
  53 +
  54 + public function unserialize($serialized)
  55 + {
  56 + return $this->content = unserialize($serialized);
  57 + }
  58 +}
  1 +<?php
  2 +
  3 +namespace addons\epay\library;
  4 +
  5 +class Response extends \Symfony\Component\HttpFoundation\Response implements \JsonSerializable, \Serializable
  6 +{
  7 + public function __toString()
  8 + {
  9 + return $this->getContent();
  10 + }
  11 +
  12 + public function jsonSerialize()
  13 + {
  14 + return $this->getContent();
  15 + }
  16 +
  17 + public function serialize()
  18 + {
  19 + return serialize($this->content);
  20 + }
  21 +
  22 + public function unserialize($serialized)
  23 + {
  24 + return $this->content = unserialize($serialized);
  25 + }
  26 +}
  1 +<?php
  2 +
  3 +namespace addons\epay\library;
  4 +
  5 +use addons\third\model\Third;
  6 +use app\common\library\Auth;
  7 +use Exception;
  8 +use think\Session;
  9 +use Yansongda\Pay\Pay;
  10 +use Yansongda\Supports\Str;
  11 +
  12 +/**
  13 + * 订单服务类
  14 + *
  15 + * @package addons\epay\library
  16 + */
  17 +class Service
  18 +{
  19 +
  20 + /**
  21 + * 提交订单
  22 + * @param array|float $amount 订单金额
  23 + * @param string $orderid 订单号
  24 + * @param string $type 支付类型,可选alipay或wechat
  25 + * @param string $title 订单标题
  26 + * @param string $notifyurl 通知回调URL
  27 + * @param string $returnurl 跳转返回URL
  28 + * @param string $method 支付方法
  29 + * @return Response|RedirectResponse|Collection
  30 + * @throws Exception
  31 + */
  32 + public static function submitOrder($amount, $orderid = null, $type = null, $title = null, $notifyurl = null, $returnurl = null, $method = null, $openid = '')
  33 + {
  34 + if (!is_array($amount)) {
  35 + $params = [
  36 + 'amount' => $amount,
  37 + 'orderid' => $orderid,
  38 + 'type' => $type,
  39 + 'title' => $title,
  40 + 'notifyurl' => $notifyurl,
  41 + 'returnurl' => $returnurl,
  42 + 'method' => $method,
  43 + 'openid' => $openid,
  44 + ];
  45 + } else {
  46 + $params = $amount;
  47 + }
  48 + $type = isset($params['type']) && in_array($params['type'], ['alipay', 'wechat']) ? $params['type'] : 'wechat';
  49 + $method = isset($params['method']) ? $params['method'] : 'web';
  50 + $orderid = isset($params['orderid']) ? $params['orderid'] : date("YmdHis") . mt_rand(100000, 999999);
  51 + $amount = isset($params['amount']) ? $params['amount'] : 1;
  52 + $title = isset($params['title']) ? $params['title'] : "支付";
  53 + $auth_code = isset($params['auth_code']) ? $params['auth_code'] : '';
  54 + $openid = isset($params['openid']) ? $params['openid'] : '';
  55 +
  56 + $request = request();
  57 + $notifyurl = isset($params['notifyurl']) ? $params['notifyurl'] : $request->root(true) . '/addons/epay/index/' . $type . 'notify';
  58 + $returnurl = isset($params['returnurl']) ? $params['returnurl'] : $request->root(true) . '/addons/epay/index/' . $type . 'return/out_trade_no/' . $orderid;
  59 + $html = '';
  60 + $config = Service::getConfig($type);
  61 + $config['notify_url'] = $notifyurl;
  62 + $config['return_url'] = $returnurl;
  63 + $isWechat = strpos($request->server('HTTP_USER_AGENT'), 'MicroMessenger') !== false;
  64 +
  65 + $result = null;
  66 + if ($type == 'alipay') {
  67 + //如果是PC支付,判断当前环境,进行跳转
  68 + if ($method == 'web') {
  69 + //如果是微信环境或后台配置PC使用扫码支付
  70 + if ($isWechat || $config['scanpay']) {
  71 + Session::set("alipayorderdata", $params);
  72 + $url = addon_url('epay/api/alipay', [], true, true);
  73 + return RedirectResponse::create($url);
  74 + } elseif ($request->isMobile()) {
  75 + $method = 'wap';
  76 + }
  77 + }
  78 + //创建支付对象
  79 + $pay = Pay::alipay($config);
  80 + $params = [
  81 + 'out_trade_no' => $orderid,//你的订单号
  82 + 'total_amount' => $amount,//单位元
  83 + 'subject' => $title,
  84 + ];
  85 +
  86 + switch ($method) {
  87 + case 'web':
  88 + //电脑支付
  89 + $result = $pay->web($params);
  90 + break;
  91 + case 'wap':
  92 + //手机网页支付
  93 + $result = $pay->wap($params);
  94 + break;
  95 + case 'app':
  96 + //APP支付
  97 + $result = $pay->app($params);
  98 + break;
  99 + case 'scan':
  100 + //扫码支付
  101 + $result = $pay->scan($params);
  102 + break;
  103 + case 'pos':
  104 + //刷卡支付必须要有auth_code
  105 + $params['auth_code'] = $auth_code;
  106 + $result = $pay->pos($params);
  107 + break;
  108 + case 'mini':
  109 + case 'miniapp':
  110 + //小程序支付
  111 + //小程序支付,直接返回字符串
  112 + //小程序支付必须要有buyer_id,这里使用openid
  113 + $params['buyer_id'] = $openid;
  114 + $result = $pay->mini($params);
  115 + break;
  116 + default:
  117 + }
  118 + } else {
  119 + //如果是PC支付,判断当前环境,进行跳转
  120 + if ($method == 'web') {
  121 + //如果是移动端,但不是微信环境
  122 + if ($request->isMobile() && !$isWechat) {
  123 + $method = 'wap';
  124 + } else {
  125 + Session::set("wechatorderdata", $params);
  126 + $url = addon_url('epay/api/wechat', [], true, true);
  127 + return RedirectResponse::create($url);
  128 + }
  129 + }
  130 +
  131 + //创建支付对象
  132 + $pay = Pay::wechat($config);
  133 + $params = [
  134 + 'out_trade_no' => $orderid,//你的订单号
  135 + 'body' => $title,
  136 + 'total_fee' => $amount * 100, //单位分
  137 + ];
  138 + switch ($method) {
  139 + //case 'web':
  140 + // //电脑支付,跳转到自定义展示页面
  141 + // $result = $pay->web($params);
  142 + // break;
  143 + case 'mp':
  144 + //公众号支付
  145 + //公众号支付必须有openid
  146 + $params['openid'] = $openid;
  147 + $result = $pay->mp($params);
  148 + break;
  149 + case 'wap':
  150 + //手机网页支付,跳转
  151 + $params['spbill_create_ip'] = $request->ip(0, false);
  152 + $result = $pay->wap($params);
  153 + break;
  154 + case 'app':
  155 + //APP支付,直接返回字符串
  156 + $result = $pay->app($params);
  157 + break;
  158 + case 'scan':
  159 + //扫码支付,直接返回字符串
  160 + $result = $pay->scan($params);
  161 + break;
  162 + case 'pos':
  163 + //刷卡支付,直接返回字符串
  164 + //刷卡支付必须要有auth_code
  165 + $params['auth_code'] = $auth_code;
  166 + $result = $pay->pos($params);
  167 + break;
  168 + case 'mini':
  169 + case 'miniapp':
  170 + //小程序支付,直接返回字符串
  171 + //小程序支付必须要有openid
  172 + $params['openid'] = $openid;
  173 + $result = $pay->miniapp($params);
  174 + break;
  175 + default:
  176 + }
  177 + }
  178 +
  179 + //使用重写的Response类、RedirectResponse、Collection类
  180 + if ($result instanceof \Symfony\Component\HttpFoundation\RedirectResponse) {
  181 + $result = RedirectResponse::create($result->getTargetUrl());
  182 + } elseif ($result instanceof \Symfony\Component\HttpFoundation\Response) {
  183 + $result = Response::create($result->getContent());
  184 + } elseif ($result instanceof \Yansongda\Supports\Collection) {
  185 + $result = Collection::make($result->all());
  186 + }
  187 +
  188 + return $result;
  189 + }
  190 +
  191 + /**
  192 + * 验证回调是否成功
  193 + * @param string $type 支付类型
  194 + * @param array $config 配置信息
  195 + * @return bool|\Yansongda\Pay\Gateways\Alipay|\Yansongda\Pay\Gateways\Wechat
  196 + */
  197 + public static function checkNotify($type, $config = [])
  198 + {
  199 + $type = strtolower($type);
  200 + if (!in_array($type, ['wechat', 'alipay'])) {
  201 + return false;
  202 + }
  203 + try {
  204 + $config = self::getConfig($type);
  205 + $pay = $type == 'wechat' ? Pay::wechat($config) : Pay::alipay($config);
  206 + $data = $pay->verify();
  207 +
  208 + if ($type == 'alipay') {
  209 + if (in_array($data['trade_status'], ['TRADE_SUCCESS', 'TRADE_FINISHED'])) {
  210 + return $pay;
  211 + }
  212 + } else {
  213 + return $pay;
  214 + }
  215 + } catch (Exception $e) {
  216 + return false;
  217 + }
  218 +
  219 + return false;
  220 + }
  221 +
  222 + /**
  223 + * 验证返回是否成功,请勿用于判断是否支付成功的逻辑验证
  224 + * 已弃用
  225 + *
  226 + * @param string $type 支付类型
  227 + * @param array $config 配置信息
  228 + * @return bool
  229 + * @deprecated 已弃用,请勿用于逻辑验证
  230 + */
  231 + public static function checkReturn($type, $config = [])
  232 + {
  233 + //由于PC及移动端无法获取请求的参数信息,取消return验证,均返回true
  234 + return true;
  235 + }
  236 +
  237 + /**
  238 + * 获取配置
  239 + * @param string $type 支付类型
  240 + * @return array|mixed
  241 + */
  242 + public static function getConfig($type = 'wechat')
  243 + {
  244 + $config = get_addon_config('epay');
  245 + $config = isset($config[$type]) ? $config[$type] : $config['wechat'];
  246 + if ($config['log']) {
  247 + $config['log'] = [
  248 + 'file' => LOG_PATH . 'epaylogs' . DS . $type . '-' . date("Y-m-d") . '.log',
  249 + 'level' => 'debug'
  250 + ];
  251 + }
  252 + if (isset($config['cert_client']) && substr($config['cert_client'], 0, 8) == '/addons/') {
  253 + $config['cert_client'] = ROOT_PATH . str_replace('/', DS, substr($config['cert_client'], 1));
  254 + }
  255 + if (isset($config['cert_key']) && substr($config['cert_key'], 0, 8) == '/addons/') {
  256 + $config['cert_key'] = ROOT_PATH . str_replace('/', DS, substr($config['cert_key'], 1));
  257 + }
  258 + if (isset($config['app_cert_public_key']) && substr($config['app_cert_public_key'], 0, 8) == '/addons/') {
  259 + $config['app_cert_public_key'] = ROOT_PATH . str_replace('/', DS, substr($config['app_cert_public_key'], 1));
  260 + }
  261 + if (isset($config['alipay_root_cert']) && substr($config['alipay_root_cert'], 0, 8) == '/addons/') {
  262 + $config['alipay_root_cert'] = ROOT_PATH . str_replace('/', DS, substr($config['alipay_root_cert'], 1));
  263 + }
  264 + if (isset($config['ali_public_key']) && (Str::endsWith($config['ali_public_key'], '.crt') || Str::endsWith($config['ali_public_key'], '.pem'))) {
  265 + $config['ali_public_key'] = ROOT_PATH . str_replace('/', DS, substr($config['ali_public_key'], 1));
  266 + }
  267 + // 可选
  268 + $config['http'] = [
  269 + 'timeout' => 10,
  270 + 'connect_timeout' => 10,
  271 + // 更多配置项请参考 [Guzzle](https://guzzle-cn.readthedocs.io/zh_CN/latest/request-options.html)
  272 + ];
  273 +
  274 + $config['notify_url'] = empty($config['notify_url']) ? addon_url('epay/api/notifyx', [], false) . '/type/' . $type : $config['notify_url'];
  275 + $config['notify_url'] = !preg_match("/^(http:\/\/|https:\/\/)/i", $config['notify_url']) ? request()->root(true) . $config['notify_url'] : $config['notify_url'];
  276 + $config['return_url'] = empty($config['return_url']) ? addon_url('epay/api/returnx', [], false) . '/type/' . $type : $config['return_url'];
  277 + $config['return_url'] = !preg_match("/^(http:\/\/|https:\/\/)/i", $config['return_url']) ? request()->root(true) . $config['return_url'] : $config['return_url'];
  278 + return $config;
  279 + }
  280 +
  281 + /**
  282 + * 获取微信Openid
  283 + *
  284 + * @return mixed|string
  285 + */
  286 + public static function getOpenid()
  287 + {
  288 + $config = self::getConfig('wechat');
  289 + $openid = '';
  290 + $auth = Auth::instance();
  291 + if ($auth->isLogin()) {
  292 + $third = get_addon_info('third');
  293 + if ($third && $third['state']) {
  294 + $thirdInfo = Third::where('user_id', $auth->id)->where('platform', 'wechat')->where('apptype', 'mp')->find();
  295 + $openid = $thirdInfo ? $thirdInfo['openid'] : '';
  296 + }
  297 + }
  298 + if (!$openid) {
  299 + $openid = Session::get("openid");
  300 +
  301 + //如果未传openid,则去读取openid
  302 + if (!$openid) {
  303 + $wechat = new Wechat($config['app_id'], $config['app_secret']);
  304 + $openid = $wechat->getOpenid();
  305 + }
  306 + }
  307 + return $openid;
  308 + }
  309 +
  310 +}
  1 +<?php
  2 +
  3 +namespace addons\epay\library;
  4 +
  5 +use fast\Http;
  6 +use think\Cache;
  7 +use think\Session;
  8 +
  9 +/**
  10 + * 微信授权
  11 + *
  12 + */
  13 +class Wechat
  14 +{
  15 + private $app_id = '';
  16 + private $app_secret = '';
  17 + private $scope = 'snsapi_userinfo';
  18 +
  19 + public function __construct($app_id, $app_secret)
  20 + {
  21 + $this->app_id = $app_id;
  22 + $this->app_secret = $app_secret;
  23 + }
  24 +
  25 + /**
  26 + * 获取微信授权链接
  27 + *
  28 + * @return string
  29 + */
  30 + public function getAuthorizeUrl()
  31 + {
  32 + $redirect_uri = addon_url('epay/api/wechat', [], true, true);
  33 + $redirect_uri = urlencode($redirect_uri);
  34 + $state = \fast\Random::alnum();
  35 + Session::set('state', $state);
  36 + 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";
  37 + }
  38 +
  39 + /**
  40 + * 获取微信openid
  41 + *
  42 + * @return mixed|string
  43 + */
  44 + public function getOpenid()
  45 + {
  46 + $openid = Session::get('openid');
  47 + if (!$openid) {
  48 + if (!isset($_GET['code'])) {
  49 + $url = $this->getAuthorizeUrl();
  50 +
  51 + Header("Location: $url");
  52 + exit();
  53 + } else {
  54 + $state = Session::get('state');
  55 + if ($state == $_GET['state']) {
  56 + $code = $_GET['code'];
  57 + $token = $this->getAccessToken($code);
  58 + if (!isset($token['openid']) && isset($token['errmsg'])) {
  59 + exception($token['errmsg']);
  60 + }
  61 + $openid = isset($token['openid']) ? $token['openid'] : '';
  62 + if ($openid) {
  63 + Session::set("openid", $openid);
  64 + }
  65 + }
  66 + }
  67 + }
  68 + return $openid;
  69 + }
  70 +
  71 + /**
  72 + * 获取授权token网页授权
  73 + *
  74 + * @param string $code
  75 + * @return mixed|string
  76 + */
  77 + public function getAccessToken($code = '')
  78 + {
  79 + $params = [
  80 + 'appid' => $this->app_id,
  81 + 'secret' => $this->app_secret,
  82 + 'code' => $code,
  83 + 'grant_type' => 'authorization_code'
  84 + ];
  85 + $ret = Http::sendRequest('https://api.weixin.qq.com/sns/oauth2/access_token', $params, 'GET');
  86 + if ($ret['ret']) {
  87 + $ar = json_decode($ret['msg'], true);
  88 + return $ar;
  89 + }
  90 + return [];
  91 + }
  92 +
  93 + public function getJsticket($code = '')
  94 + {
  95 + $jsticket = Session::get('jsticket');
  96 + if (!$jsticket) {
  97 + $token = $this->getAccessToken($code);
  98 + $params = [
  99 + 'access_token' => 'token',
  100 + 'type' => 'jsapi',
  101 + ];
  102 + $ret = Http::sendRequest('https://api.weixin.qq.com/cgi-bin/ticket/getticket', $params, 'GET');
  103 + if ($ret['ret']) {
  104 + $ar = json_decode($ret['msg'], true);
  105 + return $ar;
  106 + }
  107 + }
  108 + return $jsticket;
  109 + }
  110 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Contracts;
  4 +
  5 +use Symfony\Component\HttpFoundation\Response;
  6 +use Yansongda\Supports\Collection;
  7 +
  8 +interface GatewayApplicationInterface
  9 +{
  10 + /**
  11 + * To pay.
  12 + *
  13 + * @author yansongda <me@yansonga.cn>
  14 + *
  15 + * @param string $gateway
  16 + * @param array $params
  17 + *
  18 + * @return Collection|Response
  19 + */
  20 + public function pay($gateway, $params);
  21 +
  22 + /**
  23 + * Query an order.
  24 + *
  25 + * @author yansongda <me@yansongda.cn>
  26 + *
  27 + * @param string|array $order
  28 + *
  29 + * @return Collection
  30 + */
  31 + public function find($order, string $type);
  32 +
  33 + /**
  34 + * Refund an order.
  35 + *
  36 + * @author yansongda <me@yansongda.cn>
  37 + *
  38 + * @return Collection
  39 + */
  40 + public function refund(array $order);
  41 +
  42 + /**
  43 + * Cancel an order.
  44 + *
  45 + * @author yansongda <me@yansongda.cn>
  46 + *
  47 + * @param string|array $order
  48 + *
  49 + * @return Collection
  50 + */
  51 + public function cancel($order);
  52 +
  53 + /**
  54 + * Close an order.
  55 + *
  56 + * @author yansongda <me@yansongda.cn>
  57 + *
  58 + * @param string|array $order
  59 + *
  60 + * @return Collection
  61 + */
  62 + public function close($order);
  63 +
  64 + /**
  65 + * Verify a request.
  66 + *
  67 + * @author yansongda <me@yansongda.cn>
  68 + *
  69 + * @param string|array|null $content
  70 + *
  71 + * @return Collection
  72 + */
  73 + public function verify($content, bool $refund);
  74 +
  75 + /**
  76 + * Echo success to server.
  77 + *
  78 + * @author yansongda <me@yansongda.cn>
  79 + *
  80 + * @return Response
  81 + */
  82 + public function success();
  83 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Contracts;
  4 +
  5 +use Symfony\Component\HttpFoundation\Response;
  6 +use Yansongda\Supports\Collection;
  7 +
  8 +interface GatewayInterface
  9 +{
  10 + /**
  11 + * Pay an order.
  12 + *
  13 + * @author yansongda <me@yansongda.cn>
  14 + *
  15 + * @param string $endpoint
  16 + *
  17 + * @return Collection|Response
  18 + */
  19 + public function pay($endpoint, array $payload);
  20 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay;
  4 +
  5 +use Exception;
  6 +use Symfony\Component\EventDispatcher\EventDispatcher;
  7 +use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  8 +use Symfony\Contracts\EventDispatcher\Event;
  9 +
  10 +/**
  11 + * @author yansongda <me@yansongda.cn>
  12 + *
  13 + * @method static Event dispatch(Event $event) Dispatches an event to all registered listeners
  14 + * @method static array getListeners($eventName = null) Gets the listeners of a specific event or all listeners sorted by descending priority.
  15 + * @method static int|void getListenerPriority($eventName, $listener) Gets the listener priority for a specific event.
  16 + * @method static bool hasListeners($eventName = null) Checks whether an event has any registered listeners.
  17 + * @method static void addListener($eventName, $listener, $priority = 0) Adds an event listener that listens on the specified events.
  18 + * @method static removeListener($eventName, $listener) Removes an event listener from the specified events.
  19 + * @method static void addSubscriber(EventSubscriberInterface $subscriber) Adds an event subscriber.
  20 + * @method static void removeSubscriber(EventSubscriberInterface $subscriber)
  21 + */
  22 +class Events
  23 +{
  24 + /**
  25 + * dispatcher.
  26 + *
  27 + * @var EventDispatcher
  28 + */
  29 + protected static $dispatcher;
  30 +
  31 + /**
  32 + * Forward call.
  33 + *
  34 + * @author yansongda <me@yansongda.cn>
  35 + *
  36 + * @param string $method
  37 + * @param array $args
  38 + *
  39 + * @throws Exception
  40 + *
  41 + * @return mixed
  42 + */
  43 + public static function __callStatic($method, $args)
  44 + {
  45 + return call_user_func_array([self::getDispatcher(), $method], $args);
  46 + }
  47 +
  48 + /**
  49 + * Forward call.
  50 + *
  51 + * @author yansongda <me@yansongda.cn>
  52 + *
  53 + * @param string $method
  54 + * @param array $args
  55 + *
  56 + * @throws Exception
  57 + *
  58 + * @return mixed
  59 + */
  60 + public function __call($method, $args)
  61 + {
  62 + return call_user_func_array([self::getDispatcher(), $method], $args);
  63 + }
  64 +
  65 + /**
  66 + * setDispatcher.
  67 + *
  68 + * @author yansongda <me@yansongda.cn>
  69 + */
  70 + public static function setDispatcher(EventDispatcher $dispatcher)
  71 + {
  72 + self::$dispatcher = $dispatcher;
  73 + }
  74 +
  75 + /**
  76 + * getDispatcher.
  77 + *
  78 + * @author yansongda <me@yansongda.cn>
  79 + */
  80 + public static function getDispatcher(): EventDispatcher
  81 + {
  82 + if (self::$dispatcher) {
  83 + return self::$dispatcher;
  84 + }
  85 +
  86 + return self::$dispatcher = self::createDispatcher();
  87 + }
  88 +
  89 + /**
  90 + * createDispatcher.
  91 + *
  92 + * @author yansongda <me@yansongda.cn>
  93 + */
  94 + public static function createDispatcher(): EventDispatcher
  95 + {
  96 + return new EventDispatcher();
  97 + }
  98 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Events;
  4 +
  5 +class ApiRequested extends Event
  6 +{
  7 + /**
  8 + * Endpoint.
  9 + *
  10 + * @var string
  11 + */
  12 + public $endpoint;
  13 +
  14 + /**
  15 + * Result.
  16 + *
  17 + * @var array
  18 + */
  19 + public $result;
  20 +
  21 + /**
  22 + * Bootstrap.
  23 + */
  24 + public function __construct(string $driver, string $gateway, string $endpoint, array $result)
  25 + {
  26 + $this->endpoint = $endpoint;
  27 + $this->result = $result;
  28 +
  29 + parent::__construct($driver, $gateway);
  30 + }
  31 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Events;
  4 +
  5 +class ApiRequesting extends Event
  6 +{
  7 + /**
  8 + * Endpoint.
  9 + *
  10 + * @var string
  11 + */
  12 + public $endpoint;
  13 +
  14 + /**
  15 + * Payload.
  16 + *
  17 + * @var array
  18 + */
  19 + public $payload;
  20 +
  21 + /**
  22 + * Bootstrap.
  23 + */
  24 + public function __construct(string $driver, string $gateway, string $endpoint, array $payload)
  25 + {
  26 + $this->endpoint = $endpoint;
  27 + $this->payload = $payload;
  28 +
  29 + parent::__construct($driver, $gateway);
  30 + }
  31 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Events;
  4 +
  5 +use Symfony\Contracts\EventDispatcher\Event as SymfonyEvent;
  6 +
  7 +class Event extends SymfonyEvent
  8 +{
  9 + /**
  10 + * Driver.
  11 + *
  12 + * @var string
  13 + */
  14 + public $driver;
  15 +
  16 + /**
  17 + * Method.
  18 + *
  19 + * @var string
  20 + */
  21 + public $gateway;
  22 +
  23 + /**
  24 + * Extra attributes.
  25 + *
  26 + * @var mixed
  27 + */
  28 + public $attributes;
  29 +
  30 + /**
  31 + * Bootstrap.
  32 + *
  33 + * @author yansongda <me@yansongda.cn>
  34 + */
  35 + public function __construct(string $driver, string $gateway)
  36 + {
  37 + $this->driver = $driver;
  38 + $this->gateway = $gateway;
  39 + }
  40 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Events;
  4 +
  5 +class MethodCalled extends Event
  6 +{
  7 + /**
  8 + * endpoint.
  9 + *
  10 + * @var string
  11 + */
  12 + public $endpoint;
  13 +
  14 + /**
  15 + * payload.
  16 + *
  17 + * @var array
  18 + */
  19 + public $payload;
  20 +
  21 + /**
  22 + * Bootstrap.
  23 + *
  24 + * @author yansongda <me@yansongda.cn>
  25 + */
  26 + public function __construct(string $driver, string $gateway, string $endpoint, array $payload = [])
  27 + {
  28 + $this->endpoint = $endpoint;
  29 + $this->payload = $payload;
  30 +
  31 + parent::__construct($driver, $gateway);
  32 + }
  33 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Events;
  4 +
  5 +class PayStarted extends Event
  6 +{
  7 + /**
  8 + * Endpoint.
  9 + *
  10 + * @var string
  11 + */
  12 + public $endpoint;
  13 +
  14 + /**
  15 + * Payload.
  16 + *
  17 + * @var array
  18 + */
  19 + public $payload;
  20 +
  21 + /**
  22 + * Bootstrap.
  23 + */
  24 + public function __construct(string $driver, string $gateway, string $endpoint, array $payload)
  25 + {
  26 + $this->endpoint = $endpoint;
  27 + $this->payload = $payload;
  28 +
  29 + parent::__construct($driver, $gateway);
  30 + }
  31 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Events;
  4 +
  5 +class PayStarting extends Event
  6 +{
  7 + /**
  8 + * Params.
  9 + *
  10 + * @var array
  11 + */
  12 + public $params;
  13 +
  14 + /**
  15 + * Bootstrap.
  16 + */
  17 + public function __construct(string $driver, string $gateway, array $params)
  18 + {
  19 + $this->params = $params;
  20 +
  21 + parent::__construct($driver, $gateway);
  22 + }
  23 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Events;
  4 +
  5 +class RequestReceived extends Event
  6 +{
  7 + /**
  8 + * Received data.
  9 + *
  10 + * @var array
  11 + */
  12 + public $data;
  13 +
  14 + /**
  15 + * Bootstrap.
  16 + *
  17 + * @author yansongda <me@yansongda.cn>
  18 + */
  19 + public function __construct(string $driver, string $gateway, array $data)
  20 + {
  21 + $this->data = $data;
  22 +
  23 + parent::__construct($driver, $gateway);
  24 + }
  25 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Events;
  4 +
  5 +class SignFailed extends Event
  6 +{
  7 + /**
  8 + * Received data.
  9 + *
  10 + * @var array
  11 + */
  12 + public $data;
  13 +
  14 + /**
  15 + * Bootstrap.
  16 + *
  17 + * @author yansongda <me@yansongda.cn>
  18 + */
  19 + public function __construct(string $driver, string $gateway, array $data)
  20 + {
  21 + $this->data = $data;
  22 +
  23 + parent::__construct($driver, $gateway);
  24 + }
  25 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Exceptions;
  4 +
  5 +class BusinessException extends GatewayException
  6 +{
  7 + /**
  8 + * Bootstrap.
  9 + *
  10 + * @author yansongda <me@yansonga.cn>
  11 + *
  12 + * @param string $message
  13 + * @param array|string $raw
  14 + */
  15 + public function __construct($message, $raw = [])
  16 + {
  17 + parent::__construct('ERROR_BUSINESS: '.$message, $raw, self::ERROR_BUSINESS);
  18 + }
  19 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Exceptions;
  4 +
  5 +class Exception extends \Exception
  6 +{
  7 + const UNKNOWN_ERROR = 9999;
  8 +
  9 + const INVALID_GATEWAY = 1;
  10 +
  11 + const INVALID_CONFIG = 2;
  12 +
  13 + const INVALID_ARGUMENT = 3;
  14 +
  15 + const ERROR_GATEWAY = 4;
  16 +
  17 + const INVALID_SIGN = 5;
  18 +
  19 + const ERROR_BUSINESS = 6;
  20 +
  21 + /**
  22 + * Raw error info.
  23 + *
  24 + * @var array
  25 + */
  26 + public $raw;
  27 +
  28 + /**
  29 + * Bootstrap.
  30 + *
  31 + * @author yansongda <me@yansonga.cn>
  32 + *
  33 + * @param string $message
  34 + * @param array|string $raw
  35 + * @param int|string $code
  36 + */
  37 + public function __construct($message = '', $raw = [], $code = self::UNKNOWN_ERROR)
  38 + {
  39 + $message = '' === $message ? 'Unknown Error' : $message;
  40 + $this->raw = is_array($raw) ? $raw : [$raw];
  41 +
  42 + parent::__construct($message, intval($code));
  43 + }
  44 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Exceptions;
  4 +
  5 +class GatewayException extends Exception
  6 +{
  7 + /**
  8 + * Bootstrap.
  9 + *
  10 + * @author yansongda <me@yansonga.cn>
  11 + *
  12 + * @param string $message
  13 + * @param array|string $raw
  14 + * @param int $code
  15 + */
  16 + public function __construct($message, $raw = [], $code = self::ERROR_GATEWAY)
  17 + {
  18 + parent::__construct('ERROR_GATEWAY: '.$message, $raw, $code);
  19 + }
  20 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Exceptions;
  4 +
  5 +class InvalidArgumentException extends Exception
  6 +{
  7 + /**
  8 + * Bootstrap.
  9 + *
  10 + * @author yansongda <me@yansonga.cn>
  11 + *
  12 + * @param string $message
  13 + * @param array|string $raw
  14 + */
  15 + public function __construct($message, $raw = [])
  16 + {
  17 + parent::__construct('INVALID_ARGUMENT: '.$message, $raw, self::INVALID_ARGUMENT);
  18 + }
  19 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Exceptions;
  4 +
  5 +class InvalidConfigException extends Exception
  6 +{
  7 + /**
  8 + * Bootstrap.
  9 + *
  10 + * @author yansongda <me@yansonga.cn>
  11 + *
  12 + * @param string $message
  13 + * @param array|string $raw
  14 + */
  15 + public function __construct($message, $raw = [])
  16 + {
  17 + parent::__construct('INVALID_CONFIG: '.$message, $raw, self::INVALID_CONFIG);
  18 + }
  19 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Exceptions;
  4 +
  5 +class InvalidGatewayException extends Exception
  6 +{
  7 + /**
  8 + * Bootstrap.
  9 + *
  10 + * @author yansongda <me@yansonga.cn>
  11 + *
  12 + * @param string $message
  13 + * @param array|string $raw
  14 + */
  15 + public function __construct($message, $raw = [])
  16 + {
  17 + parent::__construct('INVALID_GATEWAY: '.$message, $raw, self::INVALID_GATEWAY);
  18 + }
  19 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Exceptions;
  4 +
  5 +class InvalidSignException extends Exception
  6 +{
  7 + /**
  8 + * Bootstrap.
  9 + *
  10 + * @author yansongda <me@yansonga.cn>
  11 + *
  12 + * @param string $message
  13 + * @param array|string $raw
  14 + */
  15 + public function __construct($message, $raw = [])
  16 + {
  17 + parent::__construct('INVALID_SIGN: '.$message, $raw, self::INVALID_SIGN);
  18 + }
  19 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways;
  4 +
  5 +use Symfony\Component\HttpFoundation\Request;
  6 +use Symfony\Component\HttpFoundation\Response;
  7 +use Yansongda\Pay\Contracts\GatewayApplicationInterface;
  8 +use Yansongda\Pay\Contracts\GatewayInterface;
  9 +use Yansongda\Pay\Events;
  10 +use Yansongda\Pay\Exceptions\GatewayException;
  11 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  12 +use Yansongda\Pay\Exceptions\InvalidConfigException;
  13 +use Yansongda\Pay\Exceptions\InvalidGatewayException;
  14 +use Yansongda\Pay\Exceptions\InvalidSignException;
  15 +use Yansongda\Pay\Gateways\Alipay\Support;
  16 +use Yansongda\Supports\Collection;
  17 +use Yansongda\Supports\Config;
  18 +use Yansongda\Supports\Str;
  19 +
  20 +/**
  21 + * @method Response app(array $config) APP 支付
  22 + * @method Collection pos(array $config) 刷卡支付
  23 + * @method Collection scan(array $config) 扫码支付
  24 + * @method Collection transfer(array $config) 帐户转账
  25 + * @method Response wap(array $config) 手机网站支付
  26 + * @method Response web(array $config) 电脑支付
  27 + * @method Collection mini(array $config) 小程序支付
  28 + */
  29 +class Alipay implements GatewayApplicationInterface
  30 +{
  31 + /**
  32 + * Const mode_normal.
  33 + */
  34 + const MODE_NORMAL = 'normal';
  35 +
  36 + /**
  37 + * Const mode_dev.
  38 + */
  39 + const MODE_DEV = 'dev';
  40 +
  41 + /**
  42 + * Const mode_service.
  43 + */
  44 + const MODE_SERVICE = 'service';
  45 +
  46 + /**
  47 + * Const url.
  48 + */
  49 + const URL = [
  50 + self::MODE_NORMAL => 'https://openapi.alipay.com/gateway.do?charset=utf-8',
  51 + self::MODE_DEV => 'https://openapi.alipaydev.com/gateway.do?charset=utf-8',
  52 + ];
  53 +
  54 + /**
  55 + * Alipay payload.
  56 + *
  57 + * @var array
  58 + */
  59 + protected $payload;
  60 +
  61 + /**
  62 + * Alipay gateway.
  63 + *
  64 + * @var string
  65 + */
  66 + protected $gateway;
  67 +
  68 + /**
  69 + * extends.
  70 + *
  71 + * @var array
  72 + */
  73 + protected $extends;
  74 +
  75 + /**
  76 + * Bootstrap.
  77 + *
  78 + * @author yansongda <me@yansongda.cn>
  79 + *
  80 + * @throws \Exception
  81 + */
  82 + public function __construct(Config $config)
  83 + {
  84 + $this->gateway = Support::create($config)->getBaseUri();
  85 + $this->payload = [
  86 + 'app_id' => $config->get('app_id'),
  87 + 'method' => '',
  88 + 'format' => 'JSON',
  89 + 'charset' => 'utf-8',
  90 + 'sign_type' => 'RSA2',
  91 + 'version' => '1.0',
  92 + 'return_url' => $config->get('return_url'),
  93 + 'notify_url' => $config->get('notify_url'),
  94 + 'timestamp' => date('Y-m-d H:i:s'),
  95 + 'sign' => '',
  96 + 'biz_content' => '',
  97 + 'app_auth_token' => $config->get('app_auth_token'),
  98 + ];
  99 +
  100 + if ($config->get('app_cert_public_key') && $config->get('alipay_root_cert')) {
  101 + $this->payload['app_cert_sn'] = Support::getCertSN($config->get('app_cert_public_key'));
  102 + $this->payload['alipay_root_cert_sn'] = Support::getRootCertSN($config->get('alipay_root_cert'));
  103 + }
  104 + }
  105 +
  106 + /**
  107 + * Magic pay.
  108 + *
  109 + * @author yansongda <me@yansongda.cn>
  110 + *
  111 + * @param string $method
  112 + * @param array $params
  113 + *
  114 + * @throws GatewayException
  115 + * @throws InvalidArgumentException
  116 + * @throws InvalidConfigException
  117 + * @throws InvalidGatewayException
  118 + * @throws InvalidSignException
  119 + *
  120 + * @return Response|Collection
  121 + */
  122 + public function __call($method, $params)
  123 + {
  124 + if (isset($this->extends[$method])) {
  125 + return $this->makeExtend($method, ...$params);
  126 + }
  127 +
  128 + return $this->pay($method, ...$params);
  129 + }
  130 +
  131 + /**
  132 + * Pay an order.
  133 + *
  134 + * @author yansongda <me@yansongda.cn>
  135 + *
  136 + * @param string $gateway
  137 + * @param array $params
  138 + *
  139 + * @throws InvalidGatewayException
  140 + *
  141 + * @return Response|Collection
  142 + */
  143 + public function pay($gateway, $params = [])
  144 + {
  145 + Events::dispatch(new Events\PayStarting('Alipay', $gateway, $params));
  146 +
  147 + $this->payload['return_url'] = $params['return_url'] ?? $this->payload['return_url'];
  148 + $this->payload['notify_url'] = $params['notify_url'] ?? $this->payload['notify_url'];
  149 +
  150 + unset($params['return_url'], $params['notify_url']);
  151 +
  152 + $this->payload['biz_content'] = json_encode($params);
  153 +
  154 + $gateway = get_class($this).'\\'.Str::studly($gateway).'Gateway';
  155 +
  156 + if (class_exists($gateway)) {
  157 + return $this->makePay($gateway);
  158 + }
  159 +
  160 + throw new InvalidGatewayException("Pay Gateway [{$gateway}] not exists");
  161 + }
  162 +
  163 + /**
  164 + * Verify sign.
  165 + *
  166 + * @author yansongda <me@yansongda.cn>
  167 + *
  168 + * @param array|null $data
  169 + *
  170 + * @throws InvalidSignException
  171 + * @throws InvalidConfigException
  172 + */
  173 + public function verify($data = null, bool $refund = false): Collection
  174 + {
  175 + if (is_null($data)) {
  176 + $request = Request::createFromGlobals();
  177 +
  178 + $data = $request->request->count() > 0 ? $request->request->all() : $request->query->all();
  179 + }
  180 +
  181 + if (isset($data['fund_bill_list'])) {
  182 + $data['fund_bill_list'] = htmlspecialchars_decode($data['fund_bill_list']);
  183 + }
  184 +
  185 + Events::dispatch(new Events\RequestReceived('Alipay', '', $data));
  186 +
  187 + if (Support::verifySign($data)) {
  188 + return new Collection($data);
  189 + }
  190 +
  191 + Events::dispatch(new Events\SignFailed('Alipay', '', $data));
  192 +
  193 + throw new InvalidSignException('Alipay Sign Verify FAILED', $data);
  194 + }
  195 +
  196 + /**
  197 + * Query an order.
  198 + *
  199 + * @author yansongda <me@yansongda.cn>
  200 + *
  201 + * @param string|array $order
  202 + *
  203 + * @throws GatewayException
  204 + * @throws InvalidConfigException
  205 + * @throws InvalidSignException
  206 + */
  207 + public function find($order, string $type = 'wap'): Collection
  208 + {
  209 + $gateway = get_class($this).'\\'.Str::studly($type).'Gateway';
  210 +
  211 + if (!class_exists($gateway) || !is_callable([new $gateway(), 'find'])) {
  212 + throw new GatewayException("{$gateway} Done Not Exist Or Done Not Has FIND Method");
  213 + }
  214 +
  215 + $config = call_user_func([new $gateway(), 'find'], $order);
  216 +
  217 + $this->payload['method'] = $config['method'];
  218 + $this->payload['biz_content'] = $config['biz_content'];
  219 + $this->payload['sign'] = Support::generateSign($this->payload);
  220 +
  221 + Events::dispatch(new Events\MethodCalled('Alipay', 'Find', $this->gateway, $this->payload));
  222 +
  223 + return Support::requestApi($this->payload);
  224 + }
  225 +
  226 + /**
  227 + * Refund an order.
  228 + *
  229 + * @author yansongda <me@yansongda.cn>
  230 + *
  231 + * @throws GatewayException
  232 + * @throws InvalidConfigException
  233 + * @throws InvalidSignException
  234 + */
  235 + public function refund(array $order): Collection
  236 + {
  237 + $this->payload['method'] = 'alipay.trade.refund';
  238 + $this->payload['biz_content'] = json_encode($order);
  239 + $this->payload['sign'] = Support::generateSign($this->payload);
  240 +
  241 + Events::dispatch(new Events\MethodCalled('Alipay', 'Refund', $this->gateway, $this->payload));
  242 +
  243 + return Support::requestApi($this->payload);
  244 + }
  245 +
  246 + /**
  247 + * Cancel an order.
  248 + *
  249 + * @author yansongda <me@yansongda.cn>
  250 + *
  251 + * @param array|string $order
  252 + *
  253 + * @throws GatewayException
  254 + * @throws InvalidConfigException
  255 + * @throws InvalidSignException
  256 + */
  257 + public function cancel($order): Collection
  258 + {
  259 + $this->payload['method'] = 'alipay.trade.cancel';
  260 + $this->payload['biz_content'] = json_encode(is_array($order) ? $order : ['out_trade_no' => $order]);
  261 + $this->payload['sign'] = Support::generateSign($this->payload);
  262 +
  263 + Events::dispatch(new Events\MethodCalled('Alipay', 'Cancel', $this->gateway, $this->payload));
  264 +
  265 + return Support::requestApi($this->payload);
  266 + }
  267 +
  268 + /**
  269 + * Close an order.
  270 + *
  271 + * @param string|array $order
  272 + *
  273 + * @author yansongda <me@yansongda.cn>
  274 + *
  275 + * @throws GatewayException
  276 + * @throws InvalidConfigException
  277 + * @throws InvalidSignException
  278 + */
  279 + public function close($order): Collection
  280 + {
  281 + $this->payload['method'] = 'alipay.trade.close';
  282 + $this->payload['biz_content'] = json_encode(is_array($order) ? $order : ['out_trade_no' => $order]);
  283 + $this->payload['sign'] = Support::generateSign($this->payload);
  284 +
  285 + Events::dispatch(new Events\MethodCalled('Alipay', 'Close', $this->gateway, $this->payload));
  286 +
  287 + return Support::requestApi($this->payload);
  288 + }
  289 +
  290 + /**
  291 + * Download bill.
  292 + *
  293 + * @author yansongda <me@yansongda.cn>
  294 + *
  295 + * @param string|array $bill
  296 + *
  297 + * @throws GatewayException
  298 + * @throws InvalidConfigException
  299 + * @throws InvalidSignException
  300 + */
  301 + public function download($bill): string
  302 + {
  303 + $this->payload['method'] = 'alipay.data.dataservice.bill.downloadurl.query';
  304 + $this->payload['biz_content'] = json_encode(is_array($bill) ? $bill : ['bill_type' => 'trade', 'bill_date' => $bill]);
  305 + $this->payload['sign'] = Support::generateSign($this->payload);
  306 +
  307 + Events::dispatch(new Events\MethodCalled('Alipay', 'Download', $this->gateway, $this->payload));
  308 +
  309 + $result = Support::requestApi($this->payload);
  310 +
  311 + return ($result instanceof Collection) ? $result->get('bill_download_url') : '';
  312 + }
  313 +
  314 + /**
  315 + * Reply success to alipay.
  316 + *
  317 + * @author yansongda <me@yansongda.cn>
  318 + */
  319 + public function success(): Response
  320 + {
  321 + Events::dispatch(new Events\MethodCalled('Alipay', 'Success', $this->gateway));
  322 +
  323 + return new Response('success');
  324 + }
  325 +
  326 + /**
  327 + * extend.
  328 + *
  329 + * @author yansongda <me@yansongda.cn>
  330 + *
  331 + * @throws GatewayException
  332 + * @throws InvalidConfigException
  333 + * @throws InvalidSignException
  334 + * @throws InvalidArgumentException
  335 + */
  336 + public function extend(string $method, callable $function, bool $now = true): ?Collection
  337 + {
  338 + if (!$now && !method_exists($this, $method)) {
  339 + $this->extends[$method] = $function;
  340 +
  341 + return null;
  342 + }
  343 +
  344 + $customize = $function($this->payload);
  345 +
  346 + if (!is_array($customize) && !($customize instanceof Collection)) {
  347 + throw new InvalidArgumentException('Return Type Must Be Array Or Collection');
  348 + }
  349 +
  350 + Events::dispatch(new Events\MethodCalled('Alipay', 'extend', $this->gateway, $customize));
  351 +
  352 + if (is_array($customize)) {
  353 + $this->payload = $customize;
  354 + $this->payload['sign'] = Support::generateSign($this->payload);
  355 +
  356 + return Support::requestApi($this->payload);
  357 + }
  358 +
  359 + return $customize;
  360 + }
  361 +
  362 + /**
  363 + * Make pay gateway.
  364 + *
  365 + * @author yansongda <me@yansongda.cn>
  366 + *
  367 + * @throws InvalidGatewayException
  368 + *
  369 + * @return Response|Collection
  370 + */
  371 + protected function makePay(string $gateway)
  372 + {
  373 + $app = new $gateway();
  374 +
  375 + if ($app instanceof GatewayInterface) {
  376 + return $app->pay($this->gateway, array_filter($this->payload, function ($value) {
  377 + return '' !== $value && !is_null($value);
  378 + }));
  379 + }
  380 +
  381 + throw new InvalidGatewayException("Pay Gateway [{$gateway}] Must Be An Instance Of GatewayInterface");
  382 + }
  383 +
  384 + /**
  385 + * makeExtend.
  386 + *
  387 + * @author yansongda <me@yansongda.cn>
  388 + *
  389 + * @throws GatewayException
  390 + * @throws InvalidArgumentException
  391 + * @throws InvalidConfigException
  392 + * @throws InvalidSignException
  393 + */
  394 + protected function makeExtend(string $method, array ...$params): Collection
  395 + {
  396 + $params = count($params) >= 1 ? $params[0] : $params;
  397 +
  398 + $function = $this->extends[$method];
  399 +
  400 + $customize = $function($this->payload, $params);
  401 +
  402 + if (!is_array($customize) && !($customize instanceof Collection)) {
  403 + throw new InvalidArgumentException('Return Type Must Be Array Or Collection');
  404 + }
  405 +
  406 + Events::dispatch(new Events\MethodCalled(
  407 + 'Alipay',
  408 + 'extend - '.$method,
  409 + $this->gateway,
  410 + is_array($customize) ? $customize : $customize->toArray()
  411 + ));
  412 +
  413 + if (is_array($customize)) {
  414 + $this->payload = $customize;
  415 + $this->payload['sign'] = Support::generateSign($this->payload);
  416 +
  417 + return Support::requestApi($this->payload);
  418 + }
  419 +
  420 + return $customize;
  421 + }
  422 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Alipay;
  4 +
  5 +use Symfony\Component\HttpFoundation\Response;
  6 +use Yansongda\Pay\Events;
  7 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  8 +use Yansongda\Pay\Exceptions\InvalidConfigException;
  9 +use Yansongda\Pay\Gateways\Alipay;
  10 +
  11 +class AppGateway extends Gateway
  12 +{
  13 + /**
  14 + * Pay an order.
  15 + *
  16 + * @author yansongda <me@yansongda.cn>
  17 + *
  18 + * @param string $endpoint
  19 + *
  20 + * @throws InvalidConfigException
  21 + * @throws InvalidArgumentException
  22 + */
  23 + public function pay($endpoint, array $payload): Response
  24 + {
  25 + $payload['method'] = 'alipay.trade.app.pay';
  26 +
  27 + $biz_array = json_decode($payload['biz_content'], true);
  28 + if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) {
  29 + $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];
  30 + }
  31 + $payload['biz_content'] = json_encode(array_merge($biz_array, ['product_code' => 'QUICK_MSECURITY_PAY']));
  32 + $payload['sign'] = Support::generateSign($payload);
  33 +
  34 + Events::dispatch(new Events\PayStarted('Alipay', 'App', $endpoint, $payload));
  35 +
  36 + return new Response(http_build_query($payload));
  37 + }
  38 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Alipay;
  4 +
  5 +use Yansongda\Pay\Contracts\GatewayInterface;
  6 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  7 +use Yansongda\Supports\Collection;
  8 +
  9 +abstract class Gateway implements GatewayInterface
  10 +{
  11 + /**
  12 + * Mode.
  13 + *
  14 + * @var string
  15 + */
  16 + protected $mode;
  17 +
  18 + /**
  19 + * Bootstrap.
  20 + *
  21 + * @author yansongda <me@yansongda.cn>
  22 + *
  23 + * @throws InvalidArgumentException
  24 + */
  25 + public function __construct()
  26 + {
  27 + $this->mode = Support::getInstance()->mode;
  28 + }
  29 +
  30 + /**
  31 + * Pay an order.
  32 + *
  33 + * @author yansongda <me@yansongda.cn>
  34 + *
  35 + * @param string $endpoint
  36 + *
  37 + * @return Collection
  38 + */
  39 + abstract public function pay($endpoint, array $payload);
  40 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Alipay;
  4 +
  5 +use Yansongda\Pay\Events;
  6 +use Yansongda\Pay\Exceptions\GatewayException;
  7 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  8 +use Yansongda\Pay\Exceptions\InvalidConfigException;
  9 +use Yansongda\Pay\Exceptions\InvalidSignException;
  10 +use Yansongda\Pay\Gateways\Alipay;
  11 +use Yansongda\Supports\Collection;
  12 +
  13 +class MiniGateway extends Gateway
  14 +{
  15 + /**
  16 + * Pay an order.
  17 + *
  18 + * @author xiaozan <i@xiaozan.me>
  19 + *
  20 + * @param string $endpoint
  21 + *
  22 + * @throws GatewayException
  23 + * @throws InvalidArgumentException
  24 + * @throws InvalidConfigException
  25 + * @throws InvalidSignException
  26 + *
  27 + * @see https://docs.alipay.com/mini/introduce/pay
  28 + */
  29 + public function pay($endpoint, array $payload): Collection
  30 + {
  31 + $biz_array = json_decode($payload['biz_content'], true);
  32 + if (empty($biz_array['buyer_id'])) {
  33 + throw new InvalidArgumentException('buyer_id required');
  34 + }
  35 + if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) {
  36 + $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];
  37 + }
  38 + $payload['biz_content'] = json_encode($biz_array);
  39 + $payload['method'] = 'alipay.trade.create';
  40 + $payload['sign'] = Support::generateSign($payload);
  41 +
  42 + Events::dispatch(new Events\PayStarted('Alipay', 'Mini', $endpoint, $payload));
  43 +
  44 + return Support::requestApi($payload);
  45 + }
  46 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Alipay;
  4 +
  5 +use Yansongda\Pay\Events;
  6 +use Yansongda\Pay\Exceptions\GatewayException;
  7 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  8 +use Yansongda\Pay\Exceptions\InvalidConfigException;
  9 +use Yansongda\Pay\Exceptions\InvalidSignException;
  10 +use Yansongda\Pay\Gateways\Alipay;
  11 +use Yansongda\Supports\Collection;
  12 +
  13 +class PosGateway extends Gateway
  14 +{
  15 + /**
  16 + * Pay an order.
  17 + *
  18 + * @author yansongda <me@yansongda.cn>
  19 + *
  20 + * @param string $endpoint
  21 + *
  22 + * @throws InvalidArgumentException
  23 + * @throws GatewayException
  24 + * @throws InvalidConfigException
  25 + * @throws InvalidSignException
  26 + */
  27 + public function pay($endpoint, array $payload): Collection
  28 + {
  29 + $payload['method'] = 'alipay.trade.pay';
  30 + $biz_array = json_decode($payload['biz_content'], true);
  31 + if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) {
  32 + $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];
  33 + }
  34 + $payload['biz_content'] = json_encode(array_merge(
  35 + $biz_array,
  36 + [
  37 + 'product_code' => 'FACE_TO_FACE_PAYMENT',
  38 + 'scene' => 'bar_code',
  39 + ]
  40 + ));
  41 + $payload['sign'] = Support::generateSign($payload);
  42 +
  43 + Events::dispatch(new Events\PayStarted('Alipay', 'Pos', $endpoint, $payload));
  44 +
  45 + return Support::requestApi($payload);
  46 + }
  47 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Alipay;
  4 +
  5 +class RefundGateway
  6 +{
  7 + /**
  8 + * Find.
  9 + *
  10 + * @author yansongda <me@yansongda.cn>
  11 + *
  12 + * @param $order
  13 + */
  14 + public function find($order): array
  15 + {
  16 + return [
  17 + 'method' => 'alipay.trade.fastpay.refund.query',
  18 + 'biz_content' => json_encode(is_array($order) ? $order : ['out_trade_no' => $order]),
  19 + ];
  20 + }
  21 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Alipay;
  4 +
  5 +use Yansongda\Pay\Events;
  6 +use Yansongda\Pay\Exceptions\GatewayException;
  7 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  8 +use Yansongda\Pay\Exceptions\InvalidConfigException;
  9 +use Yansongda\Pay\Exceptions\InvalidSignException;
  10 +use Yansongda\Pay\Gateways\Alipay;
  11 +use Yansongda\Supports\Collection;
  12 +
  13 +class ScanGateway extends Gateway
  14 +{
  15 + /**
  16 + * Pay an order.
  17 + *
  18 + * @author yansongda <me@yansongda.cn>
  19 + *
  20 + * @param string $endpoint
  21 + *
  22 + * @throws GatewayException
  23 + * @throws InvalidArgumentException
  24 + * @throws InvalidConfigException
  25 + * @throws InvalidSignException
  26 + */
  27 + public function pay($endpoint, array $payload): Collection
  28 + {
  29 + $payload['method'] = 'alipay.trade.precreate';
  30 + $biz_array = json_decode($payload['biz_content'], true);
  31 + if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) {
  32 + $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];
  33 + }
  34 + $payload['biz_content'] = json_encode(array_merge($biz_array, ['product_code' => '']));
  35 + $payload['sign'] = Support::generateSign($payload);
  36 +
  37 + Events::dispatch(new Events\PayStarted('Alipay', 'Scan', $endpoint, $payload));
  38 +
  39 + return Support::requestApi($payload);
  40 + }
  41 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Alipay;
  4 +
  5 +use Exception;
  6 +use Yansongda\Pay\Events;
  7 +use Yansongda\Pay\Exceptions\GatewayException;
  8 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  9 +use Yansongda\Pay\Exceptions\InvalidConfigException;
  10 +use Yansongda\Pay\Exceptions\InvalidSignException;
  11 +use Yansongda\Pay\Gateways\Alipay;
  12 +use Yansongda\Pay\Log;
  13 +use Yansongda\Supports\Arr;
  14 +use Yansongda\Supports\Collection;
  15 +use Yansongda\Supports\Config;
  16 +use Yansongda\Supports\Str;
  17 +use Yansongda\Supports\Traits\HasHttpRequest;
  18 +
  19 +/**
  20 + * @author yansongda <me@yansongda.cn>
  21 + *
  22 + * @property string app_id alipay app_id
  23 + * @property string ali_public_key
  24 + * @property string private_key
  25 + * @property array http http options
  26 + * @property string mode current mode
  27 + * @property array log log options
  28 + * @property string pid ali pid
  29 + */
  30 +class Support
  31 +{
  32 + use HasHttpRequest;
  33 +
  34 + /**
  35 + * Alipay gateway.
  36 + *
  37 + * @var string
  38 + */
  39 + protected $baseUri;
  40 +
  41 + /**
  42 + * Config.
  43 + *
  44 + * @var Config
  45 + */
  46 + protected $config;
  47 +
  48 + /**
  49 + * Instance.
  50 + *
  51 + * @var Support
  52 + */
  53 + private static $instance;
  54 +
  55 + /**
  56 + * Bootstrap.
  57 + *
  58 + * @author yansongda <me@yansongda.cn>
  59 + */
  60 + private function __construct(Config $config)
  61 + {
  62 + $this->baseUri = Alipay::URL[$config->get('mode', Alipay::MODE_NORMAL)];
  63 + $this->config = $config;
  64 +
  65 + $this->setHttpOptions();
  66 + }
  67 +
  68 + /**
  69 + * __get.
  70 + *
  71 + * @author yansongda <me@yansongda.cn>
  72 + *
  73 + * @param $key
  74 + *
  75 + * @return mixed|Config|null
  76 + */
  77 + public function __get($key)
  78 + {
  79 + return $this->getConfig($key);
  80 + }
  81 +
  82 + /**
  83 + * create.
  84 + *
  85 + * @author yansongda <me@yansongda.cn>
  86 + *
  87 + * @return Support
  88 + */
  89 + public static function create(Config $config)
  90 + {
  91 + if ('cli' === php_sapi_name() || !(self::$instance instanceof self)) {
  92 + self::$instance = new self($config);
  93 + }
  94 +
  95 + return self::$instance;
  96 + }
  97 +
  98 + /**
  99 + * getInstance.
  100 + *
  101 + * @author yansongda <me@yansongda.cn>
  102 + *
  103 + * @throws InvalidArgumentException
  104 + *
  105 + * @return Support
  106 + */
  107 + public static function getInstance()
  108 + {
  109 + if (is_null(self::$instance)) {
  110 + throw new InvalidArgumentException('You Should [Create] First Before Using');
  111 + }
  112 +
  113 + return self::$instance;
  114 + }
  115 +
  116 + /**
  117 + * clear.
  118 + *
  119 + * @author yansongda <me@yansongda.cn>
  120 + */
  121 + public function clear()
  122 + {
  123 + self::$instance = null;
  124 + }
  125 +
  126 + /**
  127 + * Get Alipay API result.
  128 + *
  129 + * @author yansongda <me@yansongda.cn>
  130 + *
  131 + * @throws GatewayException
  132 + * @throws InvalidConfigException
  133 + * @throws InvalidSignException
  134 + */
  135 + public static function requestApi(array $data): Collection
  136 + {
  137 + Events::dispatch(new Events\ApiRequesting('Alipay', '', self::$instance->getBaseUri(), $data));
  138 +
  139 + $data = array_filter($data, function ($value) {
  140 + return ('' == $value || is_null($value)) ? false : true;
  141 + });
  142 +
  143 + $result = json_decode(self::$instance->post('', $data), true);
  144 +
  145 + Events::dispatch(new Events\ApiRequested('Alipay', '', self::$instance->getBaseUri(), $result));
  146 +
  147 + return self::processingApiResult($data, $result);
  148 + }
  149 +
  150 + /**
  151 + * Generate sign.
  152 + *
  153 + * @author yansongda <me@yansongda.cn>
  154 + *
  155 + * @throws InvalidConfigException
  156 + */
  157 + public static function generateSign(array $params): string
  158 + {
  159 + $privateKey = self::$instance->private_key;
  160 +
  161 + if (is_null($privateKey)) {
  162 + throw new InvalidConfigException('Missing Alipay Config -- [private_key]');
  163 + }
  164 +
  165 + if (Str::endsWith($privateKey, '.pem')) {
  166 + $privateKey = openssl_pkey_get_private(
  167 + Str::startsWith($privateKey, 'file://') ? $privateKey : 'file://'.$privateKey
  168 + );
  169 + } else {
  170 + $privateKey = "-----BEGIN RSA PRIVATE KEY-----\n".
  171 + wordwrap($privateKey, 64, "\n", true).
  172 + "\n-----END RSA PRIVATE KEY-----";
  173 + }
  174 +
  175 + openssl_sign(self::getSignContent($params), $sign, $privateKey, OPENSSL_ALGO_SHA256);
  176 +
  177 + $sign = base64_encode($sign);
  178 +
  179 + Log::debug('Alipay Generate Sign', [$params, $sign]);
  180 +
  181 + if (is_resource($privateKey)) {
  182 + openssl_free_key($privateKey);
  183 + }
  184 +
  185 + return $sign;
  186 + }
  187 +
  188 + /**
  189 + * Verify sign.
  190 + *
  191 + * @author yansongda <me@yansonga.cn>
  192 + *
  193 + * @param bool $sync
  194 + * @param string|null $sign
  195 + *
  196 + * @throws InvalidConfigException
  197 + */
  198 + public static function verifySign(array $data, $sync = false, $sign = null): bool
  199 + {
  200 + $publicKey = self::$instance->ali_public_key;
  201 +
  202 + if (is_null($publicKey)) {
  203 + throw new InvalidConfigException('Missing Alipay Config -- [ali_public_key]');
  204 + }
  205 +
  206 + if (Str::endsWith($publicKey, '.crt')) {
  207 + $publicKey = file_get_contents($publicKey);
  208 + } elseif (Str::endsWith($publicKey, '.pem')) {
  209 + $publicKey = openssl_pkey_get_public(
  210 + Str::startsWith($publicKey, 'file://') ? $publicKey : 'file://'.$publicKey
  211 + );
  212 + } else {
  213 + $publicKey = "-----BEGIN PUBLIC KEY-----\n".
  214 + wordwrap($publicKey, 64, "\n", true).
  215 + "\n-----END PUBLIC KEY-----";
  216 + }
  217 +
  218 + $sign = $sign ?? $data['sign'];
  219 +
  220 + $toVerify = $sync ? json_encode($data, JSON_UNESCAPED_UNICODE) : self::getSignContent($data, true);
  221 +
  222 + $isVerify = 1 === openssl_verify($toVerify, base64_decode($sign), $publicKey, OPENSSL_ALGO_SHA256);
  223 +
  224 + if (is_resource($publicKey)) {
  225 + openssl_free_key($publicKey);
  226 + }
  227 +
  228 + return $isVerify;
  229 + }
  230 +
  231 + /**
  232 + * Get signContent that is to be signed.
  233 + *
  234 + * @author yansongda <me@yansongda.cn>
  235 + *
  236 + * @param bool $verify
  237 + */
  238 + public static function getSignContent(array $data, $verify = false): string
  239 + {
  240 + ksort($data);
  241 +
  242 + $stringToBeSigned = '';
  243 + foreach ($data as $k => $v) {
  244 + if ($verify && 'sign' != $k && 'sign_type' != $k) {
  245 + $stringToBeSigned .= $k.'='.$v.'&';
  246 + }
  247 + if (!$verify && '' !== $v && !is_null($v) && 'sign' != $k && '@' != substr($v, 0, 1)) {
  248 + $stringToBeSigned .= $k.'='.$v.'&';
  249 + }
  250 + }
  251 +
  252 + Log::debug('Alipay Generate Sign Content Before Trim', [$data, $stringToBeSigned]);
  253 +
  254 + return trim($stringToBeSigned, '&');
  255 + }
  256 +
  257 + /**
  258 + * Convert encoding.
  259 + *
  260 + * @author yansongda <me@yansonga.cn>
  261 + *
  262 + * @param string|array $data
  263 + * @param string $to
  264 + * @param string $from
  265 + */
  266 + public static function encoding($data, $to = 'utf-8', $from = 'gb2312'): array
  267 + {
  268 + return Arr::encoding((array) $data, $to, $from);
  269 + }
  270 +
  271 + /**
  272 + * Get service config.
  273 + *
  274 + * @author yansongda <me@yansongda.cn>
  275 + *
  276 + * @param string|null $key
  277 + * @param mixed|null $default
  278 + *
  279 + * @return mixed|null
  280 + */
  281 + public function getConfig($key = null, $default = null)
  282 + {
  283 + if (is_null($key)) {
  284 + return $this->config->all();
  285 + }
  286 +
  287 + if ($this->config->has($key)) {
  288 + return $this->config[$key];
  289 + }
  290 +
  291 + return $default;
  292 + }
  293 +
  294 + /**
  295 + * Get Base Uri.
  296 + *
  297 + * @author yansongda <me@yansongda.cn>
  298 + *
  299 + * @return string
  300 + */
  301 + public function getBaseUri()
  302 + {
  303 + return $this->baseUri;
  304 + }
  305 +
  306 + /**
  307 + * 生成应用证书SN.
  308 + *
  309 + * @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html
  310 + *
  311 + * @param $certPath
  312 + *
  313 + * @throws /Exception
  314 + */
  315 + public static function getCertSN($certPath): string
  316 + {
  317 + if (!is_file($certPath)) {
  318 + throw new Exception('unknown certPath -- [getCertSN]');
  319 + }
  320 + $x509data = file_get_contents($certPath);
  321 + if (false === $x509data) {
  322 + throw new Exception('Alipay CertSN Error -- [getCertSN]');
  323 + }
  324 + openssl_x509_read($x509data);
  325 + $certdata = openssl_x509_parse($x509data);
  326 + if (empty($certdata)) {
  327 + throw new Exception('Alipay openssl_x509_parse Error -- [getCertSN]');
  328 + }
  329 + $issuer_arr = [];
  330 + foreach ($certdata['issuer'] as $key => $val) {
  331 + $issuer_arr[] = $key.'='.$val;
  332 + }
  333 + $issuer = implode(',', array_reverse($issuer_arr));
  334 + Log::debug('getCertSN:', [$certPath, $issuer, $certdata['serialNumber']]);
  335 +
  336 + return md5($issuer.$certdata['serialNumber']);
  337 + }
  338 +
  339 + /**
  340 + * 生成支付宝根证书SN.
  341 + *
  342 + * @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html
  343 + *
  344 + * @param $certPath
  345 + *
  346 + * @return string
  347 + *
  348 + * @throws /Exception
  349 + */
  350 + public static function getRootCertSN($certPath)
  351 + {
  352 + if (!is_file($certPath)) {
  353 + throw new Exception('unknown certPath -- [getRootCertSN]');
  354 + }
  355 + $x509data = file_get_contents($certPath);
  356 + if (false === $x509data) {
  357 + throw new Exception('Alipay CertSN Error -- [getRootCertSN]');
  358 + }
  359 + $kCertificateEnd = '-----END CERTIFICATE-----';
  360 + $certStrList = explode($kCertificateEnd, $x509data);
  361 + $md5_arr = [];
  362 + foreach ($certStrList as $one) {
  363 + if (!empty(trim($one))) {
  364 + $_x509data = $one.$kCertificateEnd;
  365 + openssl_x509_read($_x509data);
  366 + $_certdata = openssl_x509_parse($_x509data);
  367 + if (in_array($_certdata['signatureTypeSN'], ['RSA-SHA256', 'RSA-SHA1'])) {
  368 + $issuer_arr = [];
  369 + foreach ($_certdata['issuer'] as $key => $val) {
  370 + $issuer_arr[] = $key.'='.$val;
  371 + }
  372 + $_issuer = implode(',', array_reverse($issuer_arr));
  373 + if (0 === strpos($_certdata['serialNumber'], '0x')) {
  374 + $serialNumber = self::bchexdec($_certdata['serialNumber']);
  375 + } else {
  376 + $serialNumber = $_certdata['serialNumber'];
  377 + }
  378 + $md5_arr[] = md5($_issuer.$serialNumber);
  379 + Log::debug('getRootCertSN Sub:', [$certPath, $_issuer, $serialNumber]);
  380 + }
  381 + }
  382 + }
  383 +
  384 + return implode('_', $md5_arr);
  385 + }
  386 +
  387 + /**
  388 + * processingApiResult.
  389 + *
  390 + * @author yansongda <me@yansongda.cn>
  391 + *
  392 + * @param $data
  393 + * @param $result
  394 + *
  395 + * @throws GatewayException
  396 + * @throws InvalidConfigException
  397 + * @throws InvalidSignException
  398 + */
  399 + protected static function processingApiResult($data, $result): Collection
  400 + {
  401 + $method = str_replace('.', '_', $data['method']).'_response';
  402 +
  403 + if (!isset($result['sign']) || '10000' != $result[$method]['code']) {
  404 + throw new GatewayException('Get Alipay API Error:'.$result[$method]['msg'].(isset($result[$method]['sub_code']) ? (' - '.$result[$method]['sub_code']) : ''), $result);
  405 + }
  406 +
  407 + if (self::verifySign($result[$method], true, $result['sign'])) {
  408 + return new Collection($result[$method]);
  409 + }
  410 +
  411 + Events::dispatch(new Events\SignFailed('Alipay', '', $result));
  412 +
  413 + throw new InvalidSignException('Alipay Sign Verify FAILED', $result);
  414 + }
  415 +
  416 + /**
  417 + * Set Http options.
  418 + *
  419 + * @author yansongda <me@yansongda.cn>
  420 + */
  421 + protected function setHttpOptions(): self
  422 + {
  423 + if ($this->config->has('http') && is_array($this->config->get('http'))) {
  424 + $this->config->forget('http.base_uri');
  425 + $this->httpOptions = $this->config->get('http');
  426 + }
  427 +
  428 + return $this;
  429 + }
  430 +
  431 + /**
  432 + * 0x转高精度数字.
  433 + *
  434 + * @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html
  435 + *
  436 + * @param $hex
  437 + *
  438 + * @return int|string
  439 + */
  440 + private static function bchexdec($hex)
  441 + {
  442 + $dec = 0;
  443 + $len = strlen($hex);
  444 + for ($i = 1; $i <= $len; ++$i) {
  445 + if (ctype_xdigit($hex[$i - 1])) {
  446 + $dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
  447 + }
  448 + }
  449 +
  450 + return str_replace('.00', '', $dec);
  451 + }
  452 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Alipay;
  4 +
  5 +use Yansongda\Pay\Contracts\GatewayInterface;
  6 +use Yansongda\Pay\Events;
  7 +use Yansongda\Pay\Exceptions\GatewayException;
  8 +use Yansongda\Pay\Exceptions\InvalidConfigException;
  9 +use Yansongda\Pay\Exceptions\InvalidSignException;
  10 +use Yansongda\Supports\Collection;
  11 +
  12 +class TransferGateway implements GatewayInterface
  13 +{
  14 + /**
  15 + * Pay an order.
  16 + *
  17 + * @author yansongda <me@yansongda.cn>
  18 + *
  19 + * @param string $endpoint
  20 + *
  21 + * @throws GatewayException
  22 + * @throws InvalidConfigException
  23 + * @throws InvalidSignException
  24 + */
  25 + public function pay($endpoint, array $payload): Collection
  26 + {
  27 + $payload['method'] = 'alipay.fund.trans.uni.transfer';
  28 + $payload['sign'] = Support::generateSign($payload);
  29 +
  30 + Events::dispatch(new Events\PayStarted('Alipay', 'Transfer', $endpoint, $payload));
  31 +
  32 + return Support::requestApi($payload);
  33 + }
  34 +
  35 + /**
  36 + * Find.
  37 + *
  38 + * @author yansongda <me@yansongda.cn>
  39 + *
  40 + * @param $order
  41 + */
  42 + public function find($order): array
  43 + {
  44 + return [
  45 + 'method' => 'alipay.fund.trans.order.query',
  46 + 'biz_content' => json_encode(is_array($order) ? $order : ['out_biz_no' => $order]),
  47 + ];
  48 + }
  49 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Alipay;
  4 +
  5 +class WapGateway extends WebGateway
  6 +{
  7 + /**
  8 + * Get method config.
  9 + *
  10 + * @author yansongda <me@yansongda.cn>
  11 + */
  12 + protected function getMethod(): string
  13 + {
  14 + return 'alipay.trade.wap.pay';
  15 + }
  16 +
  17 + /**
  18 + * Get productCode config.
  19 + *
  20 + * @author yansongda <me@yansongda.cn>
  21 + */
  22 + protected function getProductCode(): string
  23 + {
  24 + return 'QUICK_WAP_WAY';
  25 + }
  26 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Alipay;
  4 +
  5 +use Symfony\Component\HttpFoundation\RedirectResponse;
  6 +use Symfony\Component\HttpFoundation\Response;
  7 +use Yansongda\Pay\Events;
  8 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  9 +use Yansongda\Pay\Exceptions\InvalidConfigException;
  10 +use Yansongda\Pay\Gateways\Alipay;
  11 +
  12 +class WebGateway extends Gateway
  13 +{
  14 + /**
  15 + * Pay an order.
  16 + *
  17 + * @author yansongda <me@yansongda.cn>
  18 + *
  19 + * @param string $endpoint
  20 + *
  21 + * @throws InvalidConfigException
  22 + * @throws InvalidArgumentException
  23 + */
  24 + public function pay($endpoint, array $payload): Response
  25 + {
  26 + $biz_array = json_decode($payload['biz_content'], true);
  27 + $biz_array['product_code'] = $this->getProductCode();
  28 +
  29 + $method = $biz_array['http_method'] ?? 'POST';
  30 +
  31 + unset($biz_array['http_method']);
  32 + if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) {
  33 + $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];
  34 + }
  35 + $payload['method'] = $this->getMethod();
  36 + $payload['biz_content'] = json_encode($biz_array);
  37 + $payload['sign'] = Support::generateSign($payload);
  38 +
  39 + Events::dispatch(new Events\PayStarted('Alipay', 'Web/Wap', $endpoint, $payload));
  40 +
  41 + return $this->buildPayHtml($endpoint, $payload, $method);
  42 + }
  43 +
  44 + /**
  45 + * Find.
  46 + *
  47 + * @author yansongda <me@yansongda.cn>
  48 + *
  49 + * @param $order
  50 + */
  51 + public function find($order): array
  52 + {
  53 + return [
  54 + 'method' => 'alipay.trade.query',
  55 + 'biz_content' => json_encode(is_array($order) ? $order : ['out_trade_no' => $order]),
  56 + ];
  57 + }
  58 +
  59 + /**
  60 + * Build Html response.
  61 + *
  62 + * @author yansongda <me@yansongda.cn>
  63 + *
  64 + * @param string $endpoint
  65 + * @param array $payload
  66 + * @param string $method
  67 + */
  68 + protected function buildPayHtml($endpoint, $payload, $method = 'POST'): Response
  69 + {
  70 + if ('GET' === strtoupper($method)) {
  71 + return new RedirectResponse($endpoint.'&'.http_build_query($payload));
  72 + }
  73 +
  74 + $sHtml = "<form id='alipay_submit' name='alipay_submit' action='".$endpoint."' method='".$method."'>";
  75 + foreach ($payload as $key => $val) {
  76 + $val = str_replace("'", '&apos;', $val);
  77 + $sHtml .= "<input type='hidden' name='".$key."' value='".$val."'/>";
  78 + }
  79 + $sHtml .= "<input type='submit' value='ok' style='display:none;'></form>";
  80 + $sHtml .= "<script>document.forms['alipay_submit'].submit();</script>";
  81 +
  82 + return new Response($sHtml);
  83 + }
  84 +
  85 + /**
  86 + * Get method config.
  87 + *
  88 + * @author yansongda <me@yansongda.cn>
  89 + */
  90 + protected function getMethod(): string
  91 + {
  92 + return 'alipay.trade.page.pay';
  93 + }
  94 +
  95 + /**
  96 + * Get productCode config.
  97 + *
  98 + * @author yansongda <me@yansongda.cn>
  99 + */
  100 + protected function getProductCode(): string
  101 + {
  102 + return 'FAST_INSTANT_TRADE_PAY';
  103 + }
  104 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways;
  4 +
  5 +use Exception;
  6 +use Symfony\Component\HttpFoundation\RedirectResponse;
  7 +use Symfony\Component\HttpFoundation\Request;
  8 +use Symfony\Component\HttpFoundation\Response;
  9 +use Yansongda\Pay\Contracts\GatewayApplicationInterface;
  10 +use Yansongda\Pay\Contracts\GatewayInterface;
  11 +use Yansongda\Pay\Events;
  12 +use Yansongda\Pay\Exceptions\GatewayException;
  13 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  14 +use Yansongda\Pay\Exceptions\InvalidGatewayException;
  15 +use Yansongda\Pay\Exceptions\InvalidSignException;
  16 +use Yansongda\Pay\Gateways\Wechat\Support;
  17 +use Yansongda\Pay\Log;
  18 +use Yansongda\Supports\Collection;
  19 +use Yansongda\Supports\Config;
  20 +use Yansongda\Supports\Str;
  21 +
  22 +/**
  23 + * @method Response app(array $config) APP 支付
  24 + * @method Collection groupRedpack(array $config) 分裂红包
  25 + * @method Collection miniapp(array $config) 小程序支付
  26 + * @method Collection mp(array $config) 公众号支付
  27 + * @method Collection pos(array $config) 刷卡支付
  28 + * @method Collection redpack(array $config) 普通红包
  29 + * @method Collection scan(array $config) 扫码支付
  30 + * @method Collection transfer(array $config) 企业付款
  31 + * @method RedirectResponse web(array $config) Web 扫码支付
  32 + * @method RedirectResponse wap(array $config) H5 支付
  33 + */
  34 +class Wechat implements GatewayApplicationInterface
  35 +{
  36 + /**
  37 + * 普通模式.
  38 + */
  39 + const MODE_NORMAL = 'normal';
  40 +
  41 + /**
  42 + * 沙箱模式.
  43 + */
  44 + const MODE_DEV = 'dev';
  45 +
  46 + /**
  47 + * 香港钱包 API.
  48 + */
  49 + const MODE_HK = 'hk';
  50 +
  51 + /**
  52 + * 境外 API.
  53 + */
  54 + const MODE_US = 'us';
  55 +
  56 + /**
  57 + * 服务商模式.
  58 + */
  59 + const MODE_SERVICE = 'service';
  60 +
  61 + /**
  62 + * Const url.
  63 + */
  64 + const URL = [
  65 + self::MODE_NORMAL => 'https://api.mch.weixin.qq.com/',
  66 + self::MODE_DEV => 'https://api.mch.weixin.qq.com/xdc/apiv2sandbox/',
  67 + self::MODE_HK => 'https://apihk.mch.weixin.qq.com/',
  68 + self::MODE_SERVICE => 'https://api.mch.weixin.qq.com/',
  69 + self::MODE_US => 'https://apius.mch.weixin.qq.com/',
  70 + ];
  71 +
  72 + /**
  73 + * Wechat payload.
  74 + *
  75 + * @var array
  76 + */
  77 + protected $payload;
  78 +
  79 + /**
  80 + * Wechat gateway.
  81 + *
  82 + * @var string
  83 + */
  84 + protected $gateway;
  85 +
  86 + /**
  87 + * Bootstrap.
  88 + *
  89 + * @author yansongda <me@yansongda.cn>
  90 + *
  91 + * @throws Exception
  92 + */
  93 + public function __construct(Config $config)
  94 + {
  95 + $this->gateway = Support::create($config)->getBaseUri();
  96 + $this->payload = [
  97 + 'appid' => $config->get('app_id', ''),
  98 + 'mch_id' => $config->get('mch_id', ''),
  99 + 'nonce_str' => Str::random(),
  100 + 'notify_url' => $config->get('notify_url', ''),
  101 + 'sign' => '',
  102 + 'trade_type' => '',
  103 + 'spbill_create_ip' => Request::createFromGlobals()->getClientIp(),
  104 + ];
  105 +
  106 + if ($config->get('mode', self::MODE_NORMAL) === static::MODE_SERVICE) {
  107 + $this->payload = array_merge($this->payload, [
  108 + 'sub_mch_id' => $config->get('sub_mch_id'),
  109 + 'sub_appid' => $config->get('sub_app_id', ''),
  110 + ]);
  111 + }
  112 + }
  113 +
  114 + /**
  115 + * Magic pay.
  116 + *
  117 + * @author yansongda <me@yansongda.cn>
  118 + *
  119 + * @param string $method
  120 + * @param string $params
  121 + *
  122 + * @throws InvalidGatewayException
  123 + *
  124 + * @return Response|Collection
  125 + */
  126 + public function __call($method, $params)
  127 + {
  128 + return self::pay($method, ...$params);
  129 + }
  130 +
  131 + /**
  132 + * Pay an order.
  133 + *
  134 + * @author yansongda <me@yansongda.cn>
  135 + *
  136 + * @param string $gateway
  137 + * @param array $params
  138 + *
  139 + * @throws InvalidGatewayException
  140 + *
  141 + * @return Response|Collection
  142 + */
  143 + public function pay($gateway, $params = [])
  144 + {
  145 + Events::dispatch(new Events\PayStarting('Wechat', $gateway, $params));
  146 +
  147 + $this->payload = array_merge($this->payload, $params);
  148 +
  149 + $gateway = get_class($this).'\\'.Str::studly($gateway).'Gateway';
  150 +
  151 + if (class_exists($gateway)) {
  152 + return $this->makePay($gateway);
  153 + }
  154 +
  155 + throw new InvalidGatewayException("Pay Gateway [{$gateway}] Not Exists");
  156 + }
  157 +
  158 + /**
  159 + * Verify data.
  160 + *
  161 + * @author yansongda <me@yansongda.cn>
  162 + *
  163 + * @param string|null $content
  164 + *
  165 + * @throws InvalidSignException
  166 + * @throws InvalidArgumentException
  167 + */
  168 + public function verify($content = null, bool $refund = false): Collection
  169 + {
  170 + $content = $content ?? Request::createFromGlobals()->getContent();
  171 +
  172 + Events::dispatch(new Events\RequestReceived('Wechat', '', [$content]));
  173 +
  174 + $data = Support::fromXml($content);
  175 + if ($refund) {
  176 + $decrypt_data = Support::decryptRefundContents($data['req_info']);
  177 + $data = array_merge(Support::fromXml($decrypt_data), $data);
  178 + }
  179 +
  180 + Log::debug('Resolved The Received Wechat Request Data', $data);
  181 +
  182 + if ($refund || Support::generateSign($data) === $data['sign']) {
  183 + return new Collection($data);
  184 + }
  185 +
  186 + Events::dispatch(new Events\SignFailed('Wechat', '', $data));
  187 +
  188 + throw new InvalidSignException('Wechat Sign Verify FAILED', $data);
  189 + }
  190 +
  191 + /**
  192 + * Query an order.
  193 + *
  194 + * @author yansongda <me@yansongda.cn>
  195 + *
  196 + * @param string|array $order
  197 + *
  198 + * @throws GatewayException
  199 + * @throws InvalidSignException
  200 + * @throws InvalidArgumentException
  201 + */
  202 + public function find($order, string $type = 'wap'): Collection
  203 + {
  204 + if ('wap' != $type) {
  205 + unset($this->payload['spbill_create_ip']);
  206 + }
  207 +
  208 + $gateway = get_class($this).'\\'.Str::studly($type).'Gateway';
  209 +
  210 + if (!class_exists($gateway) || !is_callable([new $gateway(), 'find'])) {
  211 + throw new GatewayException("{$gateway} Done Not Exist Or Done Not Has FIND Method");
  212 + }
  213 +
  214 + $config = call_user_func([new $gateway(), 'find'], $order);
  215 +
  216 + $this->payload = Support::filterPayload($this->payload, $config['order']);
  217 +
  218 + Events::dispatch(new Events\MethodCalled('Wechat', 'Find', $this->gateway, $this->payload));
  219 +
  220 + return Support::requestApi(
  221 + $config['endpoint'],
  222 + $this->payload,
  223 + $config['cert']
  224 + );
  225 + }
  226 +
  227 + /**
  228 + * Refund an order.
  229 + *
  230 + * @author yansongda <me@yansongda.cn>
  231 + *
  232 + * @throws GatewayException
  233 + * @throws InvalidSignException
  234 + * @throws InvalidArgumentException
  235 + */
  236 + public function refund(array $order): Collection
  237 + {
  238 + $this->payload = Support::filterPayload($this->payload, $order, true);
  239 +
  240 + Events::dispatch(new Events\MethodCalled('Wechat', 'Refund', $this->gateway, $this->payload));
  241 +
  242 + return Support::requestApi(
  243 + 'secapi/pay/refund',
  244 + $this->payload,
  245 + true
  246 + );
  247 + }
  248 +
  249 + /**
  250 + * Cancel an order.
  251 + *
  252 + * @author yansongda <me@yansongda.cn>
  253 + *
  254 + * @param array $order
  255 + *
  256 + * @throws GatewayException
  257 + * @throws InvalidSignException
  258 + * @throws InvalidArgumentException
  259 + */
  260 + public function cancel($order): Collection
  261 + {
  262 + unset($this->payload['spbill_create_ip']);
  263 +
  264 + $this->payload = Support::filterPayload($this->payload, $order);
  265 +
  266 + Events::dispatch(new Events\MethodCalled('Wechat', 'Cancel', $this->gateway, $this->payload));
  267 +
  268 + return Support::requestApi(
  269 + 'secapi/pay/reverse',
  270 + $this->payload,
  271 + true
  272 + );
  273 + }
  274 +
  275 + /**
  276 + * Close an order.
  277 + *
  278 + * @author yansongda <me@yansongda.cn>
  279 + *
  280 + * @param string|array $order
  281 + *
  282 + * @throws GatewayException
  283 + * @throws InvalidSignException
  284 + * @throws InvalidArgumentException
  285 + */
  286 + public function close($order): Collection
  287 + {
  288 + unset($this->payload['spbill_create_ip']);
  289 +
  290 + $this->payload = Support::filterPayload($this->payload, $order);
  291 +
  292 + Events::dispatch(new Events\MethodCalled('Wechat', 'Close', $this->gateway, $this->payload));
  293 +
  294 + return Support::requestApi('pay/closeorder', $this->payload);
  295 + }
  296 +
  297 + /**
  298 + * Echo success to server.
  299 + *
  300 + * @author yansongda <me@yansongda.cn>
  301 + *
  302 + * @throws InvalidArgumentException
  303 + */
  304 + public function success(): Response
  305 + {
  306 + Events::dispatch(new Events\MethodCalled('Wechat', 'Success', $this->gateway));
  307 +
  308 + return new Response(
  309 + Support::toXml(['return_code' => 'SUCCESS', 'return_msg' => 'OK']),
  310 + 200,
  311 + ['Content-Type' => 'application/xml']
  312 + );
  313 + }
  314 +
  315 + /**
  316 + * Download the bill.
  317 + *
  318 + * @author yansongda <me@yansongda.cn>
  319 + *
  320 + * @throws GatewayException
  321 + * @throws InvalidArgumentException
  322 + */
  323 + public function download(array $params): string
  324 + {
  325 + unset($this->payload['spbill_create_ip']);
  326 +
  327 + $this->payload = Support::filterPayload($this->payload, $params, true);
  328 +
  329 + Events::dispatch(new Events\MethodCalled('Wechat', 'Download', $this->gateway, $this->payload));
  330 +
  331 + $result = Support::getInstance()->post(
  332 + 'pay/downloadbill',
  333 + Support::getInstance()->toXml($this->payload)
  334 + );
  335 +
  336 + if (is_array($result)) {
  337 + throw new GatewayException('Get Wechat API Error: '.$result['return_msg'], $result);
  338 + }
  339 +
  340 + return $result;
  341 + }
  342 +
  343 + /**
  344 + * Make pay gateway.
  345 + *
  346 + * @author yansongda <me@yansongda.cn>
  347 + *
  348 + * @param string $gateway
  349 + *
  350 + * @throws InvalidGatewayException
  351 + *
  352 + * @return Response|Collection
  353 + */
  354 + protected function makePay($gateway)
  355 + {
  356 + $app = new $gateway();
  357 +
  358 + if ($app instanceof GatewayInterface) {
  359 + return $app->pay($this->gateway, array_filter($this->payload, function ($value) {
  360 + return '' !== $value && !is_null($value);
  361 + }));
  362 + }
  363 +
  364 + throw new InvalidGatewayException("Pay Gateway [{$gateway}] Must Be An Instance Of GatewayInterface");
  365 + }
  366 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Wechat;
  4 +
  5 +use Exception;
  6 +use Symfony\Component\HttpFoundation\JsonResponse;
  7 +use Symfony\Component\HttpFoundation\Response;
  8 +use Yansongda\Pay\Events;
  9 +use Yansongda\Pay\Exceptions\GatewayException;
  10 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  11 +use Yansongda\Pay\Exceptions\InvalidSignException;
  12 +use Yansongda\Pay\Gateways\Wechat;
  13 +use Yansongda\Supports\Str;
  14 +
  15 +class AppGateway extends Gateway
  16 +{
  17 + /**
  18 + * Pay an order.
  19 + *
  20 + * @author yansongda <me@yansongda.cn>
  21 + *
  22 + * @param string $endpoint
  23 + *
  24 + * @throws GatewayException
  25 + * @throws InvalidArgumentException
  26 + * @throws InvalidSignException
  27 + * @throws Exception
  28 + */
  29 + public function pay($endpoint, array $payload): Response
  30 + {
  31 + $payload['appid'] = Support::getInstance()->appid;
  32 + $payload['trade_type'] = $this->getTradeType();
  33 +
  34 + if (Wechat::MODE_SERVICE === $this->mode) {
  35 + $payload['sub_appid'] = Support::getInstance()->sub_appid;
  36 + }
  37 +
  38 + $pay_request = [
  39 + 'appid' => Wechat::MODE_SERVICE === $this->mode ? $payload['sub_appid'] : $payload['appid'],
  40 + 'partnerid' => Wechat::MODE_SERVICE === $this->mode ? $payload['sub_mch_id'] : $payload['mch_id'],
  41 + 'prepayid' => $this->preOrder($payload)->get('prepay_id'),
  42 + 'timestamp' => strval(time()),
  43 + 'noncestr' => Str::random(),
  44 + 'package' => 'Sign=WXPay',
  45 + ];
  46 + $pay_request['sign'] = Support::generateSign($pay_request);
  47 +
  48 + Events::dispatch(new Events\PayStarted('Wechat', 'App', $endpoint, $pay_request));
  49 +
  50 + return new JsonResponse($pay_request);
  51 + }
  52 +
  53 + /**
  54 + * Get trade type config.
  55 + *
  56 + * @author yansongda <me@yansongda.cn>
  57 + */
  58 + protected function getTradeType(): string
  59 + {
  60 + return 'APP';
  61 + }
  62 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Wechat;
  4 +
  5 +use Yansongda\Pay\Contracts\GatewayInterface;
  6 +use Yansongda\Pay\Events;
  7 +use Yansongda\Pay\Exceptions\GatewayException;
  8 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  9 +use Yansongda\Pay\Exceptions\InvalidSignException;
  10 +use Yansongda\Supports\Collection;
  11 +
  12 +abstract class Gateway implements GatewayInterface
  13 +{
  14 + /**
  15 + * Mode.
  16 + *
  17 + * @var string
  18 + */
  19 + protected $mode;
  20 +
  21 + /**
  22 + * Bootstrap.
  23 + *
  24 + * @author yansongda <me@yansongda.cn>
  25 + *
  26 + * @throws InvalidArgumentException
  27 + */
  28 + public function __construct()
  29 + {
  30 + $this->mode = Support::getInstance()->mode;
  31 + }
  32 +
  33 + /**
  34 + * Pay an order.
  35 + *
  36 + * @author yansongda <me@yansongda.cn>
  37 + *
  38 + * @param string $endpoint
  39 + *
  40 + * @return Collection
  41 + */
  42 + abstract public function pay($endpoint, array $payload);
  43 +
  44 + /**
  45 + * Find.
  46 + *
  47 + * @author yansongda <me@yansongda.cn>
  48 + *
  49 + * @param string|array $order
  50 + */
  51 + public function find($order): array
  52 + {
  53 + return [
  54 + 'endpoint' => 'pay/orderquery',
  55 + 'order' => is_array($order) ? $order : ['out_trade_no' => $order],
  56 + 'cert' => false,
  57 + ];
  58 + }
  59 +
  60 + /**
  61 + * Get trade type config.
  62 + *
  63 + * @author yansongda <me@yansongda.cn>
  64 + *
  65 + * @return string
  66 + */
  67 + abstract protected function getTradeType();
  68 +
  69 + /**
  70 + * Schedule an order.
  71 + *
  72 + * @author yansongda <me@yansongda.cn>
  73 + *
  74 + * @param array $payload
  75 + *
  76 + * @throws GatewayException
  77 + * @throws InvalidArgumentException
  78 + * @throws InvalidSignException
  79 + */
  80 + protected function preOrder($payload): Collection
  81 + {
  82 + $payload['sign'] = Support::generateSign($payload);
  83 +
  84 + Events::dispatch(new Events\MethodCalled('Wechat', 'PreOrder', '', $payload));
  85 +
  86 + return Support::requestApi('pay/unifiedorder', $payload);
  87 + }
  88 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Wechat;
  4 +
  5 +use Yansongda\Pay\Events;
  6 +use Yansongda\Pay\Exceptions\GatewayException;
  7 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  8 +use Yansongda\Pay\Exceptions\InvalidSignException;
  9 +use Yansongda\Pay\Gateways\Wechat;
  10 +use Yansongda\Supports\Collection;
  11 +
  12 +class GroupRedpackGateway extends Gateway
  13 +{
  14 + /**
  15 + * Pay an order.
  16 + *
  17 + * @author yansongda <me@yansongda.cn>
  18 + *
  19 + * @param string $endpoint
  20 + *
  21 + * @throws GatewayException
  22 + * @throws InvalidArgumentException
  23 + * @throws InvalidSignException
  24 + */
  25 + public function pay($endpoint, array $payload): Collection
  26 + {
  27 + $payload['wxappid'] = $payload['appid'];
  28 + $payload['amt_type'] = 'ALL_RAND';
  29 +
  30 + if (Wechat::MODE_SERVICE === $this->mode) {
  31 + $payload['msgappid'] = $payload['appid'];
  32 + }
  33 +
  34 + unset($payload['appid'], $payload['trade_type'],
  35 + $payload['notify_url'], $payload['spbill_create_ip']);
  36 +
  37 + $payload['sign'] = Support::generateSign($payload);
  38 +
  39 + Events::dispatch(new Events\PayStarted('Wechat', 'Group Redpack', $endpoint, $payload));
  40 +
  41 + return Support::requestApi(
  42 + 'mmpaymkttransfers/sendgroupredpack',
  43 + $payload,
  44 + true
  45 + );
  46 + }
  47 +
  48 + /**
  49 + * Get trade type config.
  50 + *
  51 + * @author yansongda <me@yansongda.cn>
  52 + */
  53 + protected function getTradeType(): string
  54 + {
  55 + return '';
  56 + }
  57 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Wechat;
  4 +
  5 +use Yansongda\Pay\Exceptions\GatewayException;
  6 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  7 +use Yansongda\Pay\Exceptions\InvalidSignException;
  8 +use Yansongda\Pay\Gateways\Wechat;
  9 +use Yansongda\Supports\Collection;
  10 +
  11 +class MiniappGateway extends MpGateway
  12 +{
  13 + /**
  14 + * Pay an order.
  15 + *
  16 + * @author yansongda <me@yansongda.cn>
  17 + *
  18 + * @param string $endpoint
  19 + *
  20 + * @throws GatewayException
  21 + * @throws InvalidArgumentException
  22 + * @throws InvalidSignException
  23 + */
  24 + public function pay($endpoint, array $payload): Collection
  25 + {
  26 + $payload['appid'] = Support::getInstance()->miniapp_id;
  27 +
  28 + if (Wechat::MODE_SERVICE === $this->mode) {
  29 + $payload['sub_appid'] = Support::getInstance()->sub_miniapp_id;
  30 + $this->payRequestUseSubAppId = true;
  31 + }
  32 +
  33 + return parent::pay($endpoint, $payload);
  34 + }
  35 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Wechat;
  4 +
  5 +use Exception;
  6 +use Yansongda\Pay\Events;
  7 +use Yansongda\Pay\Exceptions\GatewayException;
  8 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  9 +use Yansongda\Pay\Exceptions\InvalidSignException;
  10 +use Yansongda\Supports\Collection;
  11 +use Yansongda\Supports\Str;
  12 +
  13 +class MpGateway extends Gateway
  14 +{
  15 + /**
  16 + * @var bool
  17 + */
  18 + protected $payRequestUseSubAppId = false;
  19 +
  20 + /**
  21 + * Pay an order.
  22 + *
  23 + * @author yansongda <me@yansongda.cn>
  24 + *
  25 + * @param string $endpoint
  26 + *
  27 + * @throws GatewayException
  28 + * @throws InvalidArgumentException
  29 + * @throws InvalidSignException
  30 + * @throws Exception
  31 + */
  32 + public function pay($endpoint, array $payload): Collection
  33 + {
  34 + $payload['trade_type'] = $this->getTradeType();
  35 +
  36 + $pay_request = [
  37 + 'appId' => !$this->payRequestUseSubAppId ? $payload['appid'] : $payload['sub_appid'],
  38 + 'timeStamp' => strval(time()),
  39 + 'nonceStr' => Str::random(),
  40 + 'package' => 'prepay_id='.$this->preOrder($payload)->get('prepay_id'),
  41 + 'signType' => 'MD5',
  42 + ];
  43 + $pay_request['paySign'] = Support::generateSign($pay_request);
  44 +
  45 + Events::dispatch(new Events\PayStarted('Wechat', 'JSAPI', $endpoint, $pay_request));
  46 +
  47 + return new Collection($pay_request);
  48 + }
  49 +
  50 + /**
  51 + * Get trade type config.
  52 + *
  53 + * @author yansongda <me@yansongda.cn>
  54 + */
  55 + protected function getTradeType(): string
  56 + {
  57 + return 'JSAPI';
  58 + }
  59 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Wechat;
  4 +
  5 +use Yansongda\Pay\Events;
  6 +use Yansongda\Pay\Exceptions\GatewayException;
  7 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  8 +use Yansongda\Pay\Exceptions\InvalidSignException;
  9 +use Yansongda\Supports\Collection;
  10 +
  11 +class PosGateway extends Gateway
  12 +{
  13 + /**
  14 + * Pay an order.
  15 + *
  16 + * @author yansongda <me@yansongda.cn>
  17 + *
  18 + * @param string $endpoint
  19 + *
  20 + * @throws GatewayException
  21 + * @throws InvalidArgumentException
  22 + * @throws InvalidSignException
  23 + */
  24 + public function pay($endpoint, array $payload): Collection
  25 + {
  26 + unset($payload['trade_type'], $payload['notify_url']);
  27 +
  28 + $payload['sign'] = Support::generateSign($payload);
  29 +
  30 + Events::dispatch(new Events\PayStarted('Wechat', 'Pos', $endpoint, $payload));
  31 +
  32 + return Support::requestApi('pay/micropay', $payload);
  33 + }
  34 +
  35 + /**
  36 + * Get trade type config.
  37 + *
  38 + * @author yansongda <me@yansongda.cn>
  39 + */
  40 + protected function getTradeType(): string
  41 + {
  42 + return 'MICROPAY';
  43 + }
  44 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Wechat;
  4 +
  5 +use Symfony\Component\HttpFoundation\Request;
  6 +use Yansongda\Pay\Events;
  7 +use Yansongda\Pay\Exceptions\GatewayException;
  8 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  9 +use Yansongda\Pay\Exceptions\InvalidSignException;
  10 +use Yansongda\Pay\Gateways\Wechat;
  11 +use Yansongda\Supports\Collection;
  12 +
  13 +class RedpackGateway extends Gateway
  14 +{
  15 + /**
  16 + * Pay an order.
  17 + *
  18 + * @author yansongda <me@yansongda.cn>
  19 + *
  20 + * @param string $endpoint
  21 + *
  22 + * @throws GatewayException
  23 + * @throws InvalidArgumentException
  24 + * @throws InvalidSignException
  25 + */
  26 + public function pay($endpoint, array $payload): Collection
  27 + {
  28 + $payload['wxappid'] = $payload['appid'];
  29 +
  30 + if ('cli' !== php_sapi_name()) {
  31 + $payload['client_ip'] = Request::createFromGlobals()->server->get('SERVER_ADDR');
  32 + }
  33 +
  34 + if (Wechat::MODE_SERVICE === $this->mode) {
  35 + $payload['msgappid'] = $payload['appid'];
  36 + }
  37 +
  38 + unset($payload['appid'], $payload['trade_type'],
  39 + $payload['notify_url'], $payload['spbill_create_ip']);
  40 +
  41 + $payload['sign'] = Support::generateSign($payload);
  42 +
  43 + Events::dispatch(new Events\PayStarted('Wechat', 'Redpack', $endpoint, $payload));
  44 +
  45 + return Support::requestApi(
  46 + 'mmpaymkttransfers/sendredpack',
  47 + $payload,
  48 + true
  49 + );
  50 + }
  51 +
  52 + /**
  53 + * Get trade type config.
  54 + *
  55 + * @author yansongda <me@yansongda.cn>
  56 + */
  57 + protected function getTradeType(): string
  58 + {
  59 + return '';
  60 + }
  61 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Wechat;
  4 +
  5 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  6 +
  7 +class RefundGateway extends Gateway
  8 +{
  9 + /**
  10 + * Find.
  11 + *
  12 + * @author yansongda <me@yansongda.cn>
  13 + *
  14 + * @param $order
  15 + */
  16 + public function find($order): array
  17 + {
  18 + return [
  19 + 'endpoint' => 'pay/refundquery',
  20 + 'order' => is_array($order) ? $order : ['out_trade_no' => $order],
  21 + 'cert' => false,
  22 + ];
  23 + }
  24 +
  25 + /**
  26 + * Pay an order.
  27 + *
  28 + * @author yansongda <me@yansongda.cn>
  29 + *
  30 + * @param string $endpoint
  31 + *
  32 + * @throws InvalidArgumentException
  33 + */
  34 + public function pay($endpoint, array $payload)
  35 + {
  36 + throw new InvalidArgumentException('Not Support Refund In Pay');
  37 + }
  38 +
  39 + /**
  40 + * Get trade type config.
  41 + *
  42 + * @author yansongda <me@yansongda.cn>
  43 + *
  44 + * @throws InvalidArgumentException
  45 + */
  46 + protected function getTradeType()
  47 + {
  48 + throw new InvalidArgumentException('Not Support Refund In Pay');
  49 + }
  50 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Wechat;
  4 +
  5 +use Symfony\Component\HttpFoundation\Request;
  6 +use Yansongda\Pay\Events;
  7 +use Yansongda\Pay\Exceptions\GatewayException;
  8 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  9 +use Yansongda\Pay\Exceptions\InvalidSignException;
  10 +use Yansongda\Supports\Collection;
  11 +
  12 +class ScanGateway extends Gateway
  13 +{
  14 + /**
  15 + * Pay an order.
  16 + *
  17 + * @author yansongda <me@yansongda.cn>
  18 + *
  19 + * @param string $endpoint
  20 + *
  21 + * @throws GatewayException
  22 + * @throws InvalidArgumentException
  23 + * @throws InvalidSignException
  24 + */
  25 + public function pay($endpoint, array $payload): Collection
  26 + {
  27 + $payload['spbill_create_ip'] = Request::createFromGlobals()->server->get('SERVER_ADDR');
  28 + $payload['trade_type'] = $this->getTradeType();
  29 +
  30 + Events::dispatch(new Events\PayStarted('Wechat', 'Scan', $endpoint, $payload));
  31 +
  32 + return $this->preOrder($payload);
  33 + }
  34 +
  35 + /**
  36 + * Get trade type config.
  37 + *
  38 + * @author yansongda <me@yansongda.cn>
  39 + */
  40 + protected function getTradeType(): string
  41 + {
  42 + return 'NATIVE';
  43 + }
  44 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Wechat;
  4 +
  5 +use Exception;
  6 +use Yansongda\Pay\Events;
  7 +use Yansongda\Pay\Exceptions\BusinessException;
  8 +use Yansongda\Pay\Exceptions\GatewayException;
  9 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  10 +use Yansongda\Pay\Exceptions\InvalidSignException;
  11 +use Yansongda\Pay\Gateways\Wechat;
  12 +use Yansongda\Pay\Log;
  13 +use Yansongda\Supports\Collection;
  14 +use Yansongda\Supports\Config;
  15 +use Yansongda\Supports\Str;
  16 +use Yansongda\Supports\Traits\HasHttpRequest;
  17 +
  18 +/**
  19 + * @author yansongda <me@yansongda.cn>
  20 + *
  21 + * @property string appid
  22 + * @property string app_id
  23 + * @property string miniapp_id
  24 + * @property string sub_appid
  25 + * @property string sub_app_id
  26 + * @property string sub_miniapp_id
  27 + * @property string mch_id
  28 + * @property string sub_mch_id
  29 + * @property string key
  30 + * @property string return_url
  31 + * @property string cert_client
  32 + * @property string cert_key
  33 + * @property array log
  34 + * @property array http
  35 + * @property string mode
  36 + */
  37 +class Support
  38 +{
  39 + use HasHttpRequest;
  40 +
  41 + /**
  42 + * Wechat gateway.
  43 + *
  44 + * @var string
  45 + */
  46 + protected $baseUri;
  47 +
  48 + /**
  49 + * Config.
  50 + *
  51 + * @var Config
  52 + */
  53 + protected $config;
  54 +
  55 + /**
  56 + * Instance.
  57 + *
  58 + * @var Support
  59 + */
  60 + private static $instance;
  61 +
  62 + /**
  63 + * Bootstrap.
  64 + *
  65 + * @author yansongda <me@yansongda.cn>
  66 + */
  67 + private function __construct(Config $config)
  68 + {
  69 + $this->baseUri = Wechat::URL[$config->get('mode', Wechat::MODE_NORMAL)];
  70 + $this->config = $config;
  71 +
  72 + $this->setHttpOptions();
  73 + }
  74 +
  75 + /**
  76 + * __get.
  77 + *
  78 + * @author yansongda <me@yansongda.cn>
  79 + *
  80 + * @param $key
  81 + *
  82 + * @return mixed|Config|null
  83 + */
  84 + public function __get($key)
  85 + {
  86 + return $this->getConfig($key);
  87 + }
  88 +
  89 + /**
  90 + * create.
  91 + *
  92 + * @author yansongda <me@yansongda.cn>
  93 + *
  94 + * @throws GatewayException
  95 + * @throws InvalidArgumentException
  96 + * @throws InvalidSignException
  97 + *
  98 + * @return Support
  99 + */
  100 + public static function create(Config $config)
  101 + {
  102 + if ('cli' === php_sapi_name() || !(self::$instance instanceof self)) {
  103 + self::$instance = new self($config);
  104 +
  105 + self::setDevKey();
  106 + }
  107 +
  108 + return self::$instance;
  109 + }
  110 +
  111 + /**
  112 + * getInstance.
  113 + *
  114 + * @author yansongda <me@yansongda.cn>
  115 + *
  116 + * @throws InvalidArgumentException
  117 + *
  118 + * @return Support
  119 + */
  120 + public static function getInstance()
  121 + {
  122 + if (is_null(self::$instance)) {
  123 + throw new InvalidArgumentException('You Should [Create] First Before Using');
  124 + }
  125 +
  126 + return self::$instance;
  127 + }
  128 +
  129 + /**
  130 + * clear.
  131 + *
  132 + * @author yansongda <me@yansongda.cn>
  133 + */
  134 + public static function clear()
  135 + {
  136 + self::$instance = null;
  137 + }
  138 +
  139 + /**
  140 + * Request wechat api.
  141 + *
  142 + * @author yansongda <me@yansongda.cn>
  143 + *
  144 + * @param string $endpoint
  145 + * @param array $data
  146 + * @param bool $cert
  147 + *
  148 + * @throws GatewayException
  149 + * @throws InvalidArgumentException
  150 + * @throws InvalidSignException
  151 + */
  152 + public static function requestApi($endpoint, $data, $cert = false): Collection
  153 + {
  154 + Events::dispatch(new Events\ApiRequesting('Wechat', '', self::$instance->getBaseUri().$endpoint, $data));
  155 +
  156 + $result = self::$instance->post(
  157 + $endpoint,
  158 + self::toXml($data),
  159 + $cert ? [
  160 + 'cert' => self::$instance->cert_client,
  161 + 'ssl_key' => self::$instance->cert_key,
  162 + ] : [
  163 + 'headers' => [
  164 + 'Content-Type'=>'application/xml',
  165 + ],
  166 + ]
  167 + );
  168 + $result = is_array($result) ? $result : self::fromXml($result);
  169 +
  170 + Events::dispatch(new Events\ApiRequested('Wechat', '', self::$instance->getBaseUri().$endpoint, $result));
  171 +
  172 + return self::processingApiResult($endpoint, $result);
  173 + }
  174 +
  175 + /**
  176 + * Filter payload.
  177 + *
  178 + * @author yansongda <me@yansongda.cn>
  179 + *
  180 + * @param array $payload
  181 + * @param array|string $params
  182 + * @param bool $preserve_notify_url
  183 + *
  184 + * @throws InvalidArgumentException
  185 + */
  186 + public static function filterPayload($payload, $params, $preserve_notify_url = false): array
  187 + {
  188 + $type = self::getTypeName($params['type'] ?? '');
  189 +
  190 + $payload = array_merge(
  191 + $payload,
  192 + is_array($params) ? $params : ['out_trade_no' => $params]
  193 + );
  194 + $payload['appid'] = self::$instance->getConfig($type, '');
  195 +
  196 + if (Wechat::MODE_SERVICE === self::$instance->getConfig('mode', Wechat::MODE_NORMAL)) {
  197 + $payload['sub_appid'] = self::$instance->getConfig('sub_'.$type, '');
  198 + }
  199 +
  200 + unset($payload['trade_type'], $payload['type']);
  201 + if (!$preserve_notify_url) {
  202 + unset($payload['notify_url']);
  203 + }
  204 +
  205 + $payload['sign'] = self::generateSign($payload);
  206 +
  207 + return $payload;
  208 + }
  209 +
  210 + /**
  211 + * Generate wechat sign.
  212 + *
  213 + * @author yansongda <me@yansongda.cn>
  214 + *
  215 + * @param array $data
  216 + *
  217 + * @throws InvalidArgumentException
  218 + */
  219 + public static function generateSign($data): string
  220 + {
  221 + $key = self::$instance->key;
  222 +
  223 + if (is_null($key)) {
  224 + throw new InvalidArgumentException('Missing Wechat Config -- [key]');
  225 + }
  226 +
  227 + ksort($data);
  228 +
  229 + $string = md5(self::getSignContent($data).'&key='.$key);
  230 +
  231 + Log::debug('Wechat Generate Sign Before UPPER', [$data, $string]);
  232 +
  233 + return strtoupper($string);
  234 + }
  235 +
  236 + /**
  237 + * Generate sign content.
  238 + *
  239 + * @author yansongda <me@yansongda.cn>
  240 + *
  241 + * @param array $data
  242 + */
  243 + public static function getSignContent($data): string
  244 + {
  245 + $buff = '';
  246 +
  247 + foreach ($data as $k => $v) {
  248 + $buff .= ('sign' != $k && '' != $v && !is_array($v)) ? $k.'='.$v.'&' : '';
  249 + }
  250 +
  251 + Log::debug('Wechat Generate Sign Content Before Trim', [$data, $buff]);
  252 +
  253 + return trim($buff, '&');
  254 + }
  255 +
  256 + /**
  257 + * Decrypt refund contents.
  258 + *
  259 + * @author yansongda <me@yansongda.cn>
  260 + *
  261 + * @param string $contents
  262 + */
  263 + public static function decryptRefundContents($contents): string
  264 + {
  265 + return openssl_decrypt(
  266 + base64_decode($contents),
  267 + 'AES-256-ECB',
  268 + md5(self::$instance->key),
  269 + OPENSSL_RAW_DATA
  270 + );
  271 + }
  272 +
  273 + /**
  274 + * Convert array to xml.
  275 + *
  276 + * @author yansongda <me@yansongda.cn>
  277 + *
  278 + * @param array $data
  279 + *
  280 + * @throws InvalidArgumentException
  281 + */
  282 + public static function toXml($data): string
  283 + {
  284 + if (!is_array($data) || count($data) <= 0) {
  285 + throw new InvalidArgumentException('Convert To Xml Error! Invalid Array!');
  286 + }
  287 +
  288 + $xml = '<xml>';
  289 + foreach ($data as $key => $val) {
  290 + $xml .= is_numeric($val) ? '<'.$key.'>'.$val.'</'.$key.'>' :
  291 + '<'.$key.'><![CDATA['.$val.']]></'.$key.'>';
  292 + }
  293 + $xml .= '</xml>';
  294 +
  295 + return $xml;
  296 + }
  297 +
  298 + /**
  299 + * Convert xml to array.
  300 + *
  301 + * @author yansongda <me@yansongda.cn>
  302 + *
  303 + * @param string $xml
  304 + *
  305 + * @throws InvalidArgumentException
  306 + */
  307 + public static function fromXml($xml): array
  308 + {
  309 + if (!$xml) {
  310 + throw new InvalidArgumentException('Convert To Array Error! Invalid Xml!');
  311 + }
  312 +
  313 + libxml_disable_entity_loader(true);
  314 +
  315 + return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true);
  316 + }
  317 +
  318 + /**
  319 + * Get service config.
  320 + *
  321 + * @author yansongda <me@yansongda.cn>
  322 + *
  323 + * @param string|null $key
  324 + * @param mixed|null $default
  325 + *
  326 + * @return mixed|null
  327 + */
  328 + public function getConfig($key = null, $default = null)
  329 + {
  330 + if (is_null($key)) {
  331 + return $this->config->all();
  332 + }
  333 +
  334 + if ($this->config->has($key)) {
  335 + return $this->config[$key];
  336 + }
  337 +
  338 + return $default;
  339 + }
  340 +
  341 + /**
  342 + * Get app id according to param type.
  343 + *
  344 + * @author yansongda <me@yansongda.cn>
  345 + *
  346 + * @param string $type
  347 + */
  348 + public static function getTypeName($type = ''): string
  349 + {
  350 + switch ($type) {
  351 + case '':
  352 + $type = 'app_id';
  353 + break;
  354 + case 'app':
  355 + $type = 'appid';
  356 + break;
  357 + default:
  358 + $type = $type.'_id';
  359 + }
  360 +
  361 + return $type;
  362 + }
  363 +
  364 + /**
  365 + * Get Base Uri.
  366 + *
  367 + * @author yansongda <me@yansongda.cn>
  368 + *
  369 + * @return string
  370 + */
  371 + public function getBaseUri()
  372 + {
  373 + return $this->baseUri;
  374 + }
  375 +
  376 + /**
  377 + * processingApiResult.
  378 + *
  379 + * @author yansongda <me@yansongda.cn>
  380 + *
  381 + * @param $endpoint
  382 + *
  383 + * @throws GatewayException
  384 + * @throws InvalidArgumentException
  385 + * @throws InvalidSignException
  386 + *
  387 + * @return Collection
  388 + */
  389 + protected static function processingApiResult($endpoint, array $result)
  390 + {
  391 + if (!isset($result['return_code']) || 'SUCCESS' != $result['return_code']) {
  392 + throw new GatewayException('Get Wechat API Error:'.($result['return_msg'] ?? $result['retmsg'] ?? ''), $result);
  393 + }
  394 +
  395 + if (isset($result['result_code']) && 'SUCCESS' != $result['result_code']) {
  396 + throw new BusinessException('Wechat Business Error: '.$result['err_code'].' - '.$result['err_code_des'], $result);
  397 + }
  398 +
  399 + if (false !== strpos($endpoint, 'xdc/apiv2getsignkey') ||
  400 + false !== strpos($endpoint, 'mmpaymkttransfers') ||
  401 + self::generateSign($result) === $result['sign']) {
  402 + return new Collection($result);
  403 + }
  404 +
  405 + Events::dispatch(new Events\SignFailed('Wechat', '', $result));
  406 +
  407 + throw new InvalidSignException('Wechat Sign Verify FAILED', $result);
  408 + }
  409 +
  410 + /**
  411 + * setDevKey.
  412 + *
  413 + * @author yansongda <me@yansongda.cn>
  414 + *
  415 + * @throws GatewayException
  416 + * @throws InvalidArgumentException
  417 + * @throws InvalidSignException
  418 + * @throws Exception
  419 + *
  420 + * @return Support
  421 + */
  422 + private static function setDevKey()
  423 + {
  424 + if (Wechat::MODE_DEV == self::$instance->mode) {
  425 + $data = [
  426 + 'mch_id' => self::$instance->mch_id,
  427 + 'nonce_str' => Str::random(),
  428 + ];
  429 + $data['sign'] = self::generateSign($data);
  430 +
  431 + $result = self::requestApi('https://api.mch.weixin.qq.com/xdc/apiv2getsignkey/sign/getsignkey', $data);
  432 +
  433 + self::$instance->config->set('key', $result['sandbox_signkey']);
  434 + }
  435 +
  436 + return self::$instance;
  437 + }
  438 +
  439 + /**
  440 + * Set Http options.
  441 + *
  442 + * @author yansongda <me@yansongda.cn>
  443 + */
  444 + private function setHttpOptions(): self
  445 + {
  446 + if ($this->config->has('http') && is_array($this->config->get('http'))) {
  447 + $this->config->forget('http.base_uri');
  448 + $this->httpOptions = $this->config->get('http');
  449 + }
  450 +
  451 + return $this;
  452 + }
  453 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Wechat;
  4 +
  5 +use Symfony\Component\HttpFoundation\Request;
  6 +use Yansongda\Pay\Events;
  7 +use Yansongda\Pay\Exceptions\GatewayException;
  8 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  9 +use Yansongda\Pay\Exceptions\InvalidSignException;
  10 +use Yansongda\Pay\Gateways\Wechat;
  11 +use Yansongda\Supports\Collection;
  12 +
  13 +class TransferGateway extends Gateway
  14 +{
  15 + /**
  16 + * Pay an order.
  17 + *
  18 + * @author yansongda <me@yansongda.cn>
  19 + *
  20 + * @param string $endpoint
  21 + *
  22 + * @throws GatewayException
  23 + * @throws InvalidArgumentException
  24 + * @throws InvalidSignException
  25 + */
  26 + public function pay($endpoint, array $payload): Collection
  27 + {
  28 + if (Wechat::MODE_SERVICE === $this->mode) {
  29 + unset($payload['sub_mch_id'], $payload['sub_appid']);
  30 + }
  31 +
  32 + $type = Support::getTypeName($payload['type'] ?? '');
  33 +
  34 + $payload['mch_appid'] = Support::getInstance()->getConfig($type, '');
  35 + $payload['mchid'] = $payload['mch_id'];
  36 +
  37 + if ('cli' !== php_sapi_name() && !isset($payload['spbill_create_ip'])) {
  38 + $payload['spbill_create_ip'] = Request::createFromGlobals()->server->get('SERVER_ADDR');
  39 + }
  40 +
  41 + unset($payload['appid'], $payload['mch_id'], $payload['trade_type'],
  42 + $payload['notify_url'], $payload['type']);
  43 +
  44 + $payload['sign'] = Support::generateSign($payload);
  45 +
  46 + Events::dispatch(new Events\PayStarted('Wechat', 'Transfer', $endpoint, $payload));
  47 +
  48 + return Support::requestApi(
  49 + 'mmpaymkttransfers/promotion/transfers',
  50 + $payload,
  51 + true
  52 + );
  53 + }
  54 +
  55 + /**
  56 + * Find.
  57 + *
  58 + * @author yansongda <me@yansongda.cn>
  59 + *
  60 + * @param $order
  61 + */
  62 + public function find($order): array
  63 + {
  64 + return [
  65 + 'endpoint' => 'mmpaymkttransfers/gettransferinfo',
  66 + 'order' => is_array($order) ? $order : ['partner_trade_no' => $order],
  67 + 'cert' => true,
  68 + ];
  69 + }
  70 +
  71 + /**
  72 + * Get trade type config.
  73 + *
  74 + * @author yansongda <me@yansongda.cn>
  75 + */
  76 + protected function getTradeType(): string
  77 + {
  78 + return '';
  79 + }
  80 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Wechat;
  4 +
  5 +use Symfony\Component\HttpFoundation\RedirectResponse;
  6 +use Yansongda\Pay\Events;
  7 +use Yansongda\Pay\Exceptions\GatewayException;
  8 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  9 +use Yansongda\Pay\Exceptions\InvalidSignException;
  10 +
  11 +class WapGateway extends Gateway
  12 +{
  13 + /**
  14 + * Pay an order.
  15 + *
  16 + * @author yansongda <me@yansongda.cn>
  17 + *
  18 + * @param string $endpoint
  19 + *
  20 + * @throws GatewayException
  21 + * @throws InvalidArgumentException
  22 + * @throws InvalidSignException
  23 + */
  24 + public function pay($endpoint, array $payload): RedirectResponse
  25 + {
  26 + $payload['trade_type'] = $this->getTradeType();
  27 +
  28 + Events::dispatch(new Events\PayStarted('Wechat', 'Wap', $endpoint, $payload));
  29 +
  30 + $mweb_url = $this->preOrder($payload)->get('mweb_url');
  31 +
  32 + $url = is_null(Support::getInstance()->return_url) ? $mweb_url : $mweb_url.
  33 + '&redirect_url='.urlencode(Support::getInstance()->return_url);
  34 +
  35 + return new RedirectResponse($url);
  36 + }
  37 +
  38 + /**
  39 + * Get trade type config.
  40 + *
  41 + * @author yansongda <me@yansongda.cn>
  42 + */
  43 + protected function getTradeType(): string
  44 + {
  45 + return 'MWEB';
  46 + }
  47 +}
  1 +<?php
  2 +
  3 +namespace Yansongda\Pay\Gateways\Wechat;
  4 +
  5 +use Symfony\Component\HttpFoundation\RedirectResponse;
  6 +use Symfony\Component\HttpFoundation\Response;
  7 +use Symfony\Component\HttpFoundation\Request;
  8 +use Yansongda\Pay\Events;
  9 +use Yansongda\Pay\Exceptions\GatewayException;
  10 +use Yansongda\Pay\Exceptions\InvalidArgumentException;
  11 +use Yansongda\Pay\Exceptions\InvalidSignException;
  12 +use Yansongda\Supports\Collection;
  13 +
  14 +class WebGateway extends Gateway
  15 +{
  16 + /**
  17 + * Pay an order.
  18 + *
  19 + * @param string $endpoint
  20 + * @param array $payload
  21 + *
  22 + * @author yansongda <me@yansongda.cn>
  23 + *
  24 + */
  25 + public function pay($endpoint, array $payload): Response
  26 + {
  27 + $payload['spbill_create_ip'] = Request::createFromGlobals()->server->get('SERVER_ADDR');
  28 + $payload['trade_type'] = $this->getTradeType();
  29 +
  30 + $code_url = $this->preOrder($payload)['code_url'];
  31 + $params = [
  32 + 'body' => $payload['body'],
  33 + 'code_url' => $code_url,
  34 + 'out_trade_no' => $payload['out_trade_no'],
  35 + 'return_url' => Support::getInstance()->return_url,
  36 + 'total_fee' => $payload['total_fee'],
  37 + ];
  38 +
  39 + $params['sign'] = md5(implode('', $params) . Support::getInstance()->app_id);
  40 + $endpoint = addon_url("epay/api/wechat");
  41 +
  42 + Events::dispatch(new Events\PayStarted('Wechat', 'Web/Wap', $endpoint, $payload));
  43 +
  44 + return $this->buildPayHtml($endpoint, $params);
  45 + }
  46 +
  47 + /**
  48 + * Build Html response.
  49 + *
  50 + * @param string $endpoint
  51 + * @param array $payload
  52 + * @param string $method
  53 + *
  54 + * @return Response
  55 + * @author yansongda <me@yansongda.cn>
  56 + *
  57 + */
  58 + protected function buildPayHtml($endpoint, $payload, $method = 'POST'): Response
  59 + {
  60 + if (strtoupper($method) === 'GET') {
  61 + return RedirectResponse::create($endpoint . '?' . http_build_query($payload));
  62 + }
  63 +
  64 + $sHtml = "<form id='wechat_submit' name='wechat_submit' action='" . $endpoint . "' method='" . $method . "'>";
  65 + foreach ($payload as $key => $val) {
  66 + $val = str_replace("'", '&apos;', $val);
  67 + $sHtml .= "<input type='hidden' name='" . $key . "' value='" . $val . "'/>";
  68 + }
  69 + $sHtml .= "<input type='submit' value='ok' style='display:none;'></form>";
  70 + $sHtml .= "<script>document.forms['wechat_submit'].submit();</script>";
  71 +
  72 + return Response::create($sHtml);
  73 + }
  74 +
  75 + /**
  76 + * Get trade type config.
  77 + *
  78 + * @return string
  79 + * @author yansongda <me@yansongda.cn>
  80 + *
  81 + */
  82 + protected function getTradeType(): string
  83 + {
  84 + return 'NATIVE';
  85 + }
  86 +}