Skip to content

Commit db85283

Browse files
carlin-rjwanghaojieyansongda
authored
feat: 新增江苏银行e融支付 (#1002)
--------- Co-authored-by: wanghaojie <[email protected]> Co-authored-by: yansongda <[email protected]>
1 parent df334dd commit db85283

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2104
-7
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 3.7.7
2+
3+
### added
4+
5+
- feat: 新增江苏银行e融支付(#1002)
6+
17
## v3.7.6
28

39
### fixed

README.md

+77-1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ yansongda/pay 100% 兼容 支付宝/微信/银联 所有功能(包括服务商
8484
- 刷卡支付
8585
- 扫码支付
8686
- ...
87+
-
88+
### 江苏银行(e融支付)
89+
90+
- 聚合扫码支付(微信,支付宝,银联,e融)
91+
- ...
8792

8893
## 安装
8994
```shell
@@ -279,9 +284,80 @@ class WechatController
279284
}
280285
```
281286

287+
### 江苏银行(e融支付)
288+
```php
289+
<?php
290+
291+
namespace App\Http\Controllers;
292+
293+
use Yansongda\Pay\Pay;
294+
295+
class EpayController
296+
{
297+
protected $config = [
298+
'jsb' => [
299+
'default' => [
300+
// 服务代码
301+
'svr_code' => '',
302+
// 必填-合作商ID
303+
'partner_id' => '',
304+
// 必填-公私钥对编号
305+
'public_key_code' => '00',
306+
// 必填-商户私钥(加密签名)
307+
'mch_secret_cert_path' => '',
308+
// 必填-商户公钥证书路径(提供江苏银行进行验证签名用)
309+
'mch_public_cert_path' => '',
310+
// 必填-江苏银行的公钥(用于解密江苏银行返回的数据)
311+
'jsb_public_cert_path' => '',
312+
//支付通知地址
313+
'notify_url' => '',
314+
// 选填-默认为正常模式。可选为: MODE_NORMAL:正式环境, MODE_SANDBOX:测试环境
315+
'mode' => Pay::MODE_NORMAL,
316+
]
317+
],
318+
'logger' => [ // optional
319+
'enable' => false,
320+
'file' => './logs/epay.log',
321+
'level' => 'info', // 建议生产环境等级调整为 info,开发环境为 debug
322+
'type' => 'single', // optional, 可选 daily.
323+
'max_file' => 30, // optional, 当 type 为 daily 时有效,默认 30 天
324+
],
325+
'http' => [ // optional
326+
'timeout' => 5.0,
327+
'connect_timeout' => 5.0,
328+
// 更多配置项请参考 [Guzzle](https://guzzle-cn.readthedocs.io/zh_CN/latest/request-options.html)
329+
],
330+
];
331+
332+
public function index()
333+
{
334+
$order = [
335+
'outTradeNo' => time().'',
336+
'proInfo' => 'subject-测试',
337+
'totalFee'=> 1,
338+
];
339+
340+
$pay = Pay::jsb($this->config)->scan($order);
341+
}
342+
343+
public function notifyCallback()
344+
{
345+
$pay = Pay::jsb($this->config);
346+
347+
try{
348+
$data = $pay->callback(); // 是的,验签就这么简单!
349+
} catch (\Exception $e) {
350+
// $e->getMessage();
351+
}
352+
353+
return $pay->success();
354+
}
355+
}
356+
```
357+
282358
## 代码贡献
283359

284-
由于测试及使用环境的限制,本项目中只开发了「支付宝」「微信支付」的相关支付网关。
360+
由于测试及使用环境的限制,本项目中只开发了「支付宝」「微信支付」、「银联」、「江苏银行」的相关支付网关。
285361

286362
如果您有其它支付网关的需求,或者发现本项目中需要改进的代码,**_欢迎 Fork 并提交 PR!_**
287363

src/Exception/Exception.php

+2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ class Exception extends \Exception
5555

5656
public const CONFIG_UNIPAY_INVALID = 9403;
5757

58+
public const CONFIG_JSB_INVALID = 9404;
59+
5860
/**
5961
* 关于签名.
6062
*/

src/Functions.php

+38
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Yansongda\Pay\Plugin\Wechat\V3\AddPayloadSignaturePlugin;
2424
use Yansongda\Pay\Plugin\Wechat\V3\WechatPublicCertsPlugin;
2525
use Yansongda\Pay\Provider\Alipay;
26+
use Yansongda\Pay\Provider\Jsb;
2627
use Yansongda\Pay\Provider\Unipay;
2728
use Yansongda\Pay\Provider\Wechat;
2829
use Yansongda\Supports\Collection;
@@ -591,3 +592,40 @@ function verify_unipay_sign_qra(array $config, array $destination): void
591592
throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证银联签名失败', $destination);
592593
}
593594
}
595+
596+
function get_jsb_url(array $config, ?Collection $payload): string
597+
{
598+
$url = get_radar_url($config, $payload) ?? '';
599+
if (str_starts_with($url, 'http')) {
600+
return $url;
601+
}
602+
603+
return Jsb::URL[$config['mode'] ?? Pay::MODE_NORMAL];
604+
}
605+
606+
/**
607+
* @throws InvalidConfigException
608+
* @throws InvalidSignException
609+
*/
610+
function verify_jsb_sign(array $config, string $content, string $sign): void
611+
{
612+
if (empty($sign)) {
613+
throw new InvalidSignException(Exception::SIGN_EMPTY, '签名异常: 江苏银行签名为空', func_get_args());
614+
}
615+
616+
$publicCert = $config['jsb_public_cert_path'] ?? null;
617+
618+
if (empty($publicCert)) {
619+
throw new InvalidConfigException(Exception::CONFIG_JSB_INVALID, '配置异常: 缺少配置参数 -- [jsb_public_cert_path]');
620+
}
621+
622+
$result = 1 === openssl_verify(
623+
$content,
624+
base64_decode($sign),
625+
get_public_cert($publicCert)
626+
);
627+
628+
if (!$result) {
629+
throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证江苏银行签名失败', func_get_args());
630+
}
631+
}

src/Pay.php

+4
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,19 @@
1010
use Yansongda\Artful\Exception\ContainerException;
1111
use Yansongda\Artful\Exception\ServiceNotFoundException;
1212
use Yansongda\Pay\Provider\Alipay;
13+
use Yansongda\Pay\Provider\Jsb;
1314
use Yansongda\Pay\Provider\Unipay;
1415
use Yansongda\Pay\Provider\Wechat;
1516
use Yansongda\Pay\Service\AlipayServiceProvider;
17+
use Yansongda\Pay\Service\JsbServiceProvider;
1618
use Yansongda\Pay\Service\UnipayServiceProvider;
1719
use Yansongda\Pay\Service\WechatServiceProvider;
1820

1921
/**
2022
* @method static Alipay alipay(array $config = [], $container = null)
2123
* @method static Wechat wechat(array $config = [], $container = null)
2224
* @method static Unipay unipay(array $config = [], $container = null)
25+
* @method static Jsb jsb(array $config = [], $container = null)
2326
*/
2427
class Pay
2528
{
@@ -42,6 +45,7 @@ class Pay
4245
AlipayServiceProvider::class,
4346
WechatServiceProvider::class,
4447
UnipayServiceProvider::class,
48+
JsbServiceProvider::class,
4549
];
4650

4751
/**
+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yansongda\Pay\Plugin\Jsb;
6+
7+
use Closure;
8+
use Yansongda\Artful\Contract\PluginInterface;
9+
use Yansongda\Artful\Exception\ContainerException;
10+
use Yansongda\Artful\Exception\InvalidConfigException;
11+
use Yansongda\Artful\Exception\InvalidParamsException;
12+
use Yansongda\Artful\Exception\ServiceNotFoundException;
13+
use Yansongda\Artful\Logger;
14+
use Yansongda\Artful\Rocket;
15+
use Yansongda\Pay\Exception\Exception;
16+
use Yansongda\Supports\Collection;
17+
18+
use function Yansongda\Pay\get_private_cert;
19+
use function Yansongda\Pay\get_provider_config;
20+
21+
class AddPayloadSignPlugin implements PluginInterface
22+
{
23+
/**
24+
* @throws ContainerException
25+
* @throws InvalidConfigException
26+
* @throws InvalidParamsException
27+
* @throws ServiceNotFoundException
28+
*/
29+
public function assembly(Rocket $rocket, Closure $next): Rocket
30+
{
31+
Logger::info('[Jsb][AddPayloadSignPlugin] 插件开始装载', ['rocket' => $rocket]);
32+
33+
$params = $rocket->getParams();
34+
$config = get_provider_config('jsb', $params);
35+
$payload = $rocket->getPayload();
36+
37+
if (empty($payload) || $payload->isEmpty()) {
38+
throw new InvalidParamsException(Exception::PARAMS_NECESSARY_PARAMS_MISSING, '参数异常: 缺少支付必要参数。可能插件用错顺序,应该先使用 `业务插件`');
39+
}
40+
41+
$privateCertPath = $config['mch_secret_cert_path'] ?? '';
42+
43+
if (empty($privateCertPath)) {
44+
throw new InvalidConfigException(Exception::CONFIG_JSB_INVALID, '配置异常: 缺少配置参数 -- [mch_secret_cert_path]');
45+
}
46+
47+
$rocket->mergePayload([
48+
'signType' => 'RSA',
49+
'sign' => $this->getSignature(get_private_cert($privateCertPath), $payload),
50+
]);
51+
52+
Logger::info('[Jsb][AddPayloadSignPlugin] 插件装载完毕', ['rocket' => $rocket]);
53+
54+
return $next($rocket);
55+
}
56+
57+
protected function getSignature(string $pkey, Collection $payload): string
58+
{
59+
$content = $payload->sortKeys()->toString();
60+
61+
openssl_sign($content, $signature, $pkey);
62+
63+
return base64_encode($signature);
64+
}
65+
}

src/Plugin/Jsb/AddRadarPlugin.php

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yansongda\Pay\Plugin\Jsb;
6+
7+
use Closure;
8+
use GuzzleHttp\Psr7\Request;
9+
use Yansongda\Artful\Contract\PluginInterface;
10+
use Yansongda\Artful\Exception\ContainerException;
11+
use Yansongda\Artful\Exception\ServiceNotFoundException;
12+
use Yansongda\Artful\Logger;
13+
use Yansongda\Artful\Rocket;
14+
use Yansongda\Supports\Collection;
15+
16+
use function Yansongda\Pay\get_jsb_url;
17+
use function Yansongda\Pay\get_provider_config;
18+
19+
class AddRadarPlugin implements PluginInterface
20+
{
21+
/**
22+
* @throws ServiceNotFoundException
23+
* @throws ContainerException
24+
*/
25+
public function assembly(Rocket $rocket, Closure $next): Rocket
26+
{
27+
Logger::info('[Jsb][AddRadarPlugin] 插件开始装载', ['rocket' => $rocket]);
28+
29+
$params = $rocket->getParams();
30+
$config = get_provider_config('jsb', $params);
31+
$payload = $rocket->getPayload();
32+
33+
$rocket->setRadar(new Request(
34+
strtoupper($params['_method'] ?? 'POST'),
35+
get_jsb_url($config, $payload),
36+
$this->getHeaders(),
37+
$this->getBody($payload),
38+
));
39+
40+
Logger::info('[Jsb][AddRadarPlugin] 插件装载完毕', ['rocket' => $rocket]);
41+
42+
return $next($rocket);
43+
}
44+
45+
protected function getHeaders(): array
46+
{
47+
return [
48+
'Content-Type' => 'text/html',
49+
'User-Agent' => 'yansongda/pay-v3',
50+
];
51+
}
52+
53+
protected function getBody(Collection $payload): string
54+
{
55+
$sign = $payload->get('sign');
56+
$signType = $payload->get('signType');
57+
58+
$payload->forget('sign');
59+
$payload->forget('signType');
60+
61+
$payload = $payload->sortKeys();
62+
63+
$payload->set('sign', $sign);
64+
$payload->set('signType', $signType);
65+
66+
return $payload->toString();
67+
}
68+
}

src/Plugin/Jsb/CallbackPlugin.php

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yansongda\Pay\Plugin\Jsb;
6+
7+
use Closure;
8+
use Yansongda\Artful\Contract\PluginInterface;
9+
use Yansongda\Artful\Direction\NoHttpRequestDirection;
10+
use Yansongda\Artful\Exception\ContainerException;
11+
use Yansongda\Artful\Exception\InvalidConfigException;
12+
use Yansongda\Artful\Exception\InvalidParamsException;
13+
use Yansongda\Artful\Exception\ServiceNotFoundException;
14+
use Yansongda\Artful\Logger;
15+
use Yansongda\Artful\Rocket;
16+
use Yansongda\Pay\Exception\Exception;
17+
use Yansongda\Pay\Exception\InvalidSignException;
18+
use Yansongda\Supports\Collection;
19+
20+
use function Yansongda\Pay\get_provider_config;
21+
use function Yansongda\Pay\verify_jsb_sign;
22+
23+
class CallbackPlugin implements PluginInterface
24+
{
25+
/**
26+
* @throws ContainerException
27+
* @throws InvalidConfigException
28+
* @throws InvalidParamsException
29+
* @throws ServiceNotFoundException
30+
* @throws InvalidSignException
31+
*/
32+
public function assembly(Rocket $rocket, Closure $next): Rocket
33+
{
34+
Logger::info('[Jsb][CallbackPlugin] 插件开始装载', ['rocket' => $rocket]);
35+
36+
$this->formatRequestAndParams($rocket);
37+
38+
$params = $rocket->getParams();
39+
$config = get_provider_config('jsb', $params);
40+
41+
$payload = $rocket->getPayload();
42+
$signature = $payload->get('sign');
43+
44+
$payload->forget('sign');
45+
$payload->forget('signType');
46+
47+
verify_jsb_sign($config, $payload->sortKeys()->toString(), $signature);
48+
49+
$rocket->setDirection(NoHttpRequestDirection::class)
50+
->setDestination($rocket->getPayload());
51+
52+
Logger::info('[Jsb][CallbackPlugin] 插件装载完毕', ['rocket' => $rocket]);
53+
54+
return $next($rocket);
55+
}
56+
57+
/**
58+
* @throws InvalidParamsException
59+
*/
60+
protected function formatRequestAndParams(Rocket $rocket): void
61+
{
62+
$request = $rocket->getParams()['request'] ?? null;
63+
64+
if (!$request instanceof Collection) {
65+
throw new InvalidParamsException(Exception::PARAMS_CALLBACK_REQUEST_INVALID);
66+
}
67+
68+
$rocket->setPayload($request)->setParams($rocket->getParams()['params'] ?? []);
69+
}
70+
}

0 commit comments

Comments
 (0)