代码初始化

This commit is contained in:
2025-08-07 20:21:47 +08:00
commit 50f3a2dbb0
2191 changed files with 374790 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Contract;
interface ConfigInterface
{
/**
* @param mixed $default default value of the entry when does not found
*
* @return mixed
*/
public function get(string $key, $default = null);
public function has(string $key): bool;
/**
* @param mixed $value
*/
public function set(string $key, $value): void;
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Contract;
interface ContainerInterface extends \Psr\Container\ContainerInterface
{
/**
* factory make.
*
* @return mixed
*/
public function make(string $name, array $parameters = []);
/**
* @param mixed $entry
*
* @return mixed
*/
public function set(string $name, $entry);
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Contract;
use Psr\Http\Message\ResponseInterface;
interface DirectionInterface
{
/**
* @return mixed
*/
public function parse(PackerInterface $packer, ?ResponseInterface $response);
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Contract;
interface EventDispatcherInterface extends \Psr\EventDispatcher\EventDispatcherInterface
{
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Contract;
use Psr\Http\Client\ClientInterface;
interface HttpClientInterface extends ClientInterface
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Contract;
interface LoggerInterface extends \Psr\Log\LoggerInterface
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Contract;
interface PackerInterface
{
public function pack(array $payload): string;
public function unpack(string $payload): ?array;
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Contract;
use Closure;
use Yansongda\Pay\Rocket;
interface PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket;
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Contract;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\InvalidParamsException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Supports\Collection;
interface ProviderInterface
{
/**
* @return null|array|Collection|MessageInterface
*
* @throws ContainerException
* @throws InvalidParamsException
* @throws ServiceNotFoundException
*/
public function pay(array $plugins, array $params);
/**
* @param array|string $order
*
* @return array|Collection
*/
public function find($order);
/**
* @param array|string $order
*
* @return array|Collection|void
*/
public function cancel($order);
/**
* @param array|string $order
*
* @return array|Collection|void
*/
public function close($order);
/**
* @return array|Collection
*/
public function refund(array $order);
/**
* @param null|array|ServerRequestInterface $contents
*/
public function callback($contents = null, ?array $params = null): Collection;
public function success(): ResponseInterface;
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Contract;
use Yansongda\Pay\Exception\ContainerException;
interface ServiceProviderInterface
{
/**
* @param mixed $data
*
* @throws ContainerException
*/
public function register($data = null): void;
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Contract;
interface ShortcutInterface
{
/**
* @return PluginInterface[]|string[]
*/
public function getPlugins(array $params): array;
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Direction;
use Psr\Http\Message\ResponseInterface;
use Yansongda\Pay\Contract\DirectionInterface;
use Yansongda\Pay\Contract\PackerInterface;
use Yansongda\Pay\Exception\Exception;
use Yansongda\Pay\Exception\InvalidResponseException;
class ArrayDirection implements DirectionInterface
{
/**
* @throws InvalidResponseException
*/
public function parse(PackerInterface $packer, ?ResponseInterface $response): array
{
if (is_null($response)) {
throw new InvalidResponseException(Exception::RESPONSE_NONE);
}
$body = (string) $response->getBody();
if (!is_null($result = $packer->unpack($body))) {
return $result;
}
throw new InvalidResponseException(Exception::UNPACK_RESPONSE_ERROR, 'Unpack Response Error', ['body' => $body, 'response' => $response]);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Direction;
use Psr\Http\Message\ResponseInterface;
use Yansongda\Pay\Contract\DirectionInterface;
use Yansongda\Pay\Contract\PackerInterface;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Pay\Pay;
use Yansongda\Supports\Collection;
class CollectionDirection implements DirectionInterface
{
/**
* @throws ContainerException
* @throws ServiceNotFoundException
*/
public function parse(PackerInterface $packer, ?ResponseInterface $response): Collection
{
return new Collection(
Pay::get(ArrayDirection::class)->parse($packer, $response)
);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Direction;
use Psr\Http\Message\ResponseInterface;
use Yansongda\Pay\Contract\DirectionInterface;
use Yansongda\Pay\Contract\PackerInterface;
class NoHttpRequestDirection implements DirectionInterface
{
public function parse(PackerInterface $packer, ?ResponseInterface $response): ?ResponseInterface
{
return $response;
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Direction;
use Psr\Http\Message\ResponseInterface;
use Yansongda\Pay\Contract\DirectionInterface;
use Yansongda\Pay\Contract\PackerInterface;
use Yansongda\Pay\Exception\Exception;
use Yansongda\Pay\Exception\InvalidResponseException;
class OriginResponseDirection implements DirectionInterface
{
/**
* @throws InvalidResponseException
*/
public function parse(PackerInterface $packer, ?ResponseInterface $response): ?ResponseInterface
{
if (!is_null($response)) {
return $response;
}
throw new InvalidResponseException(Exception::INVALID_RESPONSE_CODE);
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Direction;
class ResponseDirection extends NoHttpRequestDirection
{
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay;
use Yansongda\Pay\Contract\EventDispatcherInterface;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\InvalidConfigException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
/**
* @method static Event\Event dispatch(object $event)
*/
class Event
{
/**
* @throws ContainerException
* @throws ServiceNotFoundException
* @throws InvalidConfigException
*/
public static function __callStatic(string $method, array $args): void
{
if (!Pay::hasContainer() || !Pay::has(EventDispatcherInterface::class)) {
return;
}
$class = Pay::get(EventDispatcherInterface::class);
if ($class instanceof \Psr\EventDispatcher\EventDispatcherInterface) {
$class->{$method}(...$args);
return;
}
throw new InvalidConfigException(Exception\Exception::EVENT_CONFIG_ERROR);
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Event;
class ApiRequested extends Event
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Event;
class ApiRequesting extends Event
{
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Event;
use Psr\Http\Message\ServerRequestInterface;
use Yansongda\Pay\Rocket;
class CallbackReceived extends Event
{
public string $provider;
public ?array $params = null;
/**
* @var null|array|ServerRequestInterface
*/
public $contents;
/**
* @param null|array|ServerRequestInterface $contents
*/
public function __construct(string $provider, $contents, ?array $params = null, ?Rocket $rocket = null)
{
$this->provider = $provider;
$this->contents = $contents;
$this->params = $params;
parent::__construct($rocket);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Event;
use Yansongda\Pay\Rocket;
class Event
{
public ?Rocket $rocket = null;
public function __construct(?Rocket $rocket = null)
{
$this->rocket = $rocket;
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Event;
use Yansongda\Pay\Rocket;
class MethodCalled extends Event
{
public string $provider;
public string $name;
public array $params;
public function __construct(string $provider, string $name, array $params, ?Rocket $rocket = null)
{
$this->provider = $provider;
$this->name = $name;
$this->params = $params;
parent::__construct($rocket);
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Event;
class PayFinish extends Event
{
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Event;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Rocket;
class PayStarted extends Event
{
/**
* @var PluginInterface[]
*/
public array $plugins;
public array $params;
public function __construct(array $plugins, array $params, ?Rocket $rocket = null)
{
$this->plugins = $plugins;
$this->params = $params;
parent::__construct($rocket);
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Exception;
use Psr\Container\ContainerExceptionInterface;
use Throwable;
class ContainerException extends Exception implements ContainerExceptionInterface
{
/**
* @param mixed $extra
*/
public function __construct(string $message = '', int $code = self::CONTAINER_ERROR, $extra = null, Throwable $previous = null)
{
parent::__construct($message, $code, $extra, $previous);
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Exception;
use Throwable;
class ContainerNotFoundException extends ContainerException
{
/**
* @param mixed $extra
*/
public function __construct(string $message = 'Container Not Found', int $code = self::CONTAINER_NOT_FOUND, $extra = null, Throwable $previous = null)
{
parent::__construct($message, $code, $extra, $previous);
}
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Exception;
use Throwable;
class Exception extends \Exception
{
public const UNKNOWN_ERROR = 9999;
/**
* 关于容器.
*/
public const CONTAINER_ERROR = 1000;
public const CONTAINER_NOT_FOUND = 1001;
public const CONTAINER_NOT_FOUND_ENTRY = 1002;
/**
* 关于容器的服务.
*/
public const SERVICE_ERROR = 2000;
public const SERVICE_NOT_FOUND_ERROR = 2001;
/*
* 关于配置.
*/
public const CONFIG_ERROR = 3000;
public const INVALID_PARSER = 3001;
public const ALIPAY_CONFIG_ERROR = 3002;
public const LOGGER_CONFIG_ERROR = 3003;
public const HTTP_CLIENT_CONFIG_ERROR = 3004;
public const EVENT_CONFIG_ERROR = 3005;
public const WECHAT_CONFIG_ERROR = 3006;
public const UNIPAY_CONFIG_ERROR = 3007;
public const INVALID_PACKER = 3008;
/*
* 关于参数.
*/
public const PARAMS_ERROR = 4000;
public const SHORTCUT_NOT_FOUND = 4001;
public const PLUGIN_ERROR = 4002;
public const SHORTCUT_MULTI_ACTION_ERROR = 4003;
public const METHOD_NOT_SUPPORTED = 4004;
public const REQUEST_NULL_ERROR = 4005;
public const MISSING_NECESSARY_PARAMS = 4006;
public const NOT_IN_SERVICE_MODE = 4007;
public const WECHAT_SERIAL_NO_NOT_FOUND = 4008;
public const UNIPAY_FIND_STRING_NOT_SUPPORTED = 4009;
public const UNIPAY_CANCEL_STRING_NOT_SUPPORTED = 4010;
/**
* 关于api.
*/
public const RESPONSE_ERROR = 5000;
public const REQUEST_RESPONSE_ERROR = 5001;
public const UNPACK_RESPONSE_ERROR = 5002;
public const INVALID_RESPONSE_SIGN = 5003;
public const INVALID_RESPONSE_CODE = 5004;
public const RESPONSE_MISSING_NECESSARY_PARAMS = 5005;
public const RESPONSE_NONE = 5006;
public const INVALID_CIPHERTEXT_PARAMS = 5007;
public const INVALID_REQUEST_ENCRYPTED_DATA = 5008;
public const INVALID_REQUEST_ENCRYPTED_METHOD = 5009;
/**
* raw.
*
* @var mixed
*/
public $extra;
/**
* @param mixed $extra
*/
public function __construct(string $message = 'Unknown Error', int $code = self::UNKNOWN_ERROR, $extra = null, Throwable $previous = null)
{
$this->extra = $extra;
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Exception;
use Throwable;
class InvalidConfigException extends Exception
{
/**
* @param mixed $extra
*/
public function __construct(int $code = self::CONFIG_ERROR, string $message = 'Config Error', $extra = null, Throwable $previous = null)
{
parent::__construct($message, $code, $extra, $previous);
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Exception;
use Throwable;
class InvalidParamsException extends Exception
{
/**
* @param mixed $extra
*/
public function __construct(int $code = self::PARAMS_ERROR, string $message = 'Params Error', $extra = null, Throwable $previous = null)
{
parent::__construct($message, $code, $extra, $previous);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Exception;
use Throwable;
class InvalidResponseException extends Exception
{
public ?Throwable $exception = null;
/**
* @var mixed
*/
public $response;
/**
* @param mixed $extra
*/
public function __construct(
int $code = self::RESPONSE_ERROR,
string $message = 'Provider response Error',
$extra = null,
?Throwable $exception = null,
Throwable $previous = null
) {
$this->response = $extra;
$this->exception = $exception;
parent::__construct($message, $code, $extra, $previous);
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Exception;
use Throwable;
class ServiceException extends Exception
{
/**
* @param mixed $extra
*/
public function __construct(string $message = 'Service Error', int $code = self::SERVICE_ERROR, $extra = null, Throwable $previous = null)
{
parent::__construct($message, $code, $extra, $previous);
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Exception;
use Psr\Container\NotFoundExceptionInterface;
use Throwable;
class ServiceNotFoundException extends Exception implements NotFoundExceptionInterface
{
/**
* @param mixed $extra
*/
public function __construct(string $message = 'Service Not Found', int $code = self::SERVICE_NOT_FOUND_ERROR, $extra = null, Throwable $previous = null)
{
parent::__construct($message, $code, $extra, $previous);
}
}

View File

@@ -0,0 +1,403 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Yansongda\Pay\Contract\ConfigInterface;
use Yansongda\Pay\Direction\NoHttpRequestDirection;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\Exception;
use Yansongda\Pay\Exception\InvalidConfigException;
use Yansongda\Pay\Exception\InvalidParamsException;
use Yansongda\Pay\Exception\InvalidResponseException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Pay\Plugin\ParserPlugin;
use Yansongda\Pay\Plugin\Wechat\PreparePlugin;
use Yansongda\Pay\Plugin\Wechat\RadarSignPlugin;
use Yansongda\Pay\Plugin\Wechat\WechatPublicCertsPlugin;
use Yansongda\Pay\Provider\Wechat;
use Yansongda\Supports\Str;
if (!function_exists('should_do_http_request')) {
function should_do_http_request(string $direction): bool
{
return NoHttpRequestDirection::class !== $direction
&& !in_array(NoHttpRequestDirection::class, class_parents($direction));
}
}
if (!function_exists('get_tenant')) {
function get_tenant(array $params = []): string
{
return strval($params['_config'] ?? 'default');
}
}
if (!function_exists('get_alipay_config')) {
/**
* @throws ContainerException
* @throws ServiceNotFoundException
*/
function get_alipay_config(array $params = []): array
{
$alipay = Pay::get(ConfigInterface::class)->get('alipay');
return $alipay[get_tenant($params)] ?? [];
}
}
if (!function_exists('get_public_cert')) {
function get_public_cert(string $key): string
{
return Str::endsWith($key, ['.cer', '.crt', '.pem']) ? file_get_contents($key) : $key;
}
}
if (!function_exists('get_private_cert')) {
function get_private_cert(string $key): string
{
if (Str::endsWith($key, ['.crt', '.pem'])) {
return file_get_contents($key);
}
return "-----BEGIN RSA PRIVATE KEY-----\n".
wordwrap($key, 64, "\n", true).
"\n-----END RSA PRIVATE KEY-----";
}
}
if (!function_exists('verify_alipay_sign')) {
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws ServiceNotFoundException
* @throws InvalidResponseException
*/
function verify_alipay_sign(array $params, string $contents, string $sign): void
{
$public = get_alipay_config($params)['alipay_public_cert_path'] ?? null;
if (empty($public)) {
throw new InvalidConfigException(Exception::ALIPAY_CONFIG_ERROR, 'Missing Alipay Config -- [alipay_public_cert_path]');
}
// 优化publickey判断
if (!Str::endsWith($public, ['.cer', '.crt', '.pem']) && stripos($public, '-----BEGIN PUBLIC KEY-----') === false) {
$public = "-----BEGIN PUBLIC KEY-----\n".
wordwrap($public, 64, "\n", true).
"\n-----END PUBLIC KEY-----";
}
$result = 1 === openssl_verify(
$contents,
base64_decode($sign),
get_public_cert($public),
OPENSSL_ALGO_SHA256
);
if (!$result) {
throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, 'Verify Alipay Response Sign Failed', func_get_args());
}
}
}
if (!function_exists('get_wechat_config')) {
/**
* @throws ContainerException
* @throws ServiceNotFoundException
*/
function get_wechat_config(array $params): array
{
$wechat = Pay::get(ConfigInterface::class)->get('wechat');
return $wechat[get_tenant($params)] ?? [];
}
}
if (!function_exists('get_wechat_base_uri')) {
/**
* @throws ContainerException
* @throws ServiceNotFoundException
*/
function get_wechat_base_uri(array $params): string
{
$config = get_wechat_config($params);
return Wechat::URL[$config['mode'] ?? Pay::MODE_NORMAL];
}
}
if (!function_exists('get_wechat_sign')) {
/**
* @throws ContainerException
* @throws ServiceNotFoundException
* @throws InvalidConfigException
*/
function get_wechat_sign(array $params, string $contents): string
{
$privateKey = get_wechat_config($params)['mch_secret_cert'] ?? null;
if (empty($privateKey)) {
throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_cert]');
}
$privateKey = get_private_cert($privateKey);
openssl_sign($contents, $sign, $privateKey, 'sha256WithRSAEncryption');
return base64_encode($sign);
}
}
if (!function_exists('get_wechat_sign_v2')) {
/**
* @throws ContainerException
* @throws ServiceNotFoundException
* @throws InvalidConfigException
*/
function get_wechat_sign_v2(array $params, array $payload, bool $upper = true): string
{
$key = get_wechat_config($params)['mch_secret_key_v2'] ?? null;
if (empty($key)) {
throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_key_v2]');
}
ksort($payload);
$buff = '';
foreach ($payload as $k => $v) {
$buff .= ('sign' != $k && '' != $v && !is_array($v)) ? $k.'='.$v.'&' : '';
}
$sign = md5($buff.'key='.$key);
return $upper ? strtoupper($sign) : $sign;
}
}
if (!function_exists('verify_wechat_sign')) {
/**
* @param ResponseInterface|ServerRequestInterface $message
*
* @throws ContainerException
* @throws InvalidConfigException
* @throws InvalidResponseException
* @throws ServiceNotFoundException
* @throws InvalidParamsException
*/
function verify_wechat_sign(MessageInterface $message, array $params): void
{
if ($message instanceof ServerRequestInterface && 'localhost' === $message->getUri()->getHost()) {
return;
}
$wechatSerial = $message->getHeaderLine('Wechatpay-Serial');
$timestamp = $message->getHeaderLine('Wechatpay-Timestamp');
$random = $message->getHeaderLine('Wechatpay-Nonce');
$sign = $message->getHeaderLine('Wechatpay-Signature');
$body = (string) $message->getBody();
$content = $timestamp."\n".$random."\n".$body."\n";
$public = get_wechat_config($params)['wechat_public_cert_path'][$wechatSerial] ?? null;
if (empty($sign)) {
throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', ['headers' => $message->getHeaders(), 'body' => $body]);
}
$public = get_public_cert(
empty($public) ? reload_wechat_public_certs($params, $wechatSerial) : $public
);
$result = 1 === openssl_verify(
$content,
base64_decode($sign),
$public,
'sha256WithRSAEncryption'
);
if (!$result) {
throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', ['headers' => $message->getHeaders(), 'body' => $body]);
}
}
}
if (!function_exists('encrypt_wechat_contents')) {
function encrypt_wechat_contents(string $contents, string $publicKey): ?string
{
if (openssl_public_encrypt($contents, $encrypted, get_public_cert($publicKey), OPENSSL_PKCS1_OAEP_PADDING)) {
return base64_encode($encrypted);
}
return null;
}
}
if (!function_exists('reload_wechat_public_certs')) {
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws ServiceNotFoundException
* @throws InvalidParamsException
* @throws InvalidResponseException
*/
function reload_wechat_public_certs(array $params, ?string $serialNo = null): string
{
$data = Pay::wechat()->pay(
[PreparePlugin::class, WechatPublicCertsPlugin::class, RadarSignPlugin::class, ParserPlugin::class],
$params
)->get('data', []);
foreach ($data as $item) {
$certs[$item['serial_no']] = decrypt_wechat_resource($item['encrypt_certificate'], $params)['ciphertext'] ?? '';
}
$wechatConfig = get_wechat_config($params);
Pay::get(ConfigInterface::class)->set(
'wechat.'.get_tenant($params).'.wechat_public_cert_path',
((array) ($wechatConfig['wechat_public_cert_path'] ?? [])) + ($certs ?? [])
);
if (!is_null($serialNo) && empty($certs[$serialNo])) {
throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Get Wechat Public Cert Error');
}
return $certs[$serialNo] ?? '';
}
}
if (!function_exists('get_wechat_public_certs')) {
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws ServiceNotFoundException
* @throws InvalidParamsException
* @throws InvalidResponseException
*/
function get_wechat_public_certs(array $params = [], ?string $path = null): void
{
reload_wechat_public_certs($params);
$config = get_wechat_config($params);
if (empty($path)) {
var_dump($config['wechat_public_cert_path']);
return;
}
foreach ($config['wechat_public_cert_path'] as $serialNo => $cert) {
file_put_contents($path.'/'.$serialNo.'.crt', $cert);
}
}
}
if (!function_exists('decrypt_wechat_resource')) {
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws InvalidResponseException
* @throws ServiceNotFoundException
*/
function decrypt_wechat_resource(array $resource, array $params): array
{
$ciphertext = base64_decode($resource['ciphertext'] ?? '');
$secret = get_wechat_config($params)['mch_secret_key'] ?? null;
if (strlen($ciphertext) <= Wechat::AUTH_TAG_LENGTH_BYTE) {
throw new InvalidResponseException(Exception::INVALID_CIPHERTEXT_PARAMS);
}
if (is_null($secret) || Wechat::MCH_SECRET_KEY_LENGTH_BYTE != strlen($secret)) {
throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_key]');
}
switch ($resource['algorithm'] ?? '') {
case 'AEAD_AES_256_GCM':
$resource['ciphertext'] = decrypt_wechat_resource_aes_256_gcm($ciphertext, $secret, $resource['nonce'] ?? '', $resource['associated_data'] ?? '');
break;
default:
throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_METHOD);
}
return $resource;
}
}
if (!function_exists('decrypt_wechat_resource_aes_256_gcm')) {
/**
* @return array|string
*
* @throws InvalidResponseException
*/
function decrypt_wechat_resource_aes_256_gcm(string $ciphertext, string $secret, string $nonce, string $associatedData)
{
$decrypted = openssl_decrypt(
substr($ciphertext, 0, -Wechat::AUTH_TAG_LENGTH_BYTE),
'aes-256-gcm',
$secret,
OPENSSL_RAW_DATA,
$nonce,
substr($ciphertext, -Wechat::AUTH_TAG_LENGTH_BYTE),
$associatedData
);
if ('certificate' !== $associatedData) {
$decrypted = json_decode(strval($decrypted), true);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_DATA);
}
}
return $decrypted;
}
}
if (!function_exists('get_unipay_config')) {
/**
* @throws ContainerException
* @throws ServiceNotFoundException
*/
function get_unipay_config(array $params): array
{
$unipay = Pay::get(ConfigInterface::class)->get('unipay');
return $unipay[get_tenant($params)] ?? [];
}
}
if (!function_exists('verify_unipay_sign')) {
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws InvalidResponseException
* @throws ServiceNotFoundException
*/
function verify_unipay_sign(array $params, string $contents, string $sign): void
{
if (empty($params['signPubKeyCert'])
&& empty($public = get_unipay_config($params)['unipay_public_cert_path'] ?? null)) {
throw new InvalidConfigException(Exception::UNIPAY_CONFIG_ERROR, 'Missing Unipay Config -- [unipay_public_cert_path]');
}
$result = 1 === openssl_verify(
hash('sha256', $contents),
base64_decode($sign),
get_public_cert($params['signPubKeyCert'] ?? $public ?? ''),
'sha256'
);
if (!$result) {
throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, 'Verify Unipay Response Sign Failed', func_get_args());
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay;
use Yansongda\Pay\Contract\ConfigInterface;
use Yansongda\Pay\Contract\LoggerInterface;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\InvalidConfigException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
/**
* @method static void emergency($message, array $context = [])
* @method static void alert($message, array $context = [])
* @method static void critical($message, array $context = [])
* @method static void error($message, array $context = [])
* @method static void warning($message, array $context = [])
* @method static void notice($message, array $context = [])
* @method static void info($message, array $context = [])
* @method static void debug($message, array $context = [])
* @method static void log($message, array $context = [])
*/
class Logger
{
/**
* @throws ContainerException
* @throws ServiceNotFoundException
* @throws InvalidConfigException
*/
public static function __callStatic(string $method, array $args): void
{
if (!Pay::hasContainer() || !Pay::has(LoggerInterface::class)
|| false === Pay::get(ConfigInterface::class)->get('logger.enable', false)) {
return;
}
$class = Pay::get(LoggerInterface::class);
if ($class instanceof \Psr\Log\LoggerInterface || $class instanceof \Yansongda\Supports\Logger) {
$class->{$method}(...$args);
return;
}
throw new InvalidConfigException(Exception\Exception::LOGGER_CONFIG_ERROR);
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Packer;
use Yansongda\Pay\Contract\PackerInterface;
class JsonPacker implements PackerInterface
{
public function pack(array $payload): string
{
return json_encode($payload, JSON_UNESCAPED_UNICODE);
}
public function unpack(string $payload): ?array
{
$data = json_decode($payload, true);
if (JSON_ERROR_NONE === json_last_error()) {
return $data;
}
return null;
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Packer;
use Yansongda\Pay\Contract\PackerInterface;
use Yansongda\Supports\Str;
class QueryPacker implements PackerInterface
{
public function pack(array $payload): string
{
return http_build_query($payload, '', '&');
}
public function unpack(string $payload): ?array
{
if (empty($payload) || !Str::contains($payload, '=')) {
return [];
}
$result = [];
foreach (explode('&', $payload) as $item) {
$pos = strpos($item, '=');
$result[substr($item, 0, $pos)] = substr($item, $pos + 1);
}
return $result;
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Packer;
use Yansongda\Pay\Contract\PackerInterface;
class XmlPacker implements PackerInterface
{
public function pack(array $payload): string
{
$xml = '<xml>';
foreach ($payload as $key => $val) {
$xml .= is_numeric($val) ? '<'.$key.'>'.$val.'</'.$key.'>' :
'<'.$key.'><![CDATA['.$val.']]></'.$key.'>';
}
$xml .= '</xml>';
return $xml;
}
public function unpack(string $payload): ?array
{
if (empty($payload)) {
return [];
}
if (PHP_VERSION_ID < 80000) {
libxml_disable_entity_loader();
}
$data = json_decode(json_encode(
simplexml_load_string($payload, 'SimpleXMLElement', LIBXML_NOCDATA),
JSON_UNESCAPED_UNICODE
), true);
if (JSON_ERROR_NONE === json_last_error()) {
return $data;
}
return null;
}
}

View File

@@ -0,0 +1,267 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay;
use Closure;
use Illuminate\Container\Container as LaravelContainer;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Throwable;
use Yansongda\Pay\Contract\DirectionInterface;
use Yansongda\Pay\Contract\PackerInterface;
use Yansongda\Pay\Contract\ServiceProviderInterface;
use Yansongda\Pay\Direction\CollectionDirection;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\ContainerNotFoundException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Pay\Packer\JsonPacker;
use Yansongda\Pay\Provider\Alipay;
use Yansongda\Pay\Provider\Unipay;
use Yansongda\Pay\Provider\Wechat;
use Yansongda\Pay\Service\AlipayServiceProvider;
use Yansongda\Pay\Service\ConfigServiceProvider;
use Yansongda\Pay\Service\ContainerServiceProvider;
use Yansongda\Pay\Service\EventServiceProvider;
use Yansongda\Pay\Service\HttpServiceProvider;
use Yansongda\Pay\Service\LoggerServiceProvider;
use Yansongda\Pay\Service\UnipayServiceProvider;
use Yansongda\Pay\Service\WechatServiceProvider;
/**
* @method static Alipay alipay(array $config = [], $container = null)
* @method static Wechat wechat(array $config = [], $container = null)
* @method static Unipay unipay(array $config = [], $container = null)
*/
class Pay
{
/**
* 正常模式.
*/
public const MODE_NORMAL = 0;
/**
* 沙箱模式.
*/
public const MODE_SANDBOX = 1;
/**
* 服务商模式.
*/
public const MODE_SERVICE = 2;
/**
* @var string[]
*/
protected array $service = [
AlipayServiceProvider::class,
WechatServiceProvider::class,
UnipayServiceProvider::class,
];
/**
* @var string[]
*/
private array $coreService = [
ContainerServiceProvider::class,
ConfigServiceProvider::class,
LoggerServiceProvider::class,
EventServiceProvider::class,
HttpServiceProvider::class,
];
/**
* @var null|Closure|ContainerInterface
*/
private static $container;
/**
* @param null|Closure|ContainerInterface $container
*
* @throws ContainerException
*/
private function __construct(array $config, $container = null)
{
$this->registerServices($config, $container);
Pay::set(DirectionInterface::class, CollectionDirection::class);
Pay::set(PackerInterface::class, JsonPacker::class);
}
/**
* @return mixed
*
* @throws ContainerException
* @throws ServiceNotFoundException
*/
public static function __callStatic(string $service, array $config)
{
if (!empty($config)) {
self::config(...$config);
}
return self::get($service);
}
/**
* @param null|Closure|ContainerInterface $container
*
* @throws ContainerException
*/
public static function config(array $config = [], $container = null): bool
{
if (self::hasContainer() && !($config['_force'] ?? false)) {
return false;
}
new self($config, $container);
return true;
}
/**
* @codeCoverageIgnore
*
* @param mixed $value
*
* @throws ContainerException
*/
public static function set(string $name, $value): void
{
try {
$container = Pay::getContainer();
if ($container instanceof LaravelContainer) {
$container->singleton($name, $value instanceof Closure ? $value : static fn () => $value);
return;
}
if (method_exists($container, 'set')) {
$container->set(...func_get_args());
return;
}
} catch (ContainerNotFoundException $e) {
throw $e;
} catch (Throwable $e) {
throw new ContainerException($e->getMessage());
}
throw new ContainerException('Current container does NOT support `set` method');
}
/**
* @codeCoverageIgnore
*
* @return mixed
*
* @throws ContainerException
*/
public static function make(string $service, array $parameters = [])
{
try {
$container = Pay::getContainer();
if (method_exists($container, 'make')) {
return $container->make(...func_get_args());
}
} catch (ContainerNotFoundException $e) {
throw $e;
} catch (Throwable $e) {
throw new ContainerException($e->getMessage());
}
$parameters = array_values($parameters);
return new $service(...$parameters);
}
/**
* @return mixed
*
* @throws ServiceNotFoundException
* @throws ContainerException
*/
public static function get(string $service)
{
try {
return Pay::getContainer()->get($service);
} catch (NotFoundExceptionInterface $e) {
throw new ServiceNotFoundException($e->getMessage());
} catch (ContainerNotFoundException $e) {
throw $e;
} catch (Throwable $e) {
throw new ContainerException($e->getMessage());
}
}
/**
* @throws ContainerNotFoundException
*/
public static function has(string $service): bool
{
return Pay::getContainer()->has($service);
}
/**
* @param null|Closure|ContainerInterface $container
*/
public static function setContainer($container): void
{
self::$container = $container;
}
/**
* @throws ContainerNotFoundException
*/
public static function getContainer(): ContainerInterface
{
if (self::$container instanceof ContainerInterface) {
return self::$container;
}
if (self::$container instanceof Closure) {
return (self::$container)();
}
throw new ContainerNotFoundException('`getContainer()` failed! Maybe you should `setContainer()` first', Exception\Exception::CONTAINER_NOT_FOUND);
}
public static function hasContainer(): bool
{
return self::$container instanceof ContainerInterface || self::$container instanceof Closure;
}
public static function clear(): void
{
self::$container = null;
}
/**
* @param mixed $data
*
* @throws ContainerException
*/
public static function registerService(string $service, $data): void
{
$var = new $service();
if ($var instanceof ServiceProviderInterface) {
$var->register($data);
}
}
/**
* @param null|Closure|ContainerInterface $container
*
* @throws ContainerException
*/
private function registerServices(array $config, $container = null): void
{
foreach (array_merge($this->coreService, $this->service) as $service) {
self::registerService($service, ContainerServiceProvider::class == $service ? $container : $config);
}
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Direction\NoHttpRequestDirection;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\Exception;
use Yansongda\Pay\Exception\InvalidConfigException;
use Yansongda\Pay\Exception\InvalidResponseException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
use Yansongda\Supports\Collection;
use Yansongda\Supports\Str;
use function Yansongda\Pay\verify_alipay_sign;
class CallbackPlugin implements PluginInterface
{
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws ServiceNotFoundException
* @throws InvalidResponseException
*/
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][CallbackPlugin] 插件开始装载', ['rocket' => $rocket]);
$this->formatPayload($rocket);
$sign = $rocket->getParams()['sign'] ?? false;
if (!$sign) {
throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', $rocket->getParams());
}
verify_alipay_sign($rocket->getParams(), $this->getSignContent($rocket->getPayload()), $sign);
$rocket->setDirection(NoHttpRequestDirection::class)
->setDestination($rocket->getPayload())
;
Logger::info('[alipay][CallbackPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
protected function formatPayload(Rocket $rocket): void
{
$payload = (new Collection($rocket->getParams()))
->filter(fn ($v, $k) => '' !== $v && !is_null($v) && 'sign' != $k && 'sign_type' != $k && !Str::startsWith($k, '_'))
;
$rocket->setPayload($payload);
}
protected function getSignContent(Collection $payload): string
{
return $payload->sortKeys()->toString();
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Data;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
/**
* @see https://opendocs.alipay.com/open/02fkbl
*/
class BillDownloadUrlQueryPlugin extends GeneralPlugin
{
protected function getMethod(): string
{
return 'alipay.data.dataservice.bill.downloadurl.query';
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Data;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
/**
* @see https://opendocs.alipay.com/open/029p6g
*/
class BillEreceiptApplyPlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][BillEreceiptApplyPlugin] 插件开始装载', ['rocket' => $rocket]);
$rocket->mergePayload([
'method' => 'alipay.data.bill.ereceipt.apply',
'biz_content' => array_merge(
[
'type' => 'FUND_DETAIL',
],
$rocket->getParams(),
),
]);
Logger::info('[alipay][BillEreceiptApplyPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Data;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
/**
* @see https://opendocs.alipay.com/open/029i7e
*/
class BillEreceiptQueryPlugin extends GeneralPlugin
{
protected function getMethod(): string
{
return 'alipay.data.bill.ereceipt.query';
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Ebpp;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
/**
* @see https://opendocs.alipay.com/open/02hd36
*/
class PdeductBillStatusPlugin extends GeneralPlugin
{
protected function getMethod(): string
{
return 'alipay.ebpp.pdeduct.bill.pay.status';
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Ebpp;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
/**
* @see https://opendocs.alipay.com/open/02hd35
*/
class PdeductPayPlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][PdeductPayPlugin] 插件开始装载', ['rocket' => $rocket]);
$rocket->mergePayload([
'method' => 'alipay.ebpp.pdeduct.pay',
'biz_content' => array_merge(
[
'agent_channel' => 'PUBLICFORM',
],
$rocket->getParams(),
),
]);
Logger::info('[alipay][PdeductPayPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Ebpp;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
/**
* @see https://opendocs.alipay.com/open/02hd33
*/
class PdeductSignAddPlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][PdeductSignAddPlugin] 插件开始装载', ['rocket' => $rocket]);
$rocket->mergePayload([
'method' => 'alipay.ebpp.pdeduct.sign.add',
'biz_content' => array_merge(
[
'charge_inst' => 'CQCENTERELECTRIC',
'agent_channel' => 'PUBLICPLATFORM',
'deduct_prod_code' => 'INST_DIRECT_DEDUCT',
],
$rocket->getParams(),
),
]);
Logger::info('[alipay][PdeductSignAddPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Ebpp;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
/**
* @see https://opendocs.alipay.com/open/02hd34
*/
class PdeductSignCancelPlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][PdeductSignCancelPlugin] 插件开始装载', ['rocket' => $rocket]);
$rocket->mergePayload([
'method' => 'alipay.ebpp.pdeduct.sign.cancel',
'biz_content' => array_merge(
[
'agent_channel' => 'PUBLICPLATFORM',
],
$rocket->getParams(),
),
]);
Logger::info('[alipay][PdeductSignCancelPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Fund;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
/**
* @see https://opendocs.alipay.com/open/02byuq?scene=common
*/
class AccountQueryPlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][AccountQueryPlugin] 插件开始装载', ['rocket' => $rocket]);
$rocket->mergePayload([
'method' => 'alipay.fund.account.query',
'biz_content' => array_merge(
[
'product_code' => 'TRANS_ACCOUNT_NO_PWD',
],
$rocket->getParams(),
),
]);
Logger::info('[alipay][AccountQueryPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Fund;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
/**
* @see https://opendocs.alipay.com/open/02fkb9
*/
class AuthOrderFreezePlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][AuthOrderFreezePlugin] 插件开始装载', ['rocket' => $rocket]);
$rocket->mergePayload([
'method' => 'alipay.fund.auth.order.freeze',
'biz_content' => array_merge(
[
'product_code' => 'PRE_AUTH',
],
$rocket->getParams()
),
]);
Logger::info('[alipay][AuthOrderFreezePlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Fund;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
/**
* @see https://opendocs.alipay.com/open/02fkbc
*/
class AuthOrderUnfreezePlugin extends GeneralPlugin
{
protected function getMethod(): string
{
return 'alipay.fund.auth.order.unfreeze';
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Fund;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
/**
* @see https://opendocs.alipay.com/open/02byve
*/
class TransCommonQueryPlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][TransCommonQueryPlugin] 插件开始装载', ['rocket' => $rocket]);
$rocket->mergePayload([
'method' => 'alipay.fund.trans.common.query',
'biz_content' => array_merge(
[
'product_code' => 'TRANS_ACCOUNT_NO_PWD',
'biz_scene' => 'DIRECT_TRANSFER',
],
$rocket->getParams(),
),
]);
Logger::info('[alipay][TransCommonQueryPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Fund;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Direction\ResponseDirection;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
/**
* @see https://opendocs.alipay.com/open/03rbye
*/
class TransPagePayPlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][TransPagePayPlugin] 插件开始装载', ['rocket' => $rocket]);
$rocket->setDirection(ResponseDirection::class)
->mergePayload([
'method' => 'alipay.fund.trans.page.pay',
'biz_content' => $rocket->getParams(),
])
;
Logger::info('[alipay][TransPagePayPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Fund;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
class TransTobankTransferPlugin extends GeneralPlugin
{
protected function getMethod(): string
{
return 'alipay.fund.trans.tobank.transfer';
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Fund;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
/**
* @see https://opendocs.alipay.com/open/02byuo?scene=common
*/
class TransUniTransferPlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][TransUniTransferPlugin] 插件开始装载', ['rocket' => $rocket]);
$rocket->mergePayload([
'method' => 'alipay.fund.trans.uni.transfer',
'biz_content' => array_merge(
[
'biz_scene' => 'DIRECT_TRANSFER',
'product_code' => 'TRANS_ACCOUNT_NO_PWD',
],
$rocket->getParams(),
),
]);
Logger::info('[alipay][TransUniTransferPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
abstract class GeneralPlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][GeneralPlugin] 通用插件开始装载', ['rocket' => $rocket]);
$this->doSomethingBefore($rocket);
$rocket->mergePayload([
'method' => $this->getMethod(),
'biz_content' => $rocket->getParams(),
]);
Logger::info('[alipay][GeneralPlugin] 通用插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
protected function doSomethingBefore(Rocket $rocket): void
{
}
abstract protected function getMethod(): string;
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay;
use Closure;
use GuzzleHttp\Psr7\Response;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
use Yansongda\Supports\Collection;
class HtmlResponsePlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
/* @var Rocket $rocket */
$rocket = $next($rocket);
Logger::debug('[alipay][HtmlResponsePlugin] 插件开始装载', ['rocket' => $rocket]);
$radar = $rocket->getRadar();
$response = 'GET' === $radar->getMethod() ?
$this->buildRedirect($radar->getUri()->__toString(), $rocket->getPayload()) :
$this->buildHtml($radar->getUri()->__toString(), $rocket->getPayload());
$rocket->setDestination($response);
Logger::info('[alipay][HtmlResponsePlugin] 插件装载完毕', ['rocket' => $rocket]);
return $rocket;
}
protected function buildRedirect(string $endpoint, Collection $payload): Response
{
$url = $endpoint.(false === strpos($endpoint, '?') ? '?' : '&').$payload->query();
$content = sprintf(
'<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="refresh" content="0;url=\'%1$s\'" />
<title>Redirecting to %1$s</title>
</head>
<body>
Redirecting to %1$s.
</body>
</html>',
htmlspecialchars($url, ENT_QUOTES)
);
return new Response(302, ['Location' => $url], $content);
}
protected function buildHtml(string $endpoint, Collection $payload): Response
{
$sHtml = "<form id='alipay_submit' name='alipay_submit' action='".$endpoint."' method='POST'>";
foreach ($payload->all() as $key => $val) {
$val = str_replace("'", '&apos;', $val);
$sHtml .= "<input type='hidden' name='".$key."' value='".$val."'/>";
}
$sHtml .= "<input type='submit' value='ok' style='display:none;'></form>";
$sHtml .= "<script>document.forms['alipay_submit'].submit();</script>";
return new Response(200, [], $sHtml);
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\Exception;
use Yansongda\Pay\Exception\InvalidConfigException;
use Yansongda\Pay\Exception\InvalidResponseException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
use Yansongda\Supports\Collection;
use function Yansongda\Pay\should_do_http_request;
use function Yansongda\Pay\verify_alipay_sign;
class LaunchPlugin implements PluginInterface
{
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws InvalidResponseException
* @throws ServiceNotFoundException
*/
public function assembly(Rocket $rocket, Closure $next): Rocket
{
/* @var Rocket $rocket */
$rocket = $next($rocket);
Logger::debug('[alipay][LaunchPlugin] 插件开始装载', ['rocket' => $rocket]);
if (should_do_http_request($rocket->getDirection())) {
$response = Collection::wrap($rocket->getDestination());
$result = $response->get($this->getResultKey($rocket->getPayload()));
$this->verifySign($rocket->getParams(), $response, $result);
$rocket->setDestination(Collection::wrap($result));
}
Logger::info('[alipay][LaunchPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $rocket;
}
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws InvalidResponseException
* @throws ServiceNotFoundException
*/
protected function verifySign(array $params, Collection $response, ?array $result): void
{
$sign = $response->get('sign', '');
if ('' === $sign || is_null($result)) {
throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, 'Verify Alipay Response Sign Failed: sign is empty', $response);
}
verify_alipay_sign($params, json_encode($result, JSON_UNESCAPED_UNICODE), $sign);
}
protected function getResultKey(Collection $payload): string
{
$method = $payload->get('method');
return str_replace('.', '_', $method).'_response';
}
}

View File

@@ -0,0 +1,214 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay;
use Closure;
use Yansongda\Pay\Contract\ConfigInterface;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\Exception;
use Yansongda\Pay\Exception\InvalidConfigException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Pay;
use Yansongda\Pay\Rocket;
use function Yansongda\Pay\get_alipay_config;
use function Yansongda\Pay\get_tenant;
class PreparePlugin implements PluginInterface
{
/**
* @throws ContainerException
* @throws ServiceNotFoundException
* @throws InvalidConfigException
*/
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][PreparePlugin] 插件开始装载', ['rocket' => $rocket]);
$rocket->mergePayload($this->getPayload($rocket->getParams()));
Logger::info('[alipay][PreparePlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
/**
* @throws ContainerException
* @throws ServiceNotFoundException
* @throws InvalidConfigException
*/
protected function getPayload(array $params): array
{
$tenant = get_tenant($params);
$config = get_alipay_config($params);
$payload = [
'app_id' => $config['app_id'] ?? '',
'method' => '',
'format' => 'JSON',
'return_url' => $this->getReturnUrl($params, $config),
'charset' => 'utf-8',
'sign_type' => 'RSA2',
'sign' => '',
'timestamp' => date('Y-m-d H:i:s'),
'version' => '1.0',
'notify_url' => $this->getNotifyUrl($params, $config),
'app_auth_token' => $this->getAppAuthToken($params, $config),
'biz_content' => [],
];
if (!empty($config['app_cert_public_key']) && !empty($config['alipay_root_cert'])) {
$payload = array_merge($payload, ['app_cert_sn' => $this->getAppCertSn($tenant, $config), 'alipay_root_cert_sn' => $this->getAlipayRootCertSn($tenant, $config)]);
}
return $payload;
}
protected function getReturnUrl(array $params, array $config): string
{
if (!empty($params['_return_url'])) {
return $params['_return_url'];
}
return $config['return_url'] ?? '';
}
protected function getNotifyUrl(array $params, array $config): string
{
if (!empty($params['_notify_url'])) {
return $params['_notify_url'];
}
return $config['notify_url'] ?? '';
}
protected function getAppAuthToken(array $params, array $config): string
{
if (!empty($params['_app_auth_token'])) {
return $params['_app_auth_token'];
}
return $config['app_auth_token'] ?? '';
}
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws ServiceNotFoundException
*/
protected function getAppCertSn(string $tenant, array $config): string
{
if (!empty($config['app_public_cert_sn'])) {
return $config['app_public_cert_sn'];
}
$path = $config['app_public_cert_path'] ?? null;
if (is_null($path)) {
throw new InvalidConfigException(Exception::ALIPAY_CONFIG_ERROR, 'Missing Alipay Config -- [app_public_cert_path]');
}
$cert = file_get_contents($path);
$ssl = openssl_x509_parse($cert);
if (false === $ssl) {
throw new InvalidConfigException(Exception::ALIPAY_CONFIG_ERROR, 'Parse `app_public_cert_path` Error');
}
$result = $this->getCertSn($ssl['issuer'] ?? [], $ssl['serialNumber'] ?? '');
Pay::get(ConfigInterface::class)->set('alipay.'.$tenant.'.app_public_cert_sn', $result);
return $result;
}
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws ServiceNotFoundException
*/
protected function getAlipayRootCertSn(string $tenant, array $config): string
{
if (!empty($config['alipay_root_cert_sn'])) {
return $config['alipay_root_cert_sn'];
}
$path = $config['alipay_root_cert_path'] ?? null;
if (is_null($path)) {
throw new InvalidConfigException(Exception::ALIPAY_CONFIG_ERROR, 'Missing Alipay Config -- [alipay_root_cert_path]');
}
$sn = '';
$exploded = explode('-----END CERTIFICATE-----', file_get_contents($path));
foreach ($exploded as $cert) {
if (empty(trim($cert))) {
continue;
}
$ssl = openssl_x509_parse($cert.'-----END CERTIFICATE-----');
if (false === $ssl) {
throw new InvalidConfigException(Exception::ALIPAY_CONFIG_ERROR, 'Invalid alipay_root_cert');
}
$detail = $this->formatCert($ssl);
if ('sha1WithRSAEncryption' == $detail['signatureTypeLN'] || 'sha256WithRSAEncryption' == $detail['signatureTypeLN']) {
$sn .= $this->getCertSn($detail['issuer'], $detail['serialNumber']).'_';
}
}
$result = substr($sn, 0, -1);
Pay::get(ConfigInterface::class)->set('alipay.'.$tenant.'.alipay_root_cert_sn', $result);
return $result;
}
protected function getCertSn(array $issuer, string $serialNumber): string
{
return md5(
$this->array2string(array_reverse($issuer)).$serialNumber
);
}
protected function array2string(array $array): string
{
$string = [];
foreach ($array as $key => $value) {
$string[] = $key.'='.$value;
}
return implode(',', $string);
}
protected function formatCert(array $ssl): array
{
if (0 === strpos($ssl['serialNumber'] ?? '', '0x')) {
$ssl['serialNumber'] = $this->hex2dec($ssl['serialNumberHex'] ?? '');
}
return $ssl;
}
protected function hex2dec(string $hex): string
{
$dec = '0';
$len = strlen($hex);
for ($i = 1; $i <= $len; ++$i) {
$dec = bcadd(
$dec,
bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i), 0), 0),
0
);
}
return $dec;
}
}

View File

@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\Exception;
use Yansongda\Pay\Exception\InvalidConfigException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Pay;
use Yansongda\Pay\Provider\Alipay;
use Yansongda\Pay\Request;
use Yansongda\Pay\Rocket;
use Yansongda\Supports\Collection;
use Yansongda\Supports\Str;
use function Yansongda\Pay\get_alipay_config;
use function Yansongda\Pay\get_private_cert;
class RadarSignPlugin implements PluginInterface
{
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws ServiceNotFoundException
*/
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][RadarSignPlugin] 插件开始装载', ['rocket' => $rocket]);
$this->sign($rocket);
$this->reRadar($rocket);
Logger::info('[alipay][RadarSignPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws ServiceNotFoundException
*/
protected function sign(Rocket $rocket): void
{
$this->formatPayload($rocket);
$sign = $this->getSign($rocket);
$rocket->mergePayload(['sign' => $sign]);
}
/**
* @throws ContainerException
* @throws ServiceNotFoundException
*/
protected function reRadar(Rocket $rocket): void
{
$params = $rocket->getParams();
$rocket->setRadar(new Request(
$this->getMethod($params),
$this->getUrl($params),
$this->getHeaders(),
$this->getBody($rocket->getPayload()),
));
}
protected function formatPayload(Rocket $rocket): void
{
$payload = $rocket->getPayload()->filter(fn ($v, $k) => '' !== $v && !is_null($v) && 'sign' != $k);
$contents = array_filter($payload->get('biz_content', []), fn ($v, $k) => !Str::startsWith(strval($k), '_'), ARRAY_FILTER_USE_BOTH);
$rocket->setPayload(
$payload->merge(['biz_content' => json_encode($contents)])
);
}
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws ServiceNotFoundException
*/
protected function getSign(Rocket $rocket): string
{
$privateKey = $this->getPrivateKey($rocket->getParams());
$content = $rocket->getPayload()->sortKeys()->toString();
openssl_sign($content, $sign, $privateKey, OPENSSL_ALGO_SHA256);
return base64_encode($sign);
}
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws ServiceNotFoundException
*/
protected function getPrivateKey(array $params): string
{
$privateKey = get_alipay_config($params)['app_secret_cert'] ?? null;
if (is_null($privateKey)) {
throw new InvalidConfigException(Exception::ALIPAY_CONFIG_ERROR, 'Missing Alipay Config -- [app_secret_cert]');
}
return get_private_cert($privateKey);
}
protected function getMethod(array $params): string
{
return strtoupper($params['_method'] ?? 'POST');
}
/**
* @throws ContainerException
* @throws ServiceNotFoundException
*/
protected function getUrl(array $params): string
{
$config = get_alipay_config($params);
return Alipay::URL[$config['mode'] ?? Pay::MODE_NORMAL];
}
protected function getHeaders(): array
{
return [
'Content-Type' => 'application/x-www-form-urlencoded',
];
}
protected function getBody(Collection $payload): string
{
return $payload->query();
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Shortcut;
use Closure;
use GuzzleHttp\Psr7\Response;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Contract\ShortcutInterface;
use Yansongda\Pay\Plugin\Alipay\Trade\AppPayPlugin;
use Yansongda\Pay\Rocket;
use Yansongda\Supports\Arr;
use Yansongda\Supports\Collection;
class AppShortcut implements ShortcutInterface
{
public function getPlugins(array $params): array
{
return [
AppPayPlugin::class,
$this->buildResponse(),
];
}
protected function buildResponse(): PluginInterface
{
return new class() implements PluginInterface {
public function assembly(Rocket $rocket, Closure $next): Rocket
{
$rocket->setDestination(new Response());
/* @var Rocket $rocket */
$rocket = $next($rocket);
$response = $this->buildHtml($rocket->getPayload());
return $rocket->setDestination($response);
}
protected function buildHtml(Collection $payload): Response
{
return new Response(200, [], Arr::query($payload->all()));
}
};
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Shortcut;
use Yansongda\Pay\Contract\ShortcutInterface;
use Yansongda\Pay\Plugin\Alipay\Trade\CancelPlugin;
class CancelShortcut implements ShortcutInterface
{
public function getPlugins(array $params): array
{
return [
CancelPlugin::class,
];
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Shortcut;
use Yansongda\Pay\Contract\ShortcutInterface;
use Yansongda\Pay\Plugin\Alipay\Trade\ClosePlugin;
class CloseShortcut implements ShortcutInterface
{
public function getPlugins(array $params): array
{
return [
ClosePlugin::class,
];
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Shortcut;
use Yansongda\Pay\Contract\ShortcutInterface;
use Yansongda\Pay\Plugin\Alipay\Trade\CreatePlugin;
class MiniShortcut implements ShortcutInterface
{
public function getPlugins(array $params): array
{
return [
CreatePlugin::class,
];
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Shortcut;
use Yansongda\Pay\Contract\ShortcutInterface;
use Yansongda\Pay\Plugin\Alipay\Trade\PayPlugin;
class PosShortcut implements ShortcutInterface
{
public function getPlugins(array $params): array
{
return [
PayPlugin::class,
];
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Shortcut;
use Yansongda\Pay\Contract\ShortcutInterface;
use Yansongda\Pay\Exception\Exception;
use Yansongda\Pay\Exception\InvalidParamsException;
use Yansongda\Pay\Plugin\Alipay\Fund\TransCommonQueryPlugin;
use Yansongda\Pay\Plugin\Alipay\Trade\FastRefundQueryPlugin;
use Yansongda\Pay\Plugin\Alipay\Trade\QueryPlugin;
use Yansongda\Supports\Str;
class QueryShortcut implements ShortcutInterface
{
/**
* @throws InvalidParamsException
*/
public function getPlugins(array $params): array
{
$typeMethod = Str::camel($params['_action'] ?? 'default').'Plugins';
if (isset($params['out_request_no'])) {
return $this->refundPlugins();
}
if (method_exists($this, $typeMethod)) {
return $this->{$typeMethod}();
}
throw new InvalidParamsException(Exception::SHORTCUT_MULTI_ACTION_ERROR, "Query action [{$typeMethod}] not supported");
}
protected function defaultPlugins(): array
{
return [
QueryPlugin::class,
];
}
protected function refundPlugins(): array
{
return [
FastRefundQueryPlugin::class,
];
}
protected function transferPlugins(): array
{
return [
TransCommonQueryPlugin::class,
];
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Shortcut;
use Yansongda\Pay\Contract\ShortcutInterface;
use Yansongda\Pay\Plugin\Alipay\Trade\RefundPlugin;
class RefundShortcut implements ShortcutInterface
{
public function getPlugins(array $params): array
{
return [
RefundPlugin::class,
];
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Shortcut;
use Yansongda\Pay\Contract\ShortcutInterface;
use Yansongda\Pay\Plugin\Alipay\Trade\PreCreatePlugin;
class ScanShortcut implements ShortcutInterface
{
public function getPlugins(array $params): array
{
return [
PreCreatePlugin::class,
];
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Shortcut;
use Yansongda\Pay\Contract\ShortcutInterface;
use Yansongda\Pay\Plugin\Alipay\Fund\TransUniTransferPlugin;
class TransferShortcut implements ShortcutInterface
{
public function getPlugins(array $params): array
{
return [
TransUniTransferPlugin::class,
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Shortcut;
use Yansongda\Pay\Contract\ShortcutInterface;
use Yansongda\Pay\Plugin\Alipay\HtmlResponsePlugin;
use Yansongda\Pay\Plugin\Alipay\Trade\WapPayPlugin;
class WapShortcut implements ShortcutInterface
{
public function getPlugins(array $params): array
{
return [
WapPayPlugin::class,
HtmlResponsePlugin::class,
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Shortcut;
use Yansongda\Pay\Contract\ShortcutInterface;
use Yansongda\Pay\Plugin\Alipay\HtmlResponsePlugin;
use Yansongda\Pay\Plugin\Alipay\Trade\PagePayPlugin;
class WebShortcut implements ShortcutInterface
{
public function getPlugins(array $params): array
{
return [
PagePayPlugin::class,
HtmlResponsePlugin::class,
];
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Tools;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
/**
* @see https://opendocs.alipay.com/isv/03l9c0
*/
class OpenAuthTokenAppPlugin extends GeneralPlugin
{
protected function getMethod(): string
{
return 'alipay.open.auth.token.app';
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Tools;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
/**
* @see https://opendocs.alipay.com/isv/03l8ca
*/
class OpenAuthTokenAppQueryPlugin extends GeneralPlugin
{
protected function getMethod(): string
{
return 'alipay.open.auth.token.app.query';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Tools;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
use Yansongda\Pay\Rocket;
/**
* @see https://opendocs.alipay.com/open/02ailc
*/
class SystemOauthTokenPlugin extends GeneralPlugin
{
protected function doSomethingBefore(Rocket $rocket): void
{
$rocket->mergePayload($rocket->getParams());
}
protected function getMethod(): string
{
return 'alipay.system.oauth.token';
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Trade;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Direction\ResponseDirection;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
use Yansongda\Pay\Traits\SupportServiceProviderTrait;
/**
* @see https://opendocs.alipay.com/open/02e7gq?scene=common
*/
class AppPayPlugin implements PluginInterface
{
use SupportServiceProviderTrait;
/**
* @throws ContainerException
* @throws ServiceNotFoundException
*/
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][AppPayPlugin] 插件开始装载', ['rocket' => $rocket]);
$this->loadAlipayServiceProvider($rocket);
$rocket->setDirection(ResponseDirection::class)
->mergePayload([
'method' => 'alipay.trade.app.pay',
'biz_content' => array_merge(
['product_code' => 'QUICK_MSECURITY_PAY'],
$rocket->getParams(),
),
])
;
Logger::info('[alipay][AppPayPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Trade;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
/**
* @see https://opendocs.alipay.com/open/02ekfi
*/
class CancelPlugin extends GeneralPlugin
{
protected function getMethod(): string
{
return 'alipay.trade.cancel';
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Trade;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
/**
* @see https://opendocs.alipay.com/open/02o6e7
*/
class ClosePlugin extends GeneralPlugin
{
protected function getMethod(): string
{
return 'alipay.trade.close';
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Trade;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
/**
* @see https://opendocs.alipay.com/open/02ekfj
*/
class CreatePlugin extends GeneralPlugin
{
protected function getMethod(): string
{
return 'alipay.trade.create';
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Trade;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
/**
* @see https://opendocs.alipay.com/open/028sma
*/
class FastRefundQueryPlugin extends GeneralPlugin
{
protected function getMethod(): string
{
return 'alipay.trade.fastpay.refund.query';
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Trade;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
class OrderPlugin extends GeneralPlugin
{
protected function getMethod(): string
{
return 'alipay.trade.order.pay';
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Trade;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
/**
* @see https://opendocs.alipay.com/open/028xqz
*/
class OrderSettlePlugin extends GeneralPlugin
{
protected function getMethod(): string
{
return 'alipay.trade.order.settle';
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Trade;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Direction\ResponseDirection;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
use Yansongda\Pay\Traits\SupportServiceProviderTrait;
/**
* @see https://opendocs.alipay.com/open/028r8t?scene=22
*/
class PagePayPlugin implements PluginInterface
{
use SupportServiceProviderTrait;
/**
* @throws ContainerException
* @throws ServiceNotFoundException
*/
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][PagePayPlugin] 插件开始装载', ['rocket' => $rocket]);
$this->loadAlipayServiceProvider($rocket);
$rocket->setDirection(ResponseDirection::class)
->mergePayload([
'method' => 'alipay.trade.page.pay',
'biz_content' => array_merge(
['product_code' => 'FAST_INSTANT_TRADE_PAY'],
$rocket->getParams()
),
])
;
Logger::info('[alipay][PagePayPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Trade;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Direction\ResponseDirection;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
class PageRefundPlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][PageRefundPlugin] 插件开始装载', ['rocket' => $rocket]);
$rocket->setDirection(ResponseDirection::class)
->mergePayload([
'method' => 'alipay.trade.page.refund',
'biz_content' => $rocket->getParams(),
])
;
Logger::info('[alipay][PageRefundPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Trade;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
use Yansongda\Pay\Traits\SupportServiceProviderTrait;
/**
* @see https://opendocs.alipay.com/open/02fkat?ref=api&scene=common
*/
class PayPlugin implements PluginInterface
{
use SupportServiceProviderTrait;
/**
* @throws ContainerException
* @throws ServiceNotFoundException
*/
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][PayPlugin] 插件开始装载', ['rocket' => $rocket]);
$this->loadAlipayServiceProvider($rocket);
$rocket->mergePayload([
'method' => 'alipay.trade.pay',
'biz_content' => array_merge(
[
'product_code' => 'FACE_TO_FACE_PAYMENT',
'scene' => 'bar_code',
],
$rocket->getParams(),
),
]);
Logger::info('[alipay][PayPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Trade;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
use Yansongda\Pay\Rocket;
use Yansongda\Pay\Traits\SupportServiceProviderTrait;
/**
* @see https://opendocs.alipay.com/open/02ekfg?scene=common
*/
class PreCreatePlugin extends GeneralPlugin
{
use SupportServiceProviderTrait;
/**
* @throws ContainerException
* @throws ServiceNotFoundException
*/
protected function doSomethingBefore(Rocket $rocket): void
{
$this->loadAlipayServiceProvider($rocket);
}
protected function getMethod(): string
{
return 'alipay.trade.precreate';
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Trade;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
/**
* @see https://opendocs.alipay.com/open/02ekfh?scene=common
*/
class QueryPlugin extends GeneralPlugin
{
protected function getMethod(): string
{
return 'alipay.trade.query';
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Trade;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
/**
* @see https://opendocs.alipay.com/open/02ekfk
*/
class RefundPlugin extends GeneralPlugin
{
protected function getMethod(): string
{
return 'alipay.trade.refund';
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\Trade;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Direction\ResponseDirection;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
use Yansongda\Pay\Traits\SupportServiceProviderTrait;
/**
* @see https://opendocs.alipay.com/open/02ivbs?scene=common
*/
class WapPayPlugin implements PluginInterface
{
use SupportServiceProviderTrait;
/**
* @throws ContainerException
* @throws ServiceNotFoundException
*/
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][WapPayPlugin] 插件开始装载', ['rocket' => $rocket]);
$this->loadAlipayServiceProvider($rocket);
$rocket->setDirection(ResponseDirection::class)
->mergePayload([
'method' => 'alipay.trade.wap.pay',
'biz_content' => array_merge(
[
'product_code' => 'QUICK_WAP_PAY',
],
$rocket->getParams(),
),
])
;
Logger::info('[alipay][WapPayPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\User;
use Yansongda\Pay\Plugin\Alipay\GeneralPlugin;
/**
* @see https://opendocs.alipay.com/open/02fkaq?ref=api
*/
class AgreementExecutionPlanModifyPlugin extends GeneralPlugin
{
protected function getMethod(): string
{
return 'alipay.user.agreement.executionplan.modify';
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\User;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Direction\ResponseDirection;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
/**
* @see https://opendocs.alipay.com/open/02fkan?ref=api&scene=35
*/
class AgreementPageSignPlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][AgreementPageSignPlugin] 插件开始装载', ['rocket' => $rocket]);
$rocket->setDirection(ResponseDirection::class)
->mergePayload([
'method' => 'alipay.user.agreement.page.sign',
'biz_content' => array_merge(
['product_code' => 'CYCLE_PAY_AUTH'],
$rocket->getParams()
),
])
;
Logger::info('[alipay][AgreementPageSignPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\User;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
/**
* @see https://opendocs.alipay.com/open/02fkao?ref=api&scene=8837b4183390497f84bb53783b488ecc
*/
class AgreementQueryPlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][AgreementQueryPlugin] 插件开始装载', ['rocket' => $rocket]);
$rocket->mergePayload([
'method' => 'alipay.user.agreement.query',
'biz_content' => array_merge(
['personal_product_code' => 'CYCLE_PAY_AUTH_P'],
$rocket->getParams()
),
]);
Logger::info('[alipay][AgreementQueryPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\User;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
/**
* @see https://opendocs.alipay.com/open/02fkar?ref=api
*/
class AgreementTransferPlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][AgreementTransferPlugin] 插件开始装载', ['rocket' => $rocket]);
$rocket->mergePayload([
'method' => 'alipay.user.agreement.transfer',
'biz_content' => array_merge(
['target_product_code' => 'CYCLE_PAY_AUTH_P'],
$rocket->getParams()
),
]);
Logger::info('[alipay][AgreementTransferPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\User;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
/**
* @see https://opendocs.alipay.com/open/02fkap?ref=api&scene=90766afb41f74df6ae1676e89625ebac
*/
class AgreementUnsignPlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][AgreementUnsignPlugin] 插件开始装载', ['rocket' => $rocket]);
$rocket->mergePayload([
'method' => 'alipay.user.agreement.unsign',
'biz_content' => array_merge(
['personal_product_code' => 'CYCLE_PAY_AUTH_P'],
$rocket->getParams()
),
]);
Logger::info('[alipay][AgreementUnsignPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Alipay\User;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
/**
* @see https://opendocs.alipay.com/open/02aild
*/
class InfoSharePlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[alipay][InfoSharePlugin] 插件开始装载', ['rocket' => $rocket]);
$rocket->mergePayload([
'method' => 'alipay.user.info.share',
'auth_token' => $rocket->getParams()['auth_token'] ?? '',
]);
Logger::info('[alipay][InfoSharePlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin;
use Closure;
use Psr\Http\Message\ResponseInterface;
use Yansongda\Pay\Contract\DirectionInterface;
use Yansongda\Pay\Contract\PackerInterface;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\Exception;
use Yansongda\Pay\Exception\InvalidConfigException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Pay\Pay;
use Yansongda\Pay\Rocket;
class ParserPlugin implements PluginInterface
{
/**
* @throws ServiceNotFoundException
* @throws ContainerException
* @throws InvalidConfigException
*/
public function assembly(Rocket $rocket, Closure $next): Rocket
{
/* @var Rocket $rocket */
$rocket = $next($rocket);
/* @var ResponseInterface $response */
$response = $rocket->getDestination();
return $rocket->setDestination(
$this->getDirection($rocket)->parse($this->getPacker($rocket), $response)
);
}
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws ServiceNotFoundException
*/
protected function getDirection(Rocket $rocket): DirectionInterface
{
$packer = Pay::get($rocket->getDirection());
$packer = is_string($packer) ? Pay::get($packer) : $packer;
if (!$packer instanceof DirectionInterface) {
throw new InvalidConfigException(Exception::INVALID_PARSER);
}
return $packer;
}
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws ServiceNotFoundException
*/
protected function getPacker(Rocket $rocket): PackerInterface
{
$packer = Pay::get($rocket->getPacker());
$packer = is_string($packer) ? Pay::get($packer) : $packer;
if (!$packer instanceof PackerInterface) {
throw new InvalidConfigException(Exception::INVALID_PACKER);
}
return $packer;
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Unipay;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Direction\NoHttpRequestDirection;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\Exception;
use Yansongda\Pay\Exception\InvalidConfigException;
use Yansongda\Pay\Exception\InvalidResponseException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
use Yansongda\Supports\Collection;
use Yansongda\Supports\Str;
use function Yansongda\Pay\verify_unipay_sign;
class CallbackPlugin implements PluginInterface
{
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws ServiceNotFoundException
* @throws InvalidResponseException
*/
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[unipay][CallbackPlugin] 插件开始装载', ['rocket' => $rocket]);
$this->formatPayload($rocket);
$params = $rocket->getParams();
$signature = $params['signature'] ?? false;
if (!$signature) {
throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', $params);
}
verify_unipay_sign($params, $rocket->getPayload()->sortKeys()->toString(), $signature);
$rocket->setDirection(NoHttpRequestDirection::class)
->setDestination($rocket->getPayload())
;
Logger::info('[unipay][CallbackPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
protected function formatPayload(Rocket $rocket): void
{
$payload = (new Collection($rocket->getParams()))
->filter(fn ($v, $k) => 'signature' != $k && !Str::startsWith($k, '_'))
;
$rocket->setPayload($payload);
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Unipay;
use Closure;
use Psr\Http\Message\RequestInterface;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Pay;
use Yansongda\Pay\Provider\Unipay;
use Yansongda\Pay\Request;
use Yansongda\Pay\Rocket;
use function Yansongda\Pay\get_unipay_config;
abstract class GeneralPlugin implements PluginInterface
{
/**
* @throws ServiceNotFoundException
* @throws ContainerException
*/
public function assembly(Rocket $rocket, Closure $next): Rocket
{
Logger::debug('[unipay][GeneralPlugin] 通用插件开始装载', ['rocket' => $rocket]);
$rocket->setRadar($this->getRequest($rocket));
$this->doSomething($rocket);
Logger::info('[unipay][GeneralPlugin] 通用插件装载完毕', ['rocket' => $rocket]);
return $next($rocket);
}
/**
* @throws ContainerException
* @throws ServiceNotFoundException
*/
protected function getRequest(Rocket $rocket): RequestInterface
{
return new Request(
$this->getMethod(),
$this->getUrl($rocket),
$this->getHeaders(),
);
}
protected function getMethod(): string
{
return 'POST';
}
/**
* @throws ContainerException
* @throws ServiceNotFoundException
*/
protected function getUrl(Rocket $rocket): string
{
$url = $this->getUri($rocket);
if (0 === strpos($url, 'http')) {
return $url;
}
$config = get_unipay_config($rocket->getParams());
return Unipay::URL[$config['mode'] ?? Pay::MODE_NORMAL].$url;
}
protected function getHeaders(): array
{
return [
'User-Agent' => 'yansongda/pay-v3',
'Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8',
];
}
abstract protected function doSomething(Rocket $rocket): void;
abstract protected function getUri(Rocket $rocket): string;
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Unipay;
use Closure;
use GuzzleHttp\Psr7\Response;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
use Yansongda\Supports\Collection;
class HtmlResponsePlugin implements PluginInterface
{
public function assembly(Rocket $rocket, Closure $next): Rocket
{
/* @var Rocket $rocket */
$rocket = $next($rocket);
Logger::debug('[unipay][HtmlResponsePlugin] 插件开始装载', ['rocket' => $rocket]);
$radar = $rocket->getRadar();
$response = $this->buildHtml($radar->getUri()->__toString(), $rocket->getPayload());
$rocket->setDestination($response);
Logger::info('[unipay][HtmlResponsePlugin] 插件装载完毕', ['rocket' => $rocket]);
return $rocket;
}
protected function buildHtml(string $endpoint, Collection $payload): Response
{
$sHtml = "<form id='pay_form' name='pay_form' action='".$endpoint."' method='POST'>";
foreach ($payload->all() as $key => $val) {
$sHtml .= "<input type='hidden' name='".$key."' value='".$val."'/>";
}
$sHtml .= "<input type='submit' value='ok' style='display:none;'></form>";
$sHtml .= "<script>document.forms['pay_form'].submit();</script>";
return new Response(200, [], $sHtml);
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Unipay;
use Closure;
use Yansongda\Pay\Contract\PluginInterface;
use Yansongda\Pay\Exception\ContainerException;
use Yansongda\Pay\Exception\InvalidConfigException;
use Yansongda\Pay\Exception\InvalidResponseException;
use Yansongda\Pay\Exception\ServiceNotFoundException;
use Yansongda\Pay\Logger;
use Yansongda\Pay\Rocket;
use Yansongda\Supports\Collection;
use function Yansongda\Pay\should_do_http_request;
use function Yansongda\Pay\verify_unipay_sign;
class LaunchPlugin implements PluginInterface
{
/**
* @throws ContainerException
* @throws InvalidConfigException
* @throws InvalidResponseException
* @throws ServiceNotFoundException
*/
public function assembly(Rocket $rocket, Closure $next): Rocket
{
/* @var Rocket $rocket */
$rocket = $next($rocket);
Logger::debug('[unipay][LaunchPlugin] 插件开始装载', ['rocket' => $rocket]);
if (should_do_http_request($rocket->getDirection())) {
$response = Collection::wrap($rocket->getDestination());
$signature = $response->get('signature');
$response->forget('signature');
verify_unipay_sign(
$rocket->getParams(),
$response->sortKeys()->toString(),
$signature
);
$rocket->setDestination($response);
}
Logger::info('[unipay][LaunchPlugin] 插件装载完毕', ['rocket' => $rocket]);
return $rocket;
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Unipay\OnlineGateway;
use Yansongda\Pay\Plugin\Unipay\GeneralPlugin;
use Yansongda\Pay\Rocket;
/**
* @see https://open.unionpay.com/tjweb/acproduct/APIList?acpAPIId=755&apiservId=448&version=V2.2&bussType=0
*/
class CancelPlugin extends GeneralPlugin
{
protected function getUri(Rocket $rocket): string
{
return 'gateway/api/backTransReq.do';
}
protected function doSomething(Rocket $rocket): void
{
$rocket->mergePayload([
'bizType' => '000000',
'txnType' => '31',
'txnSubType' => '00',
'channelType' => '07',
]);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Unipay\OnlineGateway;
use Yansongda\Pay\Direction\ResponseDirection;
use Yansongda\Pay\Plugin\Unipay\GeneralPlugin;
use Yansongda\Pay\Rocket;
/**
* @see https://open.unionpay.com/tjweb/acproduct/APIList?acpAPIId=754&apiservId=448&version=V2.2&bussType=0
*/
class PagePayPlugin extends GeneralPlugin
{
protected function getUri(Rocket $rocket): string
{
return 'gateway/api/frontTransReq.do';
}
protected function doSomething(Rocket $rocket): void
{
$rocket->setDirection(ResponseDirection::class)
->mergePayload([
'bizType' => '000201',
'txnType' => '01',
'txnSubType' => '01',
'channelType' => '07',
])
;
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Yansongda\Pay\Plugin\Unipay\OnlineGateway;
use Yansongda\Pay\Plugin\Unipay\GeneralPlugin;
use Yansongda\Pay\Rocket;
/**
* @see https://open.unionpay.com/tjweb/acproduct/APIList?acpAPIId=757&apiservId=448&version=V2.2&bussType=0
*/
class QueryPlugin extends GeneralPlugin
{
protected function getUri(Rocket $rocket): string
{
return 'gateway/api/queryTrans.do';
}
protected function doSomething(Rocket $rocket): void
{
$rocket->mergePayload([
'bizType' => '000000',
'txnType' => '00',
'txnSubType' => '00',
]);
}
}

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