初始化仓库
This commit is contained in:
112
vendor/w7corp/easywechat/src/Pay/Application.php
vendored
Normal file
112
vendor/w7corp/easywechat/src/Pay/Application.php
vendored
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
use EasyWeChat\Kernel\Contracts\Config as ConfigInterface;
|
||||
use EasyWeChat\Kernel\Contracts\Server as ServerInterface;
|
||||
use EasyWeChat\Kernel\Support\PrivateKey;
|
||||
use EasyWeChat\Kernel\Support\PublicKey;
|
||||
use EasyWeChat\Kernel\Traits\InteractWithConfig;
|
||||
use EasyWeChat\Kernel\Traits\InteractWithHttpClient;
|
||||
use EasyWeChat\Kernel\Traits\InteractWithServerRequest;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class Application implements \EasyWeChat\Pay\Contracts\Application
|
||||
{
|
||||
use InteractWithConfig;
|
||||
use InteractWithHttpClient;
|
||||
use InteractWithServerRequest;
|
||||
|
||||
protected ?ServerInterface $server = null;
|
||||
|
||||
protected ?HttpClientInterface $client = null;
|
||||
|
||||
protected ?Merchant $merchant = null;
|
||||
|
||||
/**
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function getUtils(): Utils
|
||||
{
|
||||
return new Utils($this->getMerchant());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function getMerchant(): Merchant
|
||||
{
|
||||
if (! $this->merchant) {
|
||||
$this->merchant = new Merchant(
|
||||
mchId: $this->config['mch_id'], /** @phpstan-ignore-line */
|
||||
privateKey: new PrivateKey((string) $this->config['private_key']), /** @phpstan-ignore-line */
|
||||
certificate: new PublicKey((string) $this->config['certificate']), /** @phpstan-ignore-line */
|
||||
secretKey: (string) $this->config['secret_key'], /** @phpstan-ignore-line */
|
||||
v2SecretKey: (string) $this->config['v2_secret_key'], /** @phpstan-ignore-line */
|
||||
platformCerts: $this->config->has('platform_certs') ? (array) $this->config['platform_certs'] : [],/** @phpstan-ignore-line */
|
||||
);
|
||||
}
|
||||
|
||||
return $this->merchant;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \ReflectionException
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function getServer(): Server|ServerInterface
|
||||
{
|
||||
if (! $this->server) {
|
||||
$this->server = new Server(
|
||||
merchant: $this->getMerchant(),
|
||||
request: $this->getRequest(),
|
||||
);
|
||||
}
|
||||
|
||||
return $this->server;
|
||||
}
|
||||
|
||||
public function setServer(ServerInterface $server): static
|
||||
{
|
||||
$this->server = $server;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setConfig(ConfigInterface $config): static
|
||||
{
|
||||
$this->config = $config;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getConfig(): ConfigInterface
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function getClient(): HttpClientInterface
|
||||
{
|
||||
return $this->client ?? $this->client = (new Client(
|
||||
$this->getMerchant(),
|
||||
$this->getHttpClient(),
|
||||
(array) $this->config->get('http', [])
|
||||
))->setPresets($this->config->all());
|
||||
}
|
||||
|
||||
public function setClient(HttpClientInterface $client): static
|
||||
{
|
||||
$this->client = $client;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
209
vendor/w7corp/easywechat/src/Pay/Client.php
vendored
Normal file
209
vendor/w7corp/easywechat/src/Pay/Client.php
vendored
Normal file
@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
|
||||
use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
|
||||
use EasyWeChat\Kernel\HttpClient\HttpClientMethods;
|
||||
use EasyWeChat\Kernel\HttpClient\RequestUtil;
|
||||
use EasyWeChat\Kernel\HttpClient\RequestWithPresets;
|
||||
use EasyWeChat\Kernel\HttpClient\Response;
|
||||
use EasyWeChat\Kernel\Support\PrivateKey;
|
||||
use EasyWeChat\Kernel\Support\PublicKey;
|
||||
use EasyWeChat\Kernel\Support\UserAgent;
|
||||
use EasyWeChat\Kernel\Support\Xml;
|
||||
use EasyWeChat\Kernel\Traits\MockableHttpClient;
|
||||
use Exception;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use Mockery;
|
||||
use Mockery\Mock;
|
||||
use Nyholm\Psr7\Uri;
|
||||
use function str_starts_with;
|
||||
use Symfony\Component\HttpClient\DecoratorTrait;
|
||||
use Symfony\Component\HttpClient\HttpClient as SymfonyHttpClient;
|
||||
use Symfony\Component\HttpClient\HttpClientTrait;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
/**
|
||||
* @method ResponseInterface get(string $uri, array $options = [])
|
||||
* @method ResponseInterface post(string $uri, array $options = [])
|
||||
* @method ResponseInterface put(string $uri, array $options = [])
|
||||
* @method ResponseInterface patch(string $uri, array $options = [])
|
||||
* @method ResponseInterface delete(string $uri, array $options = [])
|
||||
* @method HttpClientInterface withMchId(string $value = null)
|
||||
* @method HttpClientInterface withMchIdAs(string $key)
|
||||
*/
|
||||
class Client implements HttpClientInterface
|
||||
{
|
||||
use DecoratorTrait {
|
||||
DecoratorTrait::withOptions insteadof HttpClientTrait;
|
||||
}
|
||||
use HttpClientTrait;
|
||||
use HttpClientMethods;
|
||||
use MockableHttpClient;
|
||||
use RequestWithPresets;
|
||||
|
||||
/**
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $defaultOptions = [
|
||||
'base_uri' => 'https://api.mch.weixin.qq.com/',
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
];
|
||||
|
||||
public const V3_URI_PREFIXES = [
|
||||
'/v3/',
|
||||
'/sandbox/v3/',
|
||||
'/hk/v3/',
|
||||
'/global/v3/',
|
||||
];
|
||||
|
||||
protected bool $throw = true;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $defaultOptions
|
||||
*/
|
||||
public function __construct(
|
||||
protected Merchant $merchant,
|
||||
?HttpClientInterface $client = null,
|
||||
array $defaultOptions = []
|
||||
) {
|
||||
$this->throw = (bool) ($defaultOptions['throw'] ?? true);
|
||||
|
||||
$this->defaultOptions = array_merge(self::OPTIONS_DEFAULTS, $this->defaultOptions);
|
||||
|
||||
if (! empty($defaultOptions)) {
|
||||
$defaultOptions = RequestUtil::formatDefaultOptions($this->defaultOptions);
|
||||
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
|
||||
}
|
||||
|
||||
$this->client = ($client ?? SymfonyHttpClient::create())->withOptions($this->defaultOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array|mixed> $options
|
||||
*
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws Exception
|
||||
*/
|
||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
if (empty($options['headers'])) {
|
||||
$options['headers'] = [];
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
$options['headers']['User-Agent'] = UserAgent::create();
|
||||
|
||||
if ($this->isV3Request($url)) {
|
||||
[, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);
|
||||
$options['headers']['Authorization'] = $this->createSignature($method, $url, $options);
|
||||
} else {
|
||||
// v2 全部为 xml 请求
|
||||
if (! empty($options['xml'])) {
|
||||
if (is_array($options['xml'])) {
|
||||
$options['xml'] = Xml::build($this->attachLegacySignature($options['xml']));
|
||||
}
|
||||
|
||||
if (! is_string($options['xml'])) {
|
||||
throw new \InvalidArgumentException('The `xml` option must be a string or array.');
|
||||
}
|
||||
|
||||
$options['body'] = $options['xml'];
|
||||
unset($options['xml']);
|
||||
}
|
||||
|
||||
if (! empty($options['body']) && is_array($options['body'])) {
|
||||
$options['body'] = Xml::build($this->attachLegacySignature($options['body']));
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
if (! isset($options['headers']['Content-Type']) && ! isset($options['headers']['content-type'])) {
|
||||
$options['headers']['Content-Type'] = 'text/xml'; /** @phpstan-ignore-line */
|
||||
}
|
||||
}
|
||||
|
||||
// 合并通过 withHeader 和 withHeaders 设置的信息
|
||||
if (! empty($this->prependHeaders)) {
|
||||
$options['headers'] = array_merge($this->prependHeaders, $options['headers'] ?? []);
|
||||
}
|
||||
|
||||
return new Response($this->client->request($method, $url, $options), throw: $this->throw);
|
||||
}
|
||||
|
||||
protected function isV3Request(string $url): bool
|
||||
{
|
||||
$uri = new Uri($url);
|
||||
|
||||
foreach (self::V3_URI_PREFIXES as $prefix) {
|
||||
if (str_starts_with('/'.ltrim($uri->getPath(), '/'), $prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $arguments
|
||||
*/
|
||||
public function __call(string $name, array $arguments): mixed
|
||||
{
|
||||
if (\str_starts_with($name, 'with')) {
|
||||
return $this->handleMagicWithCall($name, $arguments[0] ?? null);
|
||||
}
|
||||
|
||||
return $this->client->$name(...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function createSignature(string $method, string $url, array $options): string
|
||||
{
|
||||
return (new Signature($this->merchant))->createHeader($method, $url, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $body
|
||||
* @return array<string, mixed>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function attachLegacySignature(array $body): array
|
||||
{
|
||||
return (new LegacySignature($this->merchant))->sign($body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
* @throws InvalidConfigException
|
||||
*/
|
||||
public static function createMockClient(MockHttpClient $mockHttpClient): HttpClientInterface|Mock
|
||||
{
|
||||
$mockMerchant = new Merchant(
|
||||
'mch_id',
|
||||
/** @phpstan-ignore-next-line */
|
||||
Mockery::mock(PrivateKey::class),
|
||||
/** @phpstan-ignore-next-line */
|
||||
Mockery::mock(PublicKey::class),
|
||||
'mock-v3-key',
|
||||
'mock-v2-key',
|
||||
);
|
||||
|
||||
return Mockery::mock(static::class, [$mockMerchant, $mockHttpClient])
|
||||
->shouldAllowMockingProtectedMethods()
|
||||
->makePartial();
|
||||
}
|
||||
}
|
||||
18
vendor/w7corp/easywechat/src/Pay/Config.php
vendored
Normal file
18
vendor/w7corp/easywechat/src/Pay/Config.php
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
class Config extends \EasyWeChat\Kernel\Config
|
||||
{
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $requiredKeys = [
|
||||
'mch_id',
|
||||
'secret_key',
|
||||
'private_key',
|
||||
'certificate',
|
||||
];
|
||||
}
|
||||
19
vendor/w7corp/easywechat/src/Pay/Contracts/Application.php
vendored
Normal file
19
vendor/w7corp/easywechat/src/Pay/Contracts/Application.php
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay\Contracts;
|
||||
|
||||
use EasyWeChat\Kernel\Contracts\Config;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
interface Application
|
||||
{
|
||||
public function getMerchant(): Merchant;
|
||||
|
||||
public function getConfig(): Config;
|
||||
|
||||
public function getHttpClient(): HttpClientInterface;
|
||||
|
||||
public function getClient(): HttpClientInterface;
|
||||
}
|
||||
23
vendor/w7corp/easywechat/src/Pay/Contracts/Merchant.php
vendored
Normal file
23
vendor/w7corp/easywechat/src/Pay/Contracts/Merchant.php
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay\Contracts;
|
||||
|
||||
use EasyWeChat\Kernel\Support\PrivateKey;
|
||||
use EasyWeChat\Kernel\Support\PublicKey;
|
||||
|
||||
interface Merchant
|
||||
{
|
||||
public function getMerchantId(): int;
|
||||
|
||||
public function getPrivateKey(): PrivateKey;
|
||||
|
||||
public function getSecretKey(): string;
|
||||
|
||||
public function getV2SecretKey(): ?string;
|
||||
|
||||
public function getCertificate(): PublicKey;
|
||||
|
||||
public function getPlatformCert(string $serial): ?PublicKey;
|
||||
}
|
||||
16
vendor/w7corp/easywechat/src/Pay/Contracts/ResponseValidator.php
vendored
Normal file
16
vendor/w7corp/easywechat/src/Pay/Contracts/ResponseValidator.php
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay\Contracts;
|
||||
|
||||
use EasyWeChat\Kernel\Exceptions\BadResponseException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
interface ResponseValidator
|
||||
{
|
||||
/**
|
||||
* @throws BadResponseException if the response is not successful.
|
||||
*/
|
||||
public function validate(ResponseInterface $response): void;
|
||||
}
|
||||
69
vendor/w7corp/easywechat/src/Pay/LegacySignature.php
vendored
Normal file
69
vendor/w7corp/easywechat/src/Pay/LegacySignature.php
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
use function call_user_func_array;
|
||||
use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
|
||||
use EasyWeChat\Kernel\Exceptions\RuntimeException;
|
||||
use EasyWeChat\Kernel\Support\Str;
|
||||
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;
|
||||
use function hash_hmac;
|
||||
use function http_build_query;
|
||||
use function is_string;
|
||||
use function strtoupper;
|
||||
use function urldecode;
|
||||
|
||||
class LegacySignature
|
||||
{
|
||||
public function __construct(protected MerchantInterface $merchant)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
* @return array<string, mixed>
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function sign(array $params): array
|
||||
{
|
||||
$nonce = Str::random();
|
||||
|
||||
$params = $attributes = array_filter(
|
||||
\array_merge(
|
||||
[
|
||||
'nonce_str' => $nonce,
|
||||
'sub_mch_id' => $params['sub_mch_id'] ?? null,
|
||||
'sub_appid' => $params['sub_appid'] ?? null,
|
||||
],
|
||||
$params
|
||||
)
|
||||
);
|
||||
|
||||
ksort($attributes);
|
||||
|
||||
$attributes['key'] = $this->merchant->getV2SecretKey();
|
||||
|
||||
if (empty($attributes['key'])) {
|
||||
throw new InvalidConfigException('Missing V2 API key.');
|
||||
}
|
||||
|
||||
if (! empty($params['sign_type']) && 'HMAC-SHA256' === $params['sign_type']) {
|
||||
$signType = fn (string $message): string => hash_hmac('sha256', $message, $attributes['key']);
|
||||
} else {
|
||||
$signType = 'md5';
|
||||
}
|
||||
|
||||
$sign = call_user_func_array($signType, [urldecode(http_build_query($attributes))]);
|
||||
|
||||
if (! is_string($sign)) {
|
||||
throw new RuntimeException('Failed to sign the request.');
|
||||
}
|
||||
|
||||
$params['sign'] = strtoupper($sign);
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
95
vendor/w7corp/easywechat/src/Pay/Merchant.php
vendored
Normal file
95
vendor/w7corp/easywechat/src/Pay/Merchant.php
vendored
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
use function array_is_list;
|
||||
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
|
||||
use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
|
||||
use EasyWeChat\Kernel\Support\PrivateKey;
|
||||
use EasyWeChat\Kernel\Support\PublicKey;
|
||||
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;
|
||||
use function intval;
|
||||
use function is_string;
|
||||
|
||||
class Merchant implements MerchantInterface
|
||||
{
|
||||
/**
|
||||
* @var array<string, PublicKey>
|
||||
*/
|
||||
protected array $platformCerts = [];
|
||||
|
||||
/**
|
||||
* @param array<int|string, string|PublicKey> $platformCerts
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @throws InvalidConfigException
|
||||
*/
|
||||
public function __construct(
|
||||
protected int|string $mchId,
|
||||
protected PrivateKey $privateKey,
|
||||
protected PublicKey $certificate,
|
||||
protected string $secretKey,
|
||||
protected ?string $v2SecretKey = null,
|
||||
array $platformCerts = [],
|
||||
) {
|
||||
$this->platformCerts = $this->normalizePlatformCerts($platformCerts);
|
||||
}
|
||||
|
||||
public function getMerchantId(): int
|
||||
{
|
||||
return intval($this->mchId);
|
||||
}
|
||||
|
||||
public function getPrivateKey(): PrivateKey
|
||||
{
|
||||
return $this->privateKey;
|
||||
}
|
||||
|
||||
public function getCertificate(): PublicKey
|
||||
{
|
||||
return $this->certificate;
|
||||
}
|
||||
|
||||
public function getSecretKey(): string
|
||||
{
|
||||
return $this->secretKey;
|
||||
}
|
||||
|
||||
public function getV2SecretKey(): ?string
|
||||
{
|
||||
return $this->v2SecretKey;
|
||||
}
|
||||
|
||||
public function getPlatformCert(string $serial): ?PublicKey
|
||||
{
|
||||
return $this->platformCerts[$serial] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, string|PublicKey> $platformCerts
|
||||
* @return array<string, PublicKey>
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @throws InvalidConfigException
|
||||
*/
|
||||
protected function normalizePlatformCerts(array $platformCerts): array
|
||||
{
|
||||
$certs = [];
|
||||
$isList = array_is_list($platformCerts);
|
||||
foreach ($platformCerts as $index => $publicKey) {
|
||||
if (is_string($publicKey)) {
|
||||
$publicKey = new PublicKey($publicKey);
|
||||
}
|
||||
|
||||
if (! $publicKey instanceof PublicKey) {
|
||||
throw new InvalidArgumentException('Invalid platform certficate.');
|
||||
}
|
||||
|
||||
$certs[$isList ? $publicKey->getSerialNo() : $index] = $publicKey;
|
||||
}
|
||||
|
||||
return $certs;
|
||||
}
|
||||
}
|
||||
35
vendor/w7corp/easywechat/src/Pay/Message.php
vendored
Normal file
35
vendor/w7corp/easywechat/src/Pay/Message.php
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use function json_decode;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @property string $trade_state
|
||||
*/
|
||||
class Message extends \EasyWeChat\Kernel\Message
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getOriginalAttributes(): array
|
||||
{
|
||||
$attributes = json_decode($this->getOriginalContents(), true);
|
||||
|
||||
return is_array($attributes) ? $attributes : [];
|
||||
}
|
||||
|
||||
public function getEventType(): ?string
|
||||
{
|
||||
$eventType = $this->getOriginalAttributes()['event_type'];
|
||||
|
||||
if (! is_string($eventType)) {
|
||||
throw new RuntimeException('Invalid event type.');
|
||||
}
|
||||
|
||||
return $eventType;
|
||||
}
|
||||
}
|
||||
78
vendor/w7corp/easywechat/src/Pay/ResponseValidator.php
vendored
Normal file
78
vendor/w7corp/easywechat/src/Pay/ResponseValidator.php
vendored
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
use function base64_decode;
|
||||
use EasyWeChat\Kernel\Exceptions\BadResponseException;
|
||||
use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
|
||||
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;
|
||||
use const OPENSSL_ALGO_SHA256;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use function strval;
|
||||
|
||||
class ResponseValidator implements \EasyWeChat\Pay\Contracts\ResponseValidator
|
||||
{
|
||||
public const MAX_ALLOWED_CLOCK_OFFSET = 300;
|
||||
|
||||
public const HEADER_TIMESTAMP = 'Wechatpay-Timestamp';
|
||||
|
||||
public const HEADER_NONCE = 'Wechatpay-Nonce';
|
||||
|
||||
public const HEADER_SERIAL = 'Wechatpay-Serial';
|
||||
|
||||
public const HEADER_SIGNATURE = 'Wechatpay-Signature';
|
||||
|
||||
public function __construct(protected MerchantInterface $merchant)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\BadResponseException
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function validate(ResponseInterface $response): void
|
||||
{
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
throw new BadResponseException('Request Failed');
|
||||
}
|
||||
|
||||
foreach ([self::HEADER_SIGNATURE, self::HEADER_TIMESTAMP, self::HEADER_SERIAL, self::HEADER_NONCE] as $header) {
|
||||
if (! $response->hasHeader($header)) {
|
||||
throw new BadResponseException("Missing Header: {$header}");
|
||||
}
|
||||
}
|
||||
|
||||
[$timestamp] = $response->getHeader(self::HEADER_TIMESTAMP);
|
||||
[$nonce] = $response->getHeader(self::HEADER_NONCE);
|
||||
[$serial] = $response->getHeader(self::HEADER_SERIAL);
|
||||
[$signature] = $response->getHeader(self::HEADER_SIGNATURE);
|
||||
|
||||
$body = (string) $response->getBody();
|
||||
|
||||
$message = "{$timestamp}\n{$nonce}\n{$body}\n";
|
||||
|
||||
if (\time() - \intval($timestamp) > self::MAX_ALLOWED_CLOCK_OFFSET) {
|
||||
throw new BadResponseException('Clock Offset Exceeded');
|
||||
}
|
||||
|
||||
$publicKey = $this->merchant->getPlatformCert($serial);
|
||||
|
||||
if (! $publicKey) {
|
||||
throw new InvalidConfigException(
|
||||
"No platform certs found for serial: {$serial},
|
||||
please download from wechat pay and set it in merchant config with key `certs`."
|
||||
);
|
||||
}
|
||||
|
||||
if (false === \openssl_verify(
|
||||
$message,
|
||||
base64_decode($signature),
|
||||
strval($publicKey),
|
||||
OPENSSL_ALGO_SHA256
|
||||
)) {
|
||||
throw new BadResponseException('Invalid Signature');
|
||||
}
|
||||
}
|
||||
}
|
||||
149
vendor/w7corp/easywechat/src/Pay/Server.php
vendored
Normal file
149
vendor/w7corp/easywechat/src/Pay/Server.php
vendored
Normal file
@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
use Closure;
|
||||
use EasyWeChat\Kernel\Contracts\Server as ServerInterface;
|
||||
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
|
||||
use EasyWeChat\Kernel\Exceptions\RuntimeException;
|
||||
use EasyWeChat\Kernel\HttpClient\RequestUtil;
|
||||
use EasyWeChat\Kernel\ServerResponse;
|
||||
use EasyWeChat\Kernel\Support\AesGcm;
|
||||
use EasyWeChat\Kernel\Traits\InteractWithHandlers;
|
||||
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;
|
||||
use Exception;
|
||||
use function is_array;
|
||||
use function json_decode;
|
||||
use function json_encode;
|
||||
use Nyholm\Psr7\Response;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use function strval;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @link https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
|
||||
* @link https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_5.shtml
|
||||
*/
|
||||
class Server implements ServerInterface
|
||||
{
|
||||
use InteractWithHandlers;
|
||||
|
||||
protected ServerRequestInterface $request;
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function __construct(
|
||||
protected MerchantInterface $merchant,
|
||||
?ServerRequestInterface $request,
|
||||
) {
|
||||
$this->request = $request ?? RequestUtil::createDefaultServerRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function serve(): ResponseInterface
|
||||
{
|
||||
$message = $this->getRequestMessage();
|
||||
|
||||
try {
|
||||
$defaultResponse = new Response(
|
||||
200,
|
||||
[],
|
||||
strval(json_encode(['code' => 'SUCCESS', 'message' => '成功'], JSON_UNESCAPED_UNICODE))
|
||||
);
|
||||
$response = $this->handle($defaultResponse, $message);
|
||||
|
||||
if (! ($response instanceof ResponseInterface)) {
|
||||
$response = $defaultResponse;
|
||||
}
|
||||
|
||||
return ServerResponse::make($response);
|
||||
} catch (Exception $e) {
|
||||
return new Response(
|
||||
500,
|
||||
[],
|
||||
strval(json_encode(['code' => 'ERROR', 'message' => $e->getMessage()], JSON_UNESCAPED_UNICODE))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_5.shtml
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function handlePaid(callable $handler): static
|
||||
{
|
||||
$this->with(function (Message $message, Closure $next) use ($handler): mixed {
|
||||
return $message->getEventType() === 'TRANSACTION.SUCCESS' && $message->trade_state === 'SUCCESS'
|
||||
? $handler($message, $next) : $next($message);
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_11.shtml
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function handleRefunded(callable $handler): static
|
||||
{
|
||||
$this->with(function (Message $message, Closure $next) use ($handler): mixed {
|
||||
return in_array($message->getEventType(), [
|
||||
'REFUND.SUCCESS',
|
||||
'REFUND.ABNORMAL',
|
||||
'REFUND.CLOSED',
|
||||
]) ? $handler($message, $next) : $next($message);
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function getRequestMessage(?ServerRequestInterface $request = null): \EasyWeChat\Kernel\Message|Message
|
||||
{
|
||||
$originContent = (string) ($request ?? $this->request)->getBody();
|
||||
$attributes = json_decode($originContent, true);
|
||||
|
||||
if (! is_array($attributes)) {
|
||||
throw new RuntimeException('Invalid request body.');
|
||||
}
|
||||
|
||||
if (empty($attributes['resource']['ciphertext'])) {
|
||||
throw new RuntimeException('Invalid request.');
|
||||
}
|
||||
|
||||
$attributes = json_decode(
|
||||
AesGcm::decrypt(
|
||||
$attributes['resource']['ciphertext'],
|
||||
$this->merchant->getSecretKey(),
|
||||
$attributes['resource']['nonce'],
|
||||
$attributes['resource']['associated_data'],
|
||||
),
|
||||
true
|
||||
);
|
||||
|
||||
if (! is_array($attributes)) {
|
||||
throw new RuntimeException('Failed to decrypt request message.');
|
||||
}
|
||||
|
||||
return new Message($attributes, $originContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function getDecryptedMessage(?ServerRequestInterface $request = null): \EasyWeChat\Kernel\Message|Message
|
||||
{
|
||||
return $this->getRequestMessage($request);
|
||||
}
|
||||
}
|
||||
68
vendor/w7corp/easywechat/src/Pay/Signature.php
vendored
Normal file
68
vendor/w7corp/easywechat/src/Pay/Signature.php
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
use function base64_encode;
|
||||
use EasyWeChat\Kernel\Support\Str;
|
||||
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;
|
||||
use Exception;
|
||||
use function http_build_query;
|
||||
use function ltrim;
|
||||
use Nyholm\Psr7\Uri;
|
||||
use function openssl_sign;
|
||||
use function parse_str;
|
||||
use function strtoupper;
|
||||
use function strval;
|
||||
use function time;
|
||||
|
||||
class Signature
|
||||
{
|
||||
public function __construct(protected MerchantInterface $merchant)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $options
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function createHeader(string $method, string $url, array $options): string
|
||||
{
|
||||
$uri = new Uri($url);
|
||||
|
||||
parse_str($uri->getQuery(), $query);
|
||||
$uri = $uri->withQuery(http_build_query(array_merge($query, (array) $options['query'])));
|
||||
|
||||
$body = '';
|
||||
$query = $uri->getQuery();
|
||||
$timestamp = time();
|
||||
$nonce = Str::random();
|
||||
$path = '/'.ltrim($uri->getPath().(empty($query) ? '' : '?'.$query), '/');
|
||||
|
||||
if (! empty($options['body'])) {
|
||||
$body = strval($options['body']);
|
||||
}
|
||||
|
||||
$message = strtoupper($method)."\n".
|
||||
$path."\n".
|
||||
$timestamp."\n".
|
||||
$nonce."\n".
|
||||
$body."\n";
|
||||
|
||||
openssl_sign($message, $signature, $this->merchant->getPrivateKey()->getKey(), 'sha256WithRSAEncryption');
|
||||
|
||||
return sprintf(
|
||||
'WECHATPAY2-SHA256-RSA2048 %s',
|
||||
sprintf(
|
||||
'mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
|
||||
$this->merchant->getMerchantId(),
|
||||
$nonce,
|
||||
$timestamp,
|
||||
$this->merchant->getCertificate()->getSerialNo(),
|
||||
base64_encode($signature)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
40
vendor/w7corp/easywechat/src/Pay/URLSchemeBuilder.php
vendored
Normal file
40
vendor/w7corp/easywechat/src/Pay/URLSchemeBuilder.php
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
use EasyWeChat\Kernel\Support\Str;
|
||||
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;
|
||||
use Exception;
|
||||
use function sprintf;
|
||||
|
||||
class URLSchemeBuilder
|
||||
{
|
||||
public function __construct(protected MerchantInterface $merchant)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function forProduct(string|int $productId, string $appId): string
|
||||
{
|
||||
$params = [
|
||||
'appid' => $appId,
|
||||
'mch_id' => $this->merchant->getMerchantId(),
|
||||
'time_stamp' => time(),
|
||||
'nonce_str' => Str::random(),
|
||||
'product_id' => $productId,
|
||||
];
|
||||
|
||||
$params['sign'] = (new LegacySignature($this->merchant))->sign($params);
|
||||
|
||||
return 'weixin://wxpay/bizpayurl?'.http_build_query($params);
|
||||
}
|
||||
|
||||
public function forCodeUrl(string $codeUrl): string
|
||||
{
|
||||
return sprintf('weixin://wxpay/bizpayurl?sr=%s', $codeUrl);
|
||||
}
|
||||
}
|
||||
179
vendor/w7corp/easywechat/src/Pay/Utils.php
vendored
Normal file
179
vendor/w7corp/easywechat/src/Pay/Utils.php
vendored
Normal file
@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
use function base64_encode;
|
||||
use function call_user_func_array;
|
||||
use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
|
||||
use EasyWeChat\Kernel\Support\Str;
|
||||
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;
|
||||
use Exception;
|
||||
use function http_build_query;
|
||||
use JetBrains\PhpStorm\ArrayShape;
|
||||
use function openssl_sign;
|
||||
use function strtoupper;
|
||||
use function time;
|
||||
use function urldecode;
|
||||
|
||||
class Utils
|
||||
{
|
||||
public function __construct(protected MerchantInterface $merchant)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter4_1_4.shtml
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
#[ArrayShape([
|
||||
'appId' => 'string',
|
||||
'timeStamp' => 'string',
|
||||
'nonceStr' => 'string',
|
||||
'package' => 'string',
|
||||
'signType' => 'string',
|
||||
'paySign' => 'string',
|
||||
])]
|
||||
public function buildBridgeConfig(string $prepayId, string $appId, string $signType = 'RSA'): array
|
||||
{
|
||||
$params = [
|
||||
'appId' => $appId,
|
||||
'timeStamp' => strval(time()),
|
||||
'nonceStr' => Str::random(),
|
||||
'package' => "prepay_id=$prepayId",
|
||||
'signType' => $signType,
|
||||
];
|
||||
|
||||
$message = $params['appId']."\n".
|
||||
$params['timeStamp']."\n".
|
||||
$params['nonceStr']."\n".
|
||||
$params['package']."\n";
|
||||
|
||||
// v2
|
||||
if ($signType != 'RSA') {
|
||||
$params['paySign'] = $this->createV2Signature($params);
|
||||
} else {
|
||||
// v3
|
||||
$params['paySign'] = $this->createSignature($message);
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#58
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
#[ArrayShape([
|
||||
'appId' => 'string',
|
||||
'nonceStr' => 'string',
|
||||
'package' => 'string',
|
||||
'signType' => 'string',
|
||||
'paySign' => 'string',
|
||||
'timestamp' => 'string',
|
||||
])]
|
||||
public function buildSdkConfig(string $prepayId, string $appId, string $signType = 'RSA'): array
|
||||
{
|
||||
$params = $this->buildBridgeConfig($prepayId, $appId, $signType);
|
||||
|
||||
$params['timestamp'] = $params['timeStamp'];
|
||||
unset($params['timeStamp']);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://developers.weixin.qq.com/miniprogram/dev/api/payment/wx.requestPayment.html
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
#[ArrayShape([
|
||||
'appId' => 'string',
|
||||
'timeStamp' => 'string',
|
||||
'nonceStr' => 'string',
|
||||
'package' => 'string',
|
||||
'signType' => 'string',
|
||||
'paySign' => 'string',
|
||||
])]
|
||||
public function buildMiniAppConfig(string $prepayId, string $appId, string $signType = 'RSA'): array
|
||||
{
|
||||
return $this->buildBridgeConfig($prepayId, $appId, $signType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter4_2_4.shtml
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
#[ArrayShape([
|
||||
'appid' => 'string',
|
||||
'partnerid' => 'int',
|
||||
'prepayid' => 'string',
|
||||
'noncestr' => 'string',
|
||||
'timestamp' => 'int',
|
||||
'package' => 'string',
|
||||
'sign' => 'string',
|
||||
])]
|
||||
public function buildAppConfig(string $prepayId, string $appId): array
|
||||
{
|
||||
$params = [
|
||||
'appid' => $appId,
|
||||
'partnerid' => $this->merchant->getMerchantId(),
|
||||
'prepayid' => $prepayId,
|
||||
'noncestr' => Str::random(),
|
||||
'timestamp' => time(),
|
||||
'package' => 'Sign=WXPay',
|
||||
];
|
||||
|
||||
$message = $params['appid']."\n".
|
||||
$params['timestamp']."\n".
|
||||
$params['noncestr']."\n".
|
||||
$params['prepayid']."\n";
|
||||
|
||||
$params['sign'] = $this->createSignature($message);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
protected function createSignature(string $message): string
|
||||
{
|
||||
openssl_sign($message, $signature, $this->merchant->getPrivateKey(), 'sha256WithRSAEncryption');
|
||||
|
||||
return base64_encode($signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidConfigException
|
||||
*/
|
||||
public function createV2Signature(array $params): string
|
||||
{
|
||||
$method = 'md5';
|
||||
$secretKey = $this->merchant->getV2SecretKey();
|
||||
|
||||
if (empty($secretKey)) {
|
||||
throw new InvalidConfigException('Missing v2 secret key.');
|
||||
}
|
||||
|
||||
if ('HMAC-SHA256' === $params['signType']) {
|
||||
$method = function ($str) use ($secretKey) {
|
||||
return hash_hmac('sha256', $str, $secretKey);
|
||||
};
|
||||
}
|
||||
|
||||
ksort($params);
|
||||
|
||||
$params['key'] = $secretKey;
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
return strtoupper((string) call_user_func_array($method, [urldecode(http_build_query($params))]));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user