正在显示
61 个修改的文件
包含
4815 行增加
和
0 行删除
addons/epay/.addonrc
0 → 100644
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"]} |
addons/epay/Epay.php
0 → 100644
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 | +} |
addons/epay/certs/alipayCertPublicKey.crt
0 → 100644
addons/epay/certs/alipayRootCert.crt
0 → 100644
addons/epay/certs/apiclient_cert.pem
0 → 100644
addons/epay/certs/apiclient_key.pem
0 → 100644
addons/epay/certs/appCertPublicKey.crt
0 → 100644
addons/epay/config.html
0 → 100644
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> |
addons/epay/config.php
0 → 100644
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 | +]; |
addons/epay/controller/Api.php
0 → 100644
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 | +} |
addons/epay/controller/Index.php
0 → 100644
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 | +} |
addons/epay/info.ini
0 → 100644
addons/epay/library/Collection.php
0 → 100644
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 | +} |
addons/epay/library/OrderException.php
0 → 100644
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 | +} |
addons/epay/library/RedirectResponse.php
0 → 100644
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 | +} |
addons/epay/library/Response.php
0 → 100644
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 | +} |
addons/epay/library/Service.php
0 → 100644
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 | +} |
addons/epay/library/Wechat.php
0 → 100644
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 | +} |
addons/epay/library/Yansongda/Pay/Events.php
0 → 100644
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("'", ''', $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("'", ''', $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 | +} |
-
请 注册 或 登录 后发表评论