- 框架初始化
 - 安装插件
 - 修复PHP8.4报错
This commit is contained in:
2025-04-19 17:21:20 +08:00
commit c6a4e1f5f6
5306 changed files with 967782 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
<?php
namespace Yansongda\Pay\Contracts;
use Symfony\Component\HttpFoundation\Response;
use Yansongda\Supports\Collection;
interface GatewayApplicationInterface
{
/**
* To pay.
*
* @author yansongda <me@yansonga.cn>
*
* @param string $gateway
* @param array $params
*
* @return Collection|Response
*/
public function pay($gateway, $params);
/**
* Query an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string|array $order
*
* @return Collection
*/
public function find($order, ?string $type);
/**
* Refund an order.
*
* @author yansongda <me@yansongda.cn>
*
* @return Collection
*/
public function refund(array $order);
/**
* Cancel an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string|array $order
*
* @return Collection
*/
public function cancel($order);
/**
* Close an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string|array $order
*
* @return Collection
*/
public function close($order);
/**
* Verify a request.
*
* @author yansongda <me@yansongda.cn>
*
* @param string|array|null $content
*
* @return Collection
*/
public function verify($content, bool $refund);
/**
* Echo success to server.
*
* @author yansongda <me@yansongda.cn>
*
* @return Response
*/
public function success();
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Yansongda\Pay\Contracts;
use Symfony\Component\HttpFoundation\Response;
use Yansongda\Supports\Collection;
interface GatewayInterface
{
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @return Collection|Response
*/
public function pay($endpoint, ?array $payload);
}

View File

@@ -0,0 +1,98 @@
<?php
namespace Yansongda\Pay;
use Exception;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* @author yansongda <me@yansongda.cn>
*
* @method static Event dispatch(Event $event) Dispatches an event to all registered listeners
* @method static array getListeners($eventName = null) Gets the listeners of a specific event or all listeners sorted by descending priority.
* @method static int|void getListenerPriority($eventName, $listener) Gets the listener priority for a specific event.
* @method static bool hasListeners($eventName = null) Checks whether an event has any registered listeners.
* @method static void addListener($eventName, $listener, $priority = 0) Adds an event listener that listens on the specified events.
* @method static removeListener($eventName, $listener) Removes an event listener from the specified events.
* @method static void addSubscriber(EventSubscriberInterface $subscriber) Adds an event subscriber.
* @method static void removeSubscriber(EventSubscriberInterface $subscriber)
*/
class Events
{
/**
* dispatcher.
*
* @var EventDispatcher
*/
protected static $dispatcher;
/**
* Forward call.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $method
* @param array $args
*
* @throws Exception
*
* @return mixed
*/
public static function __callStatic($method, $args)
{
return call_user_func_array([self::getDispatcher(), $method], $args);
}
/**
* Forward call.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $method
* @param array $args
*
* @throws Exception
*
* @return mixed
*/
public function __call($method, $args)
{
return call_user_func_array([self::getDispatcher(), $method], $args);
}
/**
* setDispatcher.
*
* @author yansongda <me@yansongda.cn>
*/
public static function setDispatcher(EventDispatcher $dispatcher)
{
self::$dispatcher = $dispatcher;
}
/**
* getDispatcher.
*
* @author yansongda <me@yansongda.cn>
*/
public static function getDispatcher(): EventDispatcher
{
if (self::$dispatcher) {
return self::$dispatcher;
}
return self::$dispatcher = self::createDispatcher();
}
/**
* createDispatcher.
*
* @author yansongda <me@yansongda.cn>
*/
public static function createDispatcher(): EventDispatcher
{
return new EventDispatcher();
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Yansongda\Pay\Events;
class ApiRequested extends Event
{
/**
* Endpoint.
*
* @var string
*/
public $endpoint;
/**
* Result.
*
* @var array
*/
public $result;
/**
* Bootstrap.
*/
public function __construct(string $driver, ?string $gateway, ?string $endpoint, ?array $result)
{
$this->endpoint = $endpoint;
$this->result = $result;
parent::__construct($driver, $gateway);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Yansongda\Pay\Events;
class ApiRequesting extends Event
{
/**
* Endpoint.
*
* @var string
*/
public $endpoint;
/**
* Payload.
*
* @var array
*/
public $payload;
/**
* Bootstrap.
*/
public function __construct(string $driver, ?string $gateway, ?string $endpoint, ?array $payload)
{
$this->endpoint = $endpoint;
$this->payload = $payload;
parent::__construct($driver, $gateway);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Yansongda\Pay\Events;
use Symfony\Contracts\EventDispatcher\Event as SymfonyEvent;
class Event extends SymfonyEvent
{
/**
* Driver.
*
* @var string
*/
public $driver;
/**
* Method.
*
* @var string
*/
public $gateway;
/**
* Extra attributes.
*
* @var mixed
*/
public $attributes;
/**
* Bootstrap.
*
* @author yansongda <me@yansongda.cn>
*/
public function __construct(string $driver, ?string $gateway)
{
$this->driver = $driver;
$this->gateway = $gateway;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Yansongda\Pay\Events;
class MethodCalled extends Event
{
/**
* endpoint.
*
* @var string
*/
public $endpoint;
/**
* payload.
*
* @var array
*/
public $payload;
/**
* Bootstrap.
*
* @author yansongda <me@yansongda.cn>
*/
public function __construct(string $driver, ?string $gateway, ?string $endpoint, ?array $payload = [])
{
$this->endpoint = $endpoint;
$this->payload = $payload;
parent::__construct($driver, $gateway);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Yansongda\Pay\Events;
class PayStarted extends Event
{
/**
* Endpoint.
*
* @var string
*/
public $endpoint;
/**
* Payload.
*
* @var array
*/
public $payload;
/**
* Bootstrap.
*/
public function __construct(string $driver, ?string $gateway, ?string $endpoint, ?array $payload)
{
$this->endpoint = $endpoint;
$this->payload = $payload;
parent::__construct($driver, $gateway);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Yansongda\Pay\Events;
class PayStarting extends Event
{
/**
* Params.
*
* @var array
*/
public $params;
/**
* Bootstrap.
*/
public function __construct(string $driver, ?string $gateway, ?array $params)
{
$this->params = $params;
parent::__construct($driver, $gateway);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Yansongda\Pay\Events;
class RequestReceived extends Event
{
/**
* Received data.
*
* @var array
*/
public $data;
/**
* Bootstrap.
*
* @author yansongda <me@yansongda.cn>
*/
public function __construct(string $driver, ?string $gateway, ?array $data)
{
$this->data = $data;
parent::__construct($driver, $gateway);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Yansongda\Pay\Events;
class SignFailed extends Event
{
/**
* Received data.
*
* @var array
*/
public $data;
/**
* Bootstrap.
*
* @author yansongda <me@yansongda.cn>
*/
public function __construct(string $driver, ?string $gateway, ?array $data)
{
$this->data = $data;
parent::__construct($driver, $gateway);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Yansongda\Pay\Exceptions;
class BusinessException extends GatewayException
{
/**
* Bootstrap.
*
* @author yansongda <me@yansonga.cn>
*
* @param string $message
* @param array|string $raw
*/
public function __construct($message, $raw = [])
{
parent::__construct('ERROR_BUSINESS: '.$message, $raw, self::ERROR_BUSINESS);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Yansongda\Pay\Exceptions;
class Exception extends \Exception
{
const UNKNOWN_ERROR = 9999;
const INVALID_GATEWAY = 1;
const INVALID_CONFIG = 2;
const INVALID_ARGUMENT = 3;
const ERROR_GATEWAY = 4;
const INVALID_SIGN = 5;
const ERROR_BUSINESS = 6;
/**
* Raw error info.
*
* @var array
*/
public $raw;
/**
* Bootstrap.
*
* @author yansongda <me@yansonga.cn>
*
* @param string $message
* @param array|string $raw
* @param int|string $code
*/
public function __construct($message = '', $raw = [], $code = self::UNKNOWN_ERROR)
{
$message = '' === $message ? 'Unknown Error' : $message;
$this->raw = is_array($raw) ? $raw : [$raw];
parent::__construct($message, intval($code));
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Yansongda\Pay\Exceptions;
class GatewayException extends Exception
{
/**
* Bootstrap.
*
* @author yansongda <me@yansonga.cn>
*
* @param string $message
* @param array|string $raw
* @param int $code
*/
public function __construct($message, $raw = [], $code = self::ERROR_GATEWAY)
{
parent::__construct('ERROR_GATEWAY: '.$message, $raw, $code);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Yansongda\Pay\Exceptions;
class InvalidArgumentException extends Exception
{
/**
* Bootstrap.
*
* @author yansongda <me@yansonga.cn>
*
* @param string $message
* @param array|string $raw
*/
public function __construct($message, $raw = [])
{
parent::__construct('INVALID_ARGUMENT: '.$message, $raw, self::INVALID_ARGUMENT);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Yansongda\Pay\Exceptions;
class InvalidConfigException extends Exception
{
/**
* Bootstrap.
*
* @author yansongda <me@yansonga.cn>
*
* @param string $message
* @param array|string $raw
*/
public function __construct($message, $raw = [])
{
parent::__construct('INVALID_CONFIG: '.$message, $raw, self::INVALID_CONFIG);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Yansongda\Pay\Exceptions;
class InvalidGatewayException extends Exception
{
/**
* Bootstrap.
*
* @author yansongda <me@yansonga.cn>
*
* @param string $message
* @param array|string $raw
*/
public function __construct($message, $raw = [])
{
parent::__construct('INVALID_GATEWAY: '.$message, $raw, self::INVALID_GATEWAY);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Yansongda\Pay\Exceptions;
class InvalidSignException extends Exception
{
/**
* Bootstrap.
*
* @author yansongda <me@yansonga.cn>
*
* @param string $message
* @param array|string $raw
*/
public function __construct($message, $raw = [])
{
parent::__construct('INVALID_SIGN: '.$message, $raw, self::INVALID_SIGN);
}
}

View File

@@ -0,0 +1,423 @@
<?php
namespace Yansongda\Pay\Gateways;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Yansongda\Pay\Contracts\GatewayApplicationInterface;
use Yansongda\Pay\Contracts\GatewayInterface;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidConfigException;
use Yansongda\Pay\Exceptions\InvalidGatewayException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Pay\Gateways\Alipay\Support;
use Yansongda\Supports\Collection;
use Yansongda\Supports\Config;
use Yansongda\Supports\Str;
/**
* @method Response app(array $config) APP 支付
* @method Collection pos(array $config) 刷卡支付
* @method Collection scan(array $config) 扫码支付
* @method Collection transfer(array $config) 帐户转账
* @method Response wap(array $config) 手机网站支付
* @method Response web(array $config) 电脑支付
* @method Collection mini(array $config) 小程序支付
*/
class Alipay implements GatewayApplicationInterface
{
/**
* Const mode_normal.
*/
const MODE_NORMAL = 'normal';
/**
* Const mode_dev.
*/
const MODE_DEV = 'dev';
/**
* Const mode_service.
*/
const MODE_SERVICE = 'service';
/**
* Const url.
*/
const URL = [
self::MODE_NORMAL => 'https://openapi.alipay.com/gateway.do?charset=utf-8',
self::MODE_SERVICE => 'https://openapi.alipay.com/gateway.do?charset=utf-8',
self::MODE_DEV => 'https://openapi-sandbox.dl.alipaydev.com/gateway.do?charset=utf-8',
];
/**
* Alipay payload.
*
* @var array
*/
protected $payload;
/**
* Alipay gateway.
*
* @var string
*/
protected $gateway;
/**
* extends.
*
* @var array
*/
protected $extends;
/**
* Bootstrap.
*
* @author yansongda <me@yansongda.cn>
*
* @throws \Exception
*/
public function __construct(Config $config)
{
$this->gateway = Support::create($config)->getBaseUri();
$this->payload = [
'app_id' => $config->get('app_id'),
'method' => '',
'format' => 'JSON',
'charset' => 'utf-8',
'sign_type' => 'RSA2',
'version' => '1.0',
'return_url' => $config->get('return_url'),
'notify_url' => $config->get('notify_url'),
'timestamp' => date('Y-m-d H:i:s'),
'sign' => '',
'biz_content' => '',
'app_auth_token' => $config->get('app_auth_token'),
];
if ($config->get('app_cert_public_key') && $config->get('alipay_root_cert')) {
$this->payload['app_cert_sn'] = Support::getCertSN($config->get('app_cert_public_key'));
$this->payload['alipay_root_cert_sn'] = Support::getRootCertSN($config->get('alipay_root_cert'));
}
}
/**
* Magic pay.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $method
* @param array $params
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws InvalidGatewayException
* @throws InvalidSignException
*
* @return Response|Collection
*/
public function __call($method, $params)
{
if (isset($this->extends[$method])) {
return $this->makeExtend($method, ...$params);
}
return $this->pay($method, ...$params);
}
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $gateway
* @param array $params
*
* @throws InvalidGatewayException
*
* @return Response|Collection
*/
public function pay($gateway, $params = [])
{
Events::dispatch(new Events\PayStarting('Alipay', $gateway, $params));
$this->payload['return_url'] = $params['return_url'] ?? $this->payload['return_url'];
$this->payload['notify_url'] = $params['notify_url'] ?? $this->payload['notify_url'];
unset($params['return_url'], $params['notify_url']);
$this->payload['biz_content'] = json_encode($params);
$gateway = get_class($this).'\\'.Str::studly($gateway).'Gateway';
if (class_exists($gateway)) {
return $this->makePay($gateway);
}
throw new InvalidGatewayException("Pay Gateway [{$gateway}] not exists");
}
/**
* Verify sign.
*
* @author yansongda <me@yansongda.cn>
*
* @param array|null $data
*
* @throws InvalidSignException
* @throws InvalidConfigException
*/
public function verify($data = null, bool $refund = false): Collection
{
if (is_null($data)) {
$request = Request::createFromGlobals();
$data = $request->request->count() > 0 ? $request->request->all() : $request->query->all();
}
if (isset($data['fund_bill_list'])) {
$data['fund_bill_list'] = htmlspecialchars_decode($data['fund_bill_list']);
}
Events::dispatch(new Events\RequestReceived('Alipay', '', $data));
if (Support::verifySign($data)) {
return new Collection($data);
}
Events::dispatch(new Events\SignFailed('Alipay', '', $data));
throw new InvalidSignException('Alipay Sign Verify FAILED', $data);
}
/**
* Query an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string|array $order
*
* @throws GatewayException
* @throws InvalidConfigException
* @throws InvalidSignException
*/
public function find($order, ?string $type = 'wap'): Collection
{
$gateway = get_class($this).'\\'.Str::studly($type).'Gateway';
if (!class_exists($gateway) || !is_callable([new $gateway(), 'find'])) {
throw new GatewayException("{$gateway} Done Not Exist Or Done Not Has FIND Method");
}
$config = call_user_func([new $gateway(), 'find'], $order);
$this->payload['method'] = $config['method'];
$this->payload['biz_content'] = $config['biz_content'];
$this->payload['sign'] = Support::generateSign($this->payload);
Events::dispatch(new Events\MethodCalled('Alipay', 'Find', $this->gateway, $this->payload));
return Support::requestApi($this->payload);
}
/**
* Refund an order.
*
* @author yansongda <me@yansongda.cn>
*
* @throws GatewayException
* @throws InvalidConfigException
* @throws InvalidSignException
*/
public function refund(array $order): Collection
{
$this->payload['method'] = 'alipay.trade.refund';
$this->payload['biz_content'] = json_encode($order);
$this->payload['sign'] = Support::generateSign($this->payload);
Events::dispatch(new Events\MethodCalled('Alipay', 'Refund', $this->gateway, $this->payload));
return Support::requestApi($this->payload);
}
/**
* Cancel an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param array|string $order
*
* @throws GatewayException
* @throws InvalidConfigException
* @throws InvalidSignException
*/
public function cancel($order): Collection
{
$this->payload['method'] = 'alipay.trade.cancel';
$this->payload['biz_content'] = json_encode(is_array($order) ? $order : ['out_trade_no' => $order]);
$this->payload['sign'] = Support::generateSign($this->payload);
Events::dispatch(new Events\MethodCalled('Alipay', 'Cancel', $this->gateway, $this->payload));
return Support::requestApi($this->payload);
}
/**
* Close an order.
*
* @param string|array $order
*
* @author yansongda <me@yansongda.cn>
*
* @throws GatewayException
* @throws InvalidConfigException
* @throws InvalidSignException
*/
public function close($order): Collection
{
$this->payload['method'] = 'alipay.trade.close';
$this->payload['biz_content'] = json_encode(is_array($order) ? $order : ['out_trade_no' => $order]);
$this->payload['sign'] = Support::generateSign($this->payload);
Events::dispatch(new Events\MethodCalled('Alipay', 'Close', $this->gateway, $this->payload));
return Support::requestApi($this->payload);
}
/**
* Download bill.
*
* @author yansongda <me@yansongda.cn>
*
* @param string|array $bill
*
* @throws GatewayException
* @throws InvalidConfigException
* @throws InvalidSignException
*/
public function download($bill): string
{
$this->payload['method'] = 'alipay.data.dataservice.bill.downloadurl.query';
$this->payload['biz_content'] = json_encode(is_array($bill) ? $bill : ['bill_type' => 'trade', 'bill_date' => $bill]);
$this->payload['sign'] = Support::generateSign($this->payload);
Events::dispatch(new Events\MethodCalled('Alipay', 'Download', $this->gateway, $this->payload));
$result = Support::requestApi($this->payload);
return ($result instanceof Collection) ? $result->get('bill_download_url') : '';
}
/**
* Reply success to alipay.
*
* @author yansongda <me@yansongda.cn>
*/
public function success(): Response
{
Events::dispatch(new Events\MethodCalled('Alipay', 'Success', $this->gateway));
return new Response('success');
}
/**
* extend.
*
* @author yansongda <me@yansongda.cn>
*
* @throws GatewayException
* @throws InvalidConfigException
* @throws InvalidSignException
* @throws InvalidArgumentException
*/
public function extend(string $method, callable $function, bool $now = true): ?Collection
{
if (!$now && !method_exists($this, $method)) {
$this->extends[$method] = $function;
return null;
}
$customize = $function($this->payload);
if (!is_array($customize) && !($customize instanceof Collection)) {
throw new InvalidArgumentException('Return Type Must Be Array Or Collection');
}
Events::dispatch(new Events\MethodCalled('Alipay', 'extend', $this->gateway, $customize));
if (is_array($customize)) {
$this->payload = $customize;
$this->payload['sign'] = Support::generateSign($this->payload);
return Support::requestApi($this->payload);
}
return $customize;
}
/**
* Make pay gateway.
*
* @author yansongda <me@yansongda.cn>
*
* @throws InvalidGatewayException
*
* @return Response|Collection
*/
protected function makePay(string $gateway)
{
$app = new $gateway();
if ($app instanceof GatewayInterface) {
return $app->pay($this->gateway, array_filter($this->payload, function ($value) {
return '' !== $value && !is_null($value);
}));
}
throw new InvalidGatewayException("Pay Gateway [{$gateway}] Must Be An Instance Of GatewayInterface");
}
/**
* makeExtend.
*
* @author yansongda <me@yansongda.cn>
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws InvalidSignException
*/
protected function makeExtend(string $method, array ...$params): Collection
{
$params = count($params) >= 1 ? $params[0] : $params;
$function = $this->extends[$method];
$customize = $function($this->payload, $params);
if (!is_array($customize) && !($customize instanceof Collection)) {
throw new InvalidArgumentException('Return Type Must Be Array Or Collection');
}
Events::dispatch(new Events\MethodCalled(
'Alipay',
'extend - '.$method,
$this->gateway,
is_array($customize) ? $customize : $customize->toArray()
));
if (is_array($customize)) {
$this->payload = $customize;
$this->payload['sign'] = Support::generateSign($this->payload);
return Support::requestApi($this->payload);
}
return $customize;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Yansongda\Pay\Gateways\Alipay;
use Symfony\Component\HttpFoundation\Response;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidConfigException;
use Yansongda\Pay\Gateways\Alipay;
class AppGateway extends Gateway
{
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @throws InvalidConfigException
* @throws InvalidArgumentException
*/
public function pay($endpoint, ?array $payload): Response
{
$payload['method'] = 'alipay.trade.app.pay';
$biz_array = json_decode($payload['biz_content'], true);
if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) {
$biz_array['extend_params'] = is_array($biz_array['extend_params']) ? array_merge(['sys_service_provider_id' => Support::getInstance()->pid], $biz_array['extend_params']) : ['sys_service_provider_id' => Support::getInstance()->pid];
}
$payload['biz_content'] = json_encode(array_merge($biz_array, ['product_code' => 'QUICK_MSECURITY_PAY']));
$payload['sign'] = Support::generateSign($payload);
Events::dispatch(new Events\PayStarted('Alipay', 'App', $endpoint, $payload));
return new Response(http_build_query($payload));
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Yansongda\Pay\Gateways\Alipay;
use Yansongda\Pay\Contracts\GatewayInterface;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Supports\Collection;
abstract class Gateway implements GatewayInterface
{
/**
* Mode.
*
* @var string
*/
protected $mode;
/**
* Bootstrap.
*
* @author yansongda <me@yansongda.cn>
*
* @throws InvalidArgumentException
*/
public function __construct()
{
$this->mode = Support::getInstance()->mode;
}
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @return Collection
*/
abstract public function pay($endpoint, ?array $payload);
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Yansongda\Pay\Gateways\Alipay;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidConfigException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Pay\Gateways\Alipay;
use Yansongda\Supports\Collection;
class MiniGateway extends Gateway
{
/**
* Pay an order.
*
* @author xiaozan <i@xiaozan.me>
*
* @param string $endpoint
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws InvalidSignException
*
* @see https://docs.alipay.com/mini/introduce/pay
*/
public function pay($endpoint, ?array $payload): Collection
{
$biz_array = json_decode($payload['biz_content'], true);
if (empty($biz_array['buyer_id']) && empty($biz_array['buyer_open_id'])) {
throw new InvalidArgumentException('buyer_id or buyer_open_id required');
}
if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) {
$biz_array['extend_params'] = is_array($biz_array['extend_params']) ? array_merge(['sys_service_provider_id' => Support::getInstance()->pid], $biz_array['extend_params']) : ['sys_service_provider_id' => Support::getInstance()->pid];
}
$payload['biz_content'] = json_encode($biz_array);
$payload['method'] = 'alipay.trade.create';
$payload['sign'] = Support::generateSign($payload);
Events::dispatch(new Events\PayStarted('Alipay', 'Mini', $endpoint, $payload));
return Support::requestApi($payload);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Yansongda\Pay\Gateways\Alipay;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidConfigException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Pay\Gateways\Alipay;
use Yansongda\Supports\Collection;
class PosGateway extends Gateway
{
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @throws InvalidArgumentException
* @throws GatewayException
* @throws InvalidConfigException
* @throws InvalidSignException
*/
public function pay($endpoint, ?array $payload): Collection
{
$payload['method'] = 'alipay.trade.pay';
$biz_array = json_decode($payload['biz_content'], true);
if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) {
$biz_array['extend_params'] = is_array($biz_array['extend_params']) ? array_merge(['sys_service_provider_id' => Support::getInstance()->pid], $biz_array['extend_params']) : ['sys_service_provider_id' => Support::getInstance()->pid];
}
$payload['biz_content'] = json_encode(array_merge(
$biz_array,
[
'product_code' => 'FACE_TO_FACE_PAYMENT',
'scene' => 'bar_code',
]
));
$payload['sign'] = Support::generateSign($payload);
Events::dispatch(new Events\PayStarted('Alipay', 'Pos', $endpoint, $payload));
return Support::requestApi($payload);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Yansongda\Pay\Gateways\Alipay;
class RefundGateway
{
/**
* Find.
*
* @author yansongda <me@yansongda.cn>
*
* @param $order
*/
public function find($order): array
{
return [
'method' => 'alipay.trade.fastpay.refund.query',
'biz_content' => json_encode(is_array($order) ? $order : ['out_trade_no' => $order]),
];
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Yansongda\Pay\Gateways\Alipay;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidConfigException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Pay\Gateways\Alipay;
use Yansongda\Supports\Collection;
class ScanGateway extends Gateway
{
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws InvalidSignException
*/
public function pay($endpoint, ?array $payload): Collection
{
$payload['method'] = 'alipay.trade.precreate';
$biz_array = json_decode($payload['biz_content'], true);
if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) {
$biz_array['extend_params'] = is_array($biz_array['extend_params']) ? array_merge(['sys_service_provider_id' => Support::getInstance()->pid], $biz_array['extend_params']) : ['sys_service_provider_id' => Support::getInstance()->pid];
}
$payload['biz_content'] = json_encode(array_merge($biz_array, ['product_code' => '']));
$payload['sign'] = Support::generateSign($payload);
Events::dispatch(new Events\PayStarted('Alipay', 'Scan', $endpoint, $payload));
return Support::requestApi($payload);
}
}

View File

@@ -0,0 +1,452 @@
<?php
namespace Yansongda\Pay\Gateways\Alipay;
use Exception;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidConfigException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Pay\Gateways\Alipay;
use Yansongda\Pay\Log;
use Yansongda\Supports\Arr;
use Yansongda\Supports\Collection;
use Yansongda\Supports\Config;
use Yansongda\Supports\Str;
use Yansongda\Supports\Traits\HasHttpRequest;
/**
* @author yansongda <me@yansongda.cn>
*
* @property string app_id alipay app_id
* @property string ali_public_key
* @property string private_key
* @property array http http options
* @property string mode current mode
* @property array log log options
* @property string pid ali pid
*/
class Support
{
use HasHttpRequest;
/**
* Alipay gateway.
*
* @var string
*/
protected $baseUri;
/**
* Config.
*
* @var Config
*/
protected $config;
/**
* Instance.
*
* @var Support
*/
private static $instance;
/**
* Bootstrap.
*
* @author yansongda <me@yansongda.cn>
*/
private function __construct(Config $config)
{
$this->baseUri = Alipay::URL[$config->get('mode', Alipay::MODE_NORMAL)];
$this->config = $config;
$this->setHttpOptions();
}
/**
* __get.
*
* @author yansongda <me@yansongda.cn>
*
* @param $key
*
* @return mixed|Config|null
*/
public function __get($key)
{
return $this->getConfig($key);
}
/**
* create.
*
* @author yansongda <me@yansongda.cn>
*
* @return Support
*/
public static function create(Config $config)
{
if ('cli' === php_sapi_name() || !(self::$instance instanceof self)) {
self::$instance = new self($config);
}
return self::$instance;
}
/**
* getInstance.
*
* @author yansongda <me@yansongda.cn>
*
* @throws InvalidArgumentException
*
* @return Support
*/
public static function getInstance()
{
if (is_null(self::$instance)) {
throw new InvalidArgumentException('You Should [Create] First Before Using');
}
return self::$instance;
}
/**
* clear.
*
* @author yansongda <me@yansongda.cn>
*/
public function clear()
{
self::$instance = null;
}
/**
* Get Alipay API result.
*
* @author yansongda <me@yansongda.cn>
*
* @throws GatewayException
* @throws InvalidConfigException
* @throws InvalidSignException
*/
public static function requestApi(array $data): Collection
{
Events::dispatch(new Events\ApiRequesting('Alipay', '', self::$instance->getBaseUri(), $data));
$data = array_filter($data, function ($value) {
return ('' == $value || is_null($value)) ? false : true;
});
$result = json_decode(self::$instance->post('', $data), true);
Events::dispatch(new Events\ApiRequested('Alipay', '', self::$instance->getBaseUri(), $result));
return self::processingApiResult($data, $result);
}
/**
* Generate sign.
*
* @author yansongda <me@yansongda.cn>
*
* @throws InvalidConfigException
*/
public static function generateSign(array $params): string
{
$privateKey = self::$instance->private_key;
if (is_null($privateKey)) {
throw new InvalidConfigException('Missing Alipay Config -- [private_key]');
}
if (Str::endsWith($privateKey, '.pem')) {
$privateKey = openssl_pkey_get_private(
Str::startsWith($privateKey, 'file://') ? $privateKey : 'file://'.$privateKey
);
} else {
$privateKey = "-----BEGIN RSA PRIVATE KEY-----\n".
wordwrap($privateKey, 64, "\n", true).
"\n-----END RSA PRIVATE KEY-----";
}
openssl_sign(self::getSignContent($params), $sign, $privateKey, OPENSSL_ALGO_SHA256);
$sign = base64_encode($sign);
Log::debug('Alipay Generate Sign', [$params, $sign]);
if (is_resource($privateKey)) {
openssl_free_key($privateKey);
}
return $sign;
}
/**
* Verify sign.
*
* @author yansongda <me@yansonga.cn>
*
* @param bool $sync
* @param string|null $sign
*
* @throws InvalidConfigException
*/
public static function verifySign(array $data, $sync = false, $sign = null): bool
{
$publicKey = self::$instance->ali_public_key;
if (is_null($publicKey)) {
throw new InvalidConfigException('Missing Alipay Config -- [ali_public_key]');
}
if (Str::endsWith($publicKey, '.crt')) {
$publicKey = file_get_contents($publicKey);
} elseif (Str::endsWith($publicKey, '.pem')) {
$publicKey = openssl_pkey_get_public(
Str::startsWith($publicKey, 'file://') ? $publicKey : 'file://'.$publicKey
);
} else {
$publicKey = "-----BEGIN PUBLIC KEY-----\n".
wordwrap($publicKey, 64, "\n", true).
"\n-----END PUBLIC KEY-----";
}
$sign = $sign ?? $data['sign'];
$toVerify = $sync ? json_encode($data, JSON_UNESCAPED_UNICODE) : self::getSignContent($data, true);
$isVerify = 1 === openssl_verify($toVerify, base64_decode($sign), $publicKey, OPENSSL_ALGO_SHA256);
if (is_resource($publicKey)) {
openssl_free_key($publicKey);
}
return $isVerify;
}
/**
* Get signContent that is to be signed.
*
* @author yansongda <me@yansongda.cn>
*
* @param bool $verify
*/
public static function getSignContent(array $data, $verify = false): string
{
ksort($data);
$stringToBeSigned = '';
foreach ($data as $k => $v) {
if ($verify && 'sign' != $k && 'sign_type' != $k) {
$stringToBeSigned .= $k.'='.$v.'&';
}
if (!$verify && '' !== $v && !is_null($v) && 'sign' != $k && '@' != substr($v, 0, 1)) {
$stringToBeSigned .= $k.'='.$v.'&';
}
}
Log::debug('Alipay Generate Sign Content Before Trim', [$data, $stringToBeSigned]);
return trim($stringToBeSigned, '&');
}
/**
* Convert encoding.
*
* @author yansongda <me@yansonga.cn>
*
* @param string|array $data
* @param string $to
* @param string $from
*/
public static function encoding($data, $to = 'utf-8', $from = 'gb2312'): array
{
return Arr::encoding((array) $data, $to, $from);
}
/**
* Get service config.
*
* @author yansongda <me@yansongda.cn>
*
* @param string|null $key
* @param mixed|null $default
*
* @return mixed|null
*/
public function getConfig($key = null, $default = null)
{
if (is_null($key)) {
return $this->config->all();
}
if ($this->config->has($key)) {
return $this->config[$key];
}
return $default;
}
/**
* Get Base Uri.
*
* @author yansongda <me@yansongda.cn>
*
* @return string
*/
public function getBaseUri()
{
return $this->baseUri;
}
/**
* 生成应用证书SN.
*
* @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html
*
* @param $certPath
*
* @throws /Exception
*/
public static function getCertSN($certPath): string
{
if (!is_file($certPath)) {
throw new Exception('unknown certPath -- [getCertSN]');
}
$x509data = file_get_contents($certPath);
if (false === $x509data) {
throw new Exception('Alipay CertSN Error -- [getCertSN]');
}
openssl_x509_read($x509data);
$certdata = openssl_x509_parse($x509data);
if (empty($certdata)) {
throw new Exception('Alipay openssl_x509_parse Error -- [getCertSN]');
}
$issuer_arr = [];
foreach ($certdata['issuer'] as $key => $val) {
$issuer_arr[] = $key.'='.$val;
}
$issuer = implode(',', array_reverse($issuer_arr));
Log::debug('getCertSN:', [$certPath, $issuer, $certdata['serialNumber']]);
return md5($issuer.$certdata['serialNumber']);
}
/**
* 生成支付宝根证书SN.
*
* @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html
*
* @param $certPath
*
* @return string
*
* @throws /Exception
*/
public static function getRootCertSN($certPath)
{
if (!is_file($certPath)) {
throw new Exception('unknown certPath -- [getRootCertSN]');
}
$x509data = file_get_contents($certPath);
if (false === $x509data) {
throw new Exception('Alipay CertSN Error -- [getRootCertSN]');
}
$kCertificateEnd = '-----END CERTIFICATE-----';
$certStrList = explode($kCertificateEnd, $x509data);
$md5_arr = [];
foreach ($certStrList as $one) {
if (!empty(trim($one))) {
$_x509data = $one.$kCertificateEnd;
openssl_x509_read($_x509data);
$_certdata = openssl_x509_parse($_x509data);
if (in_array($_certdata['signatureTypeSN'], ['RSA-SHA256', 'RSA-SHA1'])) {
$issuer_arr = [];
foreach ($_certdata['issuer'] as $key => $val) {
$issuer_arr[] = $key.'='.$val;
}
$_issuer = implode(',', array_reverse($issuer_arr));
if (0 === strpos($_certdata['serialNumber'], '0x')) {
$serialNumber = self::bchexdec($_certdata['serialNumber']);
} else {
$serialNumber = $_certdata['serialNumber'];
}
$md5_arr[] = md5($_issuer.$serialNumber);
Log::debug('getRootCertSN Sub:', [$certPath, $_issuer, $serialNumber]);
}
}
}
return implode('_', $md5_arr);
}
/**
* processingApiResult.
*
* @author yansongda <me@yansongda.cn>
*
* @param $data
* @param $result
*
* @throws GatewayException
* @throws InvalidConfigException
* @throws InvalidSignException
*/
protected static function processingApiResult($data, $result): Collection
{
$method = str_replace('.', '_', $data['method']).'_response';
if (!isset($result['sign']) || '10000' != $result[$method]['code']) {
throw new GatewayException('Get Alipay API Error:'.$result[$method]['msg'].(isset($result[$method]['sub_code']) ? (' - '.$result[$method]['sub_code']) : ''), $result);
}
if (self::verifySign($result[$method], true, $result['sign'])) {
return new Collection($result[$method]);
}
Events::dispatch(new Events\SignFailed('Alipay', '', $result));
throw new InvalidSignException('Alipay Sign Verify FAILED', $result);
}
/**
* Set Http options.
*
* @author yansongda <me@yansongda.cn>
*/
protected function setHttpOptions(): self
{
if ($this->config->has('http') && is_array($this->config->get('http'))) {
$this->config->forget('http.base_uri');
$this->httpOptions = $this->config->get('http');
}
return $this;
}
/**
* 0x转高精度数字.
*
* @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html
*
* @param $hex
*
* @return int|string
*/
private static function bchexdec($hex)
{
$dec = 0;
$len = strlen($hex);
for ($i = 1; $i <= $len; ++$i) {
if (ctype_xdigit($hex[$i - 1])) {
$dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
}
}
return str_replace('.00', '', $dec);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Yansongda\Pay\Gateways\Alipay;
use Yansongda\Pay\Contracts\GatewayInterface;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidConfigException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Supports\Collection;
class TransferGateway implements GatewayInterface
{
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @throws GatewayException
* @throws InvalidConfigException
* @throws InvalidSignException
*/
public function pay($endpoint, ?array $payload): Collection
{
$payload['method'] = 'alipay.fund.trans.uni.transfer';
$payload['sign'] = Support::generateSign($payload);
Events::dispatch(new Events\PayStarted('Alipay', 'Transfer', $endpoint, $payload));
return Support::requestApi($payload);
}
/**
* Find.
*
* @author yansongda <me@yansongda.cn>
*
* @param $order
*/
public function find($order): array
{
return [
'method' => 'alipay.fund.trans.order.query',
'biz_content' => json_encode(is_array($order) ? $order : ['out_biz_no' => $order]),
];
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Yansongda\Pay\Gateways\Alipay;
class WapGateway extends WebGateway
{
/**
* Get method config.
*
* @author yansongda <me@yansongda.cn>
*/
protected function getMethod(): string
{
return 'alipay.trade.wap.pay';
}
/**
* Get productCode config.
*
* @author yansongda <me@yansongda.cn>
*/
protected function getProductCode(): string
{
return 'QUICK_WAP_WAY';
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace Yansongda\Pay\Gateways\Alipay;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidConfigException;
use Yansongda\Pay\Gateways\Alipay;
class WebGateway extends Gateway
{
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @throws InvalidConfigException
* @throws InvalidArgumentException
*/
public function pay($endpoint, ?array $payload): Response
{
$biz_array = json_decode($payload['biz_content'], true);
$biz_array['product_code'] = $this->getProductCode();
$method = $biz_array['http_method'] ?? 'POST';
unset($biz_array['http_method']);
if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) {
$biz_array['extend_params'] = is_array($biz_array['extend_params']) ? array_merge(['sys_service_provider_id' => Support::getInstance()->pid], $biz_array['extend_params']) : ['sys_service_provider_id' => Support::getInstance()->pid];
}
$payload['method'] = $this->getMethod();
$payload['biz_content'] = json_encode($biz_array);
$payload['sign'] = Support::generateSign($payload);
Events::dispatch(new Events\PayStarted('Alipay', 'Web/Wap', $endpoint, $payload));
return $this->buildPayHtml($endpoint, $payload, $method);
}
/**
* Find.
*
* @author yansongda <me@yansongda.cn>
*
* @param $order
*/
public function find($order): array
{
return [
'method' => 'alipay.trade.query',
'biz_content' => json_encode(is_array($order) ? $order : ['out_trade_no' => $order]),
];
}
/**
* Build Html response.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
* @param array $payload
* @param string $method
*/
protected function buildPayHtml($endpoint, $payload, $method = 'POST'): Response
{
if ('GET' === strtoupper($method)) {
return new RedirectResponse($endpoint.'&'.http_build_query($payload));
}
$sHtml = "<form id='alipay_submit' name='alipay_submit' action='".$endpoint."' method='".$method."'>";
foreach ($payload 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($sHtml);
}
/**
* Get method config.
*
* @author yansongda <me@yansongda.cn>
*/
protected function getMethod(): string
{
return 'alipay.trade.page.pay';
}
/**
* Get productCode config.
*
* @author yansongda <me@yansongda.cn>
*/
protected function getProductCode(): string
{
return 'FAST_INSTANT_TRADE_PAY';
}
}

View File

@@ -0,0 +1,366 @@
<?php
namespace Yansongda\Pay\Gateways;
use Exception;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Yansongda\Pay\Contracts\GatewayApplicationInterface;
use Yansongda\Pay\Contracts\GatewayInterface;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidGatewayException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Pay\Gateways\Wechat\Support;
use Yansongda\Pay\Log;
use Yansongda\Supports\Collection;
use Yansongda\Supports\Config;
use Yansongda\Supports\Str;
/**
* @method Response app(array $config) APP 支付
* @method Collection groupRedpack(array $config) 分裂红包
* @method Collection miniapp(array $config) 小程序支付
* @method Collection mp(array $config) 公众号支付
* @method Collection pos(array $config) 刷卡支付
* @method Collection redpack(array $config) 普通红包
* @method Collection scan(array $config) 扫码支付
* @method Collection transfer(array $config) 企业付款
* @method RedirectResponse web(array $config) Web 扫码支付
* @method RedirectResponse wap(array $config) H5 支付
*/
class Wechat implements GatewayApplicationInterface
{
/**
* 普通模式.
*/
const MODE_NORMAL = 'normal';
/**
* 沙箱模式.
*/
const MODE_DEV = 'dev';
/**
* 香港钱包 API.
*/
const MODE_HK = 'hk';
/**
* 境外 API.
*/
const MODE_US = 'us';
/**
* 服务商模式.
*/
const MODE_SERVICE = 'service';
/**
* Const url.
*/
const URL = [
self::MODE_NORMAL => 'https://api.mch.weixin.qq.com/',
self::MODE_DEV => 'https://api.mch.weixin.qq.com/xdc/apiv2sandbox/',
self::MODE_HK => 'https://apihk.mch.weixin.qq.com/',
self::MODE_SERVICE => 'https://api.mch.weixin.qq.com/',
self::MODE_US => 'https://apius.mch.weixin.qq.com/',
];
/**
* Wechat payload.
*
* @var array
*/
protected $payload;
/**
* Wechat gateway.
*
* @var string
*/
protected $gateway;
/**
* Bootstrap.
*
* @author yansongda <me@yansongda.cn>
*
* @throws Exception
*/
public function __construct(Config $config)
{
$this->gateway = Support::create($config)->getBaseUri();
$this->payload = [
'appid' => $config->get('app_id', ''),
'mch_id' => $config->get('mch_id', ''),
'nonce_str' => Str::random(),
'notify_url' => $config->get('notify_url', ''),
'sign' => '',
'trade_type' => '',
'spbill_create_ip' => Request::createFromGlobals()->getClientIp(),
];
if ($config->get('mode', self::MODE_NORMAL) === static::MODE_SERVICE) {
$this->payload = array_merge($this->payload, [
'sub_mch_id' => $config->get('sub_mch_id'),
'sub_appid' => $config->get('sub_app_id', ''),
]);
}
}
/**
* Magic pay.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $method
* @param string $params
*
* @throws InvalidGatewayException
*
* @return Response|Collection
*/
public function __call($method, $params)
{
return self::pay($method, ...$params);
}
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $gateway
* @param array $params
*
* @throws InvalidGatewayException
*
* @return Response|Collection
*/
public function pay($gateway, $params = [])
{
Events::dispatch(new Events\PayStarting('Wechat', $gateway, $params));
$this->payload = array_merge($this->payload, $params);
$gateway = get_class($this).'\\'.Str::studly($gateway).'Gateway';
if (class_exists($gateway)) {
return $this->makePay($gateway);
}
throw new InvalidGatewayException("Pay Gateway [{$gateway}] Not Exists");
}
/**
* Verify data.
*
* @author yansongda <me@yansongda.cn>
*
* @param string|null $content
*
* @throws InvalidSignException
* @throws InvalidArgumentException
*/
public function verify($content = null, bool $refund = false): Collection
{
$content = $content ?? Request::createFromGlobals()->getContent();
Events::dispatch(new Events\RequestReceived('Wechat', '', [$content]));
$data = Support::fromXml($content);
if ($refund) {
$decrypt_data = Support::decryptRefundContents($data['req_info']);
$data = array_merge(Support::fromXml($decrypt_data), $data);
}
Log::debug('Resolved The Received Wechat Request Data', $data);
if ($refund || Support::generateSign($data) === $data['sign']) {
return new Collection($data);
}
Events::dispatch(new Events\SignFailed('Wechat', '', $data));
throw new InvalidSignException('Wechat Sign Verify FAILED', $data);
}
/**
* Query an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string|array $order
*
* @throws GatewayException
* @throws InvalidSignException
* @throws InvalidArgumentException
*/
public function find($order, ?string $type = 'wap'): Collection
{
unset($this->payload['spbill_create_ip']);
$gateway = get_class($this).'\\'.Str::studly($type).'Gateway';
if (!class_exists($gateway) || !is_callable([new $gateway(), 'find'])) {
throw new GatewayException("{$gateway} Done Not Exist Or Done Not Has FIND Method");
}
$config = call_user_func([new $gateway(), 'find'], $order);
$this->payload = Support::filterPayload($this->payload, $config['order']);
Events::dispatch(new Events\MethodCalled('Wechat', 'Find', $this->gateway, $this->payload));
return Support::requestApi(
$config['endpoint'],
$this->payload,
$config['cert']
);
}
/**
* Refund an order.
*
* @author yansongda <me@yansongda.cn>
*
* @throws GatewayException
* @throws InvalidSignException
* @throws InvalidArgumentException
*/
public function refund(array $order): Collection
{
unset($this->payload['spbill_create_ip']);
$this->payload = Support::filterPayload($this->payload, $order, true);
Events::dispatch(new Events\MethodCalled('Wechat', 'Refund', $this->gateway, $this->payload));
return Support::requestApi(
'secapi/pay/refund',
$this->payload,
true
);
}
/**
* Cancel an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param array $order
*
* @throws GatewayException
* @throws InvalidSignException
* @throws InvalidArgumentException
*/
public function cancel($order): Collection
{
unset($this->payload['spbill_create_ip']);
$this->payload = Support::filterPayload($this->payload, $order);
Events::dispatch(new Events\MethodCalled('Wechat', 'Cancel', $this->gateway, $this->payload));
return Support::requestApi(
'secapi/pay/reverse',
$this->payload,
true
);
}
/**
* Close an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string|array $order
*
* @throws GatewayException
* @throws InvalidSignException
* @throws InvalidArgumentException
*/
public function close($order): Collection
{
unset($this->payload['spbill_create_ip']);
$this->payload = Support::filterPayload($this->payload, $order);
Events::dispatch(new Events\MethodCalled('Wechat', 'Close', $this->gateway, $this->payload));
return Support::requestApi('pay/closeorder', $this->payload);
}
/**
* Echo success to server.
*
* @author yansongda <me@yansongda.cn>
*
* @throws InvalidArgumentException
*/
public function success(): Response
{
Events::dispatch(new Events\MethodCalled('Wechat', 'Success', $this->gateway));
return new Response(
Support::toXml(['return_code' => 'SUCCESS', 'return_msg' => 'OK']),
200,
['Content-Type' => 'application/xml']
);
}
/**
* Download the bill.
*
* @author yansongda <me@yansongda.cn>
*
* @throws GatewayException
* @throws InvalidArgumentException
*/
public function download(array $params): string
{
unset($this->payload['spbill_create_ip']);
$this->payload = Support::filterPayload($this->payload, $params, true);
Events::dispatch(new Events\MethodCalled('Wechat', 'Download', $this->gateway, $this->payload));
$result = Support::getInstance()->post(
'pay/downloadbill',
Support::getInstance()->toXml($this->payload)
);
if (is_array($result)) {
throw new GatewayException('Get Wechat API Error: '.$result['return_msg'], $result);
}
return $result;
}
/**
* Make pay gateway.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $gateway
*
* @throws InvalidGatewayException
*
* @return Response|Collection
*/
protected function makePay($gateway)
{
$app = new $gateway();
if ($app instanceof GatewayInterface) {
return $app->pay($this->gateway, array_filter($this->payload, function ($value) {
return '' !== $value && !is_null($value);
}));
}
throw new InvalidGatewayException("Pay Gateway [{$gateway}] Must Be An Instance Of GatewayInterface");
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Yansongda\Pay\Gateways\Wechat;
use Exception;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Pay\Gateways\Wechat;
use Yansongda\Supports\Str;
class AppGateway extends Gateway
{
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidSignException
* @throws Exception
*/
public function pay($endpoint, ?array $payload): Response
{
$payload['appid'] = Support::getInstance()->appid;
$payload['trade_type'] = $this->getTradeType();
if (Wechat::MODE_SERVICE === $this->mode) {
$payload['sub_appid'] = Support::getInstance()->sub_appid;
}
$pay_request = [
'appid' => Wechat::MODE_SERVICE === $this->mode ? $payload['sub_appid'] : $payload['appid'],
'partnerid' => Wechat::MODE_SERVICE === $this->mode ? $payload['sub_mch_id'] : $payload['mch_id'],
'prepayid' => $this->preOrder($payload)->get('prepay_id'),
'timestamp' => strval(time()),
'noncestr' => Str::random(),
'package' => 'Sign=WXPay',
];
$pay_request['sign'] = Support::generateSign($pay_request);
Events::dispatch(new Events\PayStarted('Wechat', 'App', $endpoint, $pay_request));
return new JsonResponse($pay_request);
}
/**
* Get trade type config.
*
* @author yansongda <me@yansongda.cn>
*/
protected function getTradeType(): string
{
return 'APP';
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Yansongda\Pay\Gateways\Wechat;
use Yansongda\Pay\Contracts\GatewayInterface;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Supports\Collection;
abstract class Gateway implements GatewayInterface
{
/**
* Mode.
*
* @var string
*/
protected $mode;
/**
* Bootstrap.
*
* @author yansongda <me@yansongda.cn>
*
* @throws InvalidArgumentException
*/
public function __construct()
{
$this->mode = Support::getInstance()->mode;
}
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @return Collection
*/
abstract public function pay($endpoint, ?array $payload);
/**
* Find.
*
* @author yansongda <me@yansongda.cn>
*
* @param string|array $order
*/
public function find($order): array
{
return [
'endpoint' => 'pay/orderquery',
'order' => is_array($order) ? $order : ['out_trade_no' => $order],
'cert' => false,
];
}
/**
* Get trade type config.
*
* @author yansongda <me@yansongda.cn>
*
* @return string
*/
abstract protected function getTradeType();
/**
* Schedule an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param array $payload
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidSignException
*/
protected function preOrder($payload): Collection
{
$payload['sign'] = Support::generateSign($payload);
Events::dispatch(new Events\MethodCalled('Wechat', 'PreOrder', '', $payload));
return Support::requestApi('pay/unifiedorder', $payload);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Yansongda\Pay\Gateways\Wechat;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Pay\Gateways\Wechat;
use Yansongda\Supports\Collection;
class GroupRedpackGateway extends Gateway
{
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidSignException
*/
public function pay($endpoint, ?array $payload): Collection
{
$payload['wxappid'] = $payload['appid'];
$payload['amt_type'] = 'ALL_RAND';
if (Wechat::MODE_SERVICE === $this->mode) {
$payload['msgappid'] = $payload['appid'];
}
unset($payload['appid'], $payload['trade_type'],
$payload['notify_url'], $payload['spbill_create_ip']);
$payload['sign'] = Support::generateSign($payload);
Events::dispatch(new Events\PayStarted('Wechat', 'Group Redpack', $endpoint, $payload));
return Support::requestApi(
'mmpaymkttransfers/sendgroupredpack',
$payload,
true
);
}
/**
* Get trade type config.
*
* @author yansongda <me@yansongda.cn>
*/
protected function getTradeType(): string
{
return '';
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Yansongda\Pay\Gateways\Wechat;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Pay\Gateways\Wechat;
use Yansongda\Supports\Collection;
class MiniappGateway extends MpGateway
{
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidSignException
*/
public function pay($endpoint, ?array $payload): Collection
{
$payload['appid'] = Support::getInstance()->miniapp_id;
if (Wechat::MODE_SERVICE === $this->mode) {
$payload['sub_appid'] = Support::getInstance()->sub_miniapp_id;
$this->payRequestUseSubAppId = true;
}
return parent::pay($endpoint, $payload);
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Yansongda\Pay\Gateways\Wechat;
use Exception;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Supports\Collection;
use Yansongda\Supports\Str;
class MpGateway extends Gateway
{
/**
* @var bool
*/
protected $payRequestUseSubAppId = false;
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidSignException
* @throws Exception
*/
public function pay($endpoint, ?array $payload): Collection
{
$payload['trade_type'] = $this->getTradeType();
$pay_request = [
'appId' => !$this->payRequestUseSubAppId ? $payload['appid'] : $payload['sub_appid'],
'timeStamp' => strval(time()),
'nonceStr' => Str::random(),
'package' => 'prepay_id='.$this->preOrder($payload)->get('prepay_id'),
'signType' => 'MD5',
];
$pay_request['paySign'] = Support::generateSign($pay_request);
Events::dispatch(new Events\PayStarted('Wechat', 'JSAPI', $endpoint, $pay_request));
return new Collection($pay_request);
}
/**
* Get trade type config.
*
* @author yansongda <me@yansongda.cn>
*/
protected function getTradeType(): string
{
return 'JSAPI';
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Yansongda\Pay\Gateways\Wechat;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Supports\Collection;
class PosGateway extends Gateway
{
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidSignException
*/
public function pay($endpoint, ?array $payload): Collection
{
unset($payload['trade_type'], $payload['notify_url']);
$payload['sign'] = Support::generateSign($payload);
Events::dispatch(new Events\PayStarted('Wechat', 'Pos', $endpoint, $payload));
return Support::requestApi('pay/micropay', $payload);
}
/**
* Get trade type config.
*
* @author yansongda <me@yansongda.cn>
*/
protected function getTradeType(): string
{
return 'MICROPAY';
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Yansongda\Pay\Gateways\Wechat;
use Symfony\Component\HttpFoundation\Request;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Pay\Gateways\Wechat;
use Yansongda\Supports\Collection;
class RedpackGateway extends Gateway
{
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidSignException
*/
public function pay($endpoint, ?array $payload): Collection
{
$payload['wxappid'] = $payload['appid'];
if ('cli' !== php_sapi_name()) {
$payload['client_ip'] = Request::createFromGlobals()->server->get('SERVER_ADDR');
}
if (Wechat::MODE_SERVICE === $this->mode) {
$payload['msgappid'] = $payload['appid'];
}
unset($payload['appid'], $payload['trade_type'],
$payload['notify_url'], $payload['spbill_create_ip']);
$payload['sign'] = Support::generateSign($payload);
Events::dispatch(new Events\PayStarted('Wechat', 'Redpack', $endpoint, $payload));
return Support::requestApi(
'mmpaymkttransfers/sendredpack',
$payload,
true
);
}
/**
* Get trade type config.
*
* @author yansongda <me@yansongda.cn>
*/
protected function getTradeType(): string
{
return '';
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Yansongda\Pay\Gateways\Wechat;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
class RefundGateway extends Gateway
{
/**
* Find.
*
* @author yansongda <me@yansongda.cn>
*
* @param $order
*/
public function find($order): array
{
return [
'endpoint' => 'pay/refundquery',
'order' => is_array($order) ? $order : ['out_trade_no' => $order],
'cert' => false,
];
}
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @throws InvalidArgumentException
*/
public function pay($endpoint, ?array $payload)
{
throw new InvalidArgumentException('Not Support Refund In Pay');
}
/**
* Get trade type config.
*
* @author yansongda <me@yansongda.cn>
*
* @throws InvalidArgumentException
*/
protected function getTradeType()
{
throw new InvalidArgumentException('Not Support Refund In Pay');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Yansongda\Pay\Gateways\Wechat;
use Symfony\Component\HttpFoundation\Request;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Supports\Collection;
class ScanGateway extends Gateway
{
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidSignException
*/
public function pay($endpoint, ?array $payload): Collection
{
$payload['spbill_create_ip'] = Request::createFromGlobals()->server->get('SERVER_ADDR');
$payload['trade_type'] = $this->getTradeType();
Events::dispatch(new Events\PayStarted('Wechat', 'Scan', $endpoint, $payload));
return $this->preOrder($payload);
}
/**
* Get trade type config.
*
* @author yansongda <me@yansongda.cn>
*/
protected function getTradeType(): string
{
return 'NATIVE';
}
}

View File

@@ -0,0 +1,460 @@
<?php
namespace Yansongda\Pay\Gateways\Wechat;
use Exception;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\BusinessException;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Pay\Gateways\Wechat;
use Yansongda\Pay\Log;
use Yansongda\Supports\Collection;
use Yansongda\Supports\Config;
use Yansongda\Supports\Str;
use Yansongda\Supports\Traits\HasHttpRequest;
/**
* @author yansongda <me@yansongda.cn>
*
* @property string appid
* @property string app_id
* @property string miniapp_id
* @property string sub_appid
* @property string sub_app_id
* @property string sub_miniapp_id
* @property string mch_id
* @property string sub_mch_id
* @property string key
* @property string return_url
* @property string cert_client
* @property string cert_key
* @property array log
* @property array http
* @property string mode
*/
class Support
{
use HasHttpRequest;
/**
* Wechat gateway.
*
* @var string
*/
protected $baseUri;
/**
* Config.
*
* @var Config
*/
protected $config;
/**
* Instance.
*
* @var Support
*/
private static $instance;
/**
* Bootstrap.
*
* @author yansongda <me@yansongda.cn>
*/
private function __construct(Config $config)
{
$this->baseUri = Wechat::URL[$config->get('mode', Wechat::MODE_NORMAL)];
$this->config = $config;
$this->setHttpOptions();
}
/**
* __get.
*
* @author yansongda <me@yansongda.cn>
*
* @param $key
*
* @return mixed|Config|null
*/
public function __get($key)
{
return $this->getConfig($key);
}
/**
* create.
*
* @author yansongda <me@yansongda.cn>
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidSignException
*
* @return Support
*/
public static function create(Config $config)
{
if ('cli' === php_sapi_name() || !(self::$instance instanceof self)) {
self::$instance = new self($config);
self::setDevKey();
}
return self::$instance;
}
/**
* getInstance.
*
* @author yansongda <me@yansongda.cn>
*
* @throws InvalidArgumentException
*
* @return Support
*/
public static function getInstance()
{
if (is_null(self::$instance)) {
throw new InvalidArgumentException('You Should [Create] First Before Using');
}
return self::$instance;
}
/**
* clear.
*
* @author yansongda <me@yansongda.cn>
*/
public static function clear()
{
self::$instance = null;
}
/**
* Request wechat api.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
* @param array $data
* @param bool $cert
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidSignException
*/
public static function requestApi($endpoint, $data, $cert = false): Collection
{
Events::dispatch(new Events\ApiRequesting('Wechat', '', self::$instance->getBaseUri().$endpoint, $data));
//xmlData需增加headers配置否则微信方无法接收到参数
$options = [
'headers' => [
'Content-Type' => 'application/xml',
],
];
$certOptions = $cert ? [
'cert' => self::$instance->cert_client,
'ssl_key' => self::$instance->cert_key,
] : [];
$result = self::$instance->post(
$endpoint,
self::toXml($data),
array_merge($options, $certOptions)
);
$result = is_array($result) ? $result : self::fromXml($result);
Events::dispatch(new Events\ApiRequested('Wechat', '', self::$instance->getBaseUri().$endpoint, $result));
return self::processingApiResult($endpoint, $result);
}
/**
* Filter payload.
*
* @author yansongda <me@yansongda.cn>
*
* @param array $payload
* @param array|string $params
* @param bool $preserve_notify_url
*
* @throws InvalidArgumentException
*/
public static function filterPayload($payload, $params, $preserve_notify_url = false): array
{
$type = self::getTypeName($params['type'] ?? '');
$payload = array_merge(
$payload,
is_array($params) ? $params : ['out_trade_no' => $params]
);
$payload['appid'] = self::$instance->getConfig($type, '');
if (Wechat::MODE_SERVICE === self::$instance->getConfig('mode', Wechat::MODE_NORMAL)) {
$payload['sub_appid'] = self::$instance->getConfig('sub_'.$type, '');
}
unset($payload['trade_type'], $payload['type']);
if (!$preserve_notify_url) {
unset($payload['notify_url']);
}
$payload['sign'] = self::generateSign($payload);
return $payload;
}
/**
* Generate wechat sign.
*
* @author yansongda <me@yansongda.cn>
*
* @param array $data
*
* @throws InvalidArgumentException
*/
public static function generateSign($data): string
{
$key = self::$instance->key;
if (is_null($key)) {
throw new InvalidArgumentException('Missing Wechat Config -- [key]');
}
ksort($data);
$string = md5(self::getSignContent($data).'&key='.$key);
Log::debug('Wechat Generate Sign Before UPPER', [$data, $string]);
return strtoupper($string);
}
/**
* Generate sign content.
*
* @author yansongda <me@yansongda.cn>
*
* @param array $data
*/
public static function getSignContent($data): string
{
$buff = '';
foreach ($data as $k => $v) {
$buff .= ('sign' != $k && '' != $v && !is_array($v)) ? $k.'='.$v.'&' : '';
}
Log::debug('Wechat Generate Sign Content Before Trim', [$data, $buff]);
return trim($buff, '&');
}
/**
* Decrypt refund contents.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $contents
*/
public static function decryptRefundContents($contents): string
{
return openssl_decrypt(
base64_decode($contents),
'AES-256-ECB',
md5(self::$instance->key),
OPENSSL_RAW_DATA
);
}
/**
* Convert array to xml.
*
* @author yansongda <me@yansongda.cn>
*
* @param array $data
*
* @throws InvalidArgumentException
*/
public static function toXml($data): string
{
if (!is_array($data) || count($data) <= 0) {
throw new InvalidArgumentException('Convert To Xml Error! Invalid Array!');
}
$xml = '<xml>';
foreach ($data as $key => $val) {
$xml .= is_numeric($val) ? '<'.$key.'>'.$val.'</'.$key.'>' :
'<'.$key.'><![CDATA['.$val.']]></'.$key.'>';
}
$xml .= '</xml>';
return $xml;
}
/**
* Convert xml to array.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $xml
*
* @throws InvalidArgumentException
*/
public static function fromXml($xml): array
{
if (!$xml) {
throw new InvalidArgumentException('Convert To Array Error! Invalid Xml!');
}
if (PHP_VERSION_ID < 80000) {
libxml_disable_entity_loader(true);
}
return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true);
}
/**
* Get service config.
*
* @author yansongda <me@yansongda.cn>
*
* @param string|null $key
* @param mixed|null $default
*
* @return mixed|null
*/
public function getConfig($key = null, $default = null)
{
if (is_null($key)) {
return $this->config->all();
}
if ($this->config->has($key)) {
return $this->config[$key];
}
return $default;
}
/**
* Get app id according to param type.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $type
*/
public static function getTypeName($type = ''): string
{
switch ($type) {
case '':
$type = 'app_id';
break;
case 'app':
$type = 'appid';
break;
default:
$type = $type.'_id';
}
return $type;
}
/**
* Get Base Uri.
*
* @author yansongda <me@yansongda.cn>
*
* @return string
*/
public function getBaseUri()
{
return $this->baseUri;
}
/**
* processingApiResult.
*
* @author yansongda <me@yansongda.cn>
*
* @param $endpoint
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidSignException
*
* @return Collection
*/
protected static function processingApiResult($endpoint, ?array $result)
{
if (!isset($result['return_code']) || 'SUCCESS' != $result['return_code']) {
throw new GatewayException('Get Wechat API Error:'.($result['return_msg'] ?? $result['retmsg'] ?? ''), $result);
}
if (isset($result['result_code']) && 'SUCCESS' != $result['result_code']) {
throw new BusinessException('Wechat Business Error: '.$result['err_code'].' - '.$result['err_code_des'], $result);
}
if (false !== strpos($endpoint, 'xdc/apiv2getsignkey') ||
false !== strpos($endpoint, 'mmpaymkttransfers') ||
self::generateSign($result) === $result['sign']) {
return new Collection($result);
}
Events::dispatch(new Events\SignFailed('Wechat', '', $result));
throw new InvalidSignException('Wechat Sign Verify FAILED', $result);
}
/**
* setDevKey.
*
* @author yansongda <me@yansongda.cn>
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidSignException
* @throws Exception
*
* @return Support
*/
private static function setDevKey()
{
if (Wechat::MODE_DEV == self::$instance->mode) {
$data = [
'mch_id' => self::$instance->mch_id,
'nonce_str' => Str::random(),
];
$data['sign'] = self::generateSign($data);
$result = self::requestApi('https://api.mch.weixin.qq.com/xdc/apiv2getsignkey/sign/getsignkey', $data);
self::$instance->config->set('key', $result['sandbox_signkey']);
}
return self::$instance;
}
/**
* Set Http options.
*
* @author yansongda <me@yansongda.cn>
*/
private function setHttpOptions(): self
{
if ($this->config->has('http') && is_array($this->config->get('http'))) {
$this->config->forget('http.base_uri');
$this->httpOptions = $this->config->get('http');
}
return $this;
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Yansongda\Pay\Gateways\Wechat;
use Symfony\Component\HttpFoundation\Request;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Pay\Gateways\Wechat;
use Yansongda\Supports\Collection;
class TransferGateway extends Gateway
{
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidSignException
*/
public function pay($endpoint, ?array $payload): Collection
{
if (Wechat::MODE_SERVICE === $this->mode) {
unset($payload['sub_mch_id'], $payload['sub_appid']);
}
$type = Support::getTypeName($payload['type'] ?? '');
$payload['mch_appid'] = Support::getInstance()->getConfig($type, '');
$payload['mchid'] = $payload['mch_id'];
if ('cli' !== php_sapi_name() && !isset($payload['spbill_create_ip'])) {
$payload['spbill_create_ip'] = Request::createFromGlobals()->server->get('SERVER_ADDR');
}
unset($payload['appid'], $payload['mch_id'], $payload['trade_type'],
$payload['notify_url'], $payload['type']);
$payload['sign'] = Support::generateSign($payload);
Events::dispatch(new Events\PayStarted('Wechat', 'Transfer', $endpoint, $payload));
return Support::requestApi(
'mmpaymkttransfers/promotion/transfers',
$payload,
true
);
}
/**
* Find.
*
* @author yansongda <me@yansongda.cn>
*
* @param $order
*/
public function find($order): array
{
return [
'endpoint' => 'mmpaymkttransfers/gettransferinfo',
'order' => is_array($order) ? $order : ['partner_trade_no' => $order],
'cert' => true,
];
}
/**
* Get trade type config.
*
* @author yansongda <me@yansongda.cn>
*/
protected function getTradeType(): string
{
return '';
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Yansongda\Pay\Gateways\Wechat;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidSignException;
class WapGateway extends Gateway
{
/**
* Pay an order.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $endpoint
*
* @throws GatewayException
* @throws InvalidArgumentException
* @throws InvalidSignException
*/
public function pay($endpoint, ?array $payload): RedirectResponse
{
$payload['trade_type'] = $this->getTradeType();
Events::dispatch(new Events\PayStarted('Wechat', 'Wap', $endpoint, $payload));
$mweb_url = $this->preOrder($payload)->get('mweb_url');
$url = is_null(Support::getInstance()->return_url) ? $mweb_url : $mweb_url.
'&redirect_url='.urlencode(Support::getInstance()->return_url);
return new RedirectResponse($url);
}
/**
* Get trade type config.
*
* @author yansongda <me@yansongda.cn>
*/
protected function getTradeType(): string
{
return 'MWEB';
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Yansongda\Pay\Gateways\Wechat;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Yansongda\Pay\Events;
use Yansongda\Pay\Exceptions\GatewayException;
use Yansongda\Pay\Exceptions\InvalidArgumentException;
use Yansongda\Pay\Exceptions\InvalidSignException;
use Yansongda\Supports\Collection;
class WebGateway extends Gateway
{
/**
* Pay an order.
*
* @param string $endpoint
* @param array $payload
*
* @author yansongda <me@yansongda.cn>
*
*/
public function pay($endpoint, ?array $payload): Response
{
$payload['spbill_create_ip'] = Request::createFromGlobals()->server->get('SERVER_ADDR');
$payload['trade_type'] = $this->getTradeType();
$code_url = $this->preOrder($payload)['code_url'];
$params = [
'body' => $payload['body'],
'code_url' => $code_url,
'out_trade_no' => $payload['out_trade_no'],
'return_url' => Support::getInstance()->return_url,
'total_fee' => $payload['total_fee'],
];
$params['sign'] = md5(implode('', $params) . Support::getInstance()->app_id);
$endpoint = addon_url("epay/api/wechat");
Events::dispatch(new Events\PayStarted('Wechat', 'Web/Wap', $endpoint, $payload));
return $this->buildPayHtml($endpoint, $params);
}
/**
* Build Html response.
*
* @param string $endpoint
* @param array $payload
* @param string $method
*
* @return Response
* @author yansongda <me@yansongda.cn>
*
*/
protected function buildPayHtml($endpoint, $payload, $method = 'POST'): Response
{
if (strtoupper($method) === 'GET') {
return new RedirectResponse($endpoint . '?' . http_build_query($payload));
}
$sHtml = "<form id='wechat_submit' name='wechat_submit' action='" . $endpoint . "' method='" . $method . "'>";
foreach ($payload 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['wechat_submit'].submit();</script>";
return new Response($sHtml);
}
/**
* Get trade type config.
*
* @return string
* @author yansongda <me@yansongda.cn>
*
*/
protected function getTradeType(): string
{
return 'NATIVE';
}
}

View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2017 yansongda <me@yansongda.cn>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,114 @@
<?php
namespace Yansongda\Pay\Listeners;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Yansongda\Pay\Events;
use Yansongda\Pay\Log;
class KernelLogSubscriber implements EventSubscriberInterface
{
/**
* Returns an array of event names this subscriber wants to listen to.
*
* The array keys are event names and the value can be:
*
* * The method name to call (priority defaults to 0)
* * An array composed of the method name to call and the priority
* * An array of arrays composed of the method names to call and respective
* priorities, or 0 if unset
*
* For instance:
*
* * array('eventName' => 'methodName')
* * array('eventName' => array('methodName', $priority))
* * array('eventName' => array(array('methodName1', $priority), array('methodName2')))
*
* @return array The event names to listen to
*/
public static function getSubscribedEvents()
{
return [
Events\PayStarting::class => ['writePayStartingLog', 256],
Events\PayStarted::class => ['writePayStartedLog', 256],
Events\ApiRequesting::class => ['writeApiRequestingLog', 256],
Events\ApiRequested::class => ['writeApiRequestedLog', 256],
Events\SignFailed::class => ['writeSignFailedLog', 256],
Events\RequestReceived::class => ['writeRequestReceivedLog', 256],
Events\MethodCalled::class => ['writeMethodCalledLog', 256],
];
}
/**
* writePayStartingLog.
*
* @author yansongda <me@yansongda.cn>
*/
public function writePayStartingLog(Events\PayStarting $event)
{
Log::debug("Starting To {$event->driver}", [$event->gateway, $event->params]);
}
/**
* writePayStartedLog.
*
* @author yansongda <me@yansongda.cn>
*/
public function writePayStartedLog(Events\PayStarted $event)
{
Log::info(
"{$event->driver} {$event->gateway} Has Started",
[$event->endpoint, $event->payload]
);
}
/**
* writeApiRequestingLog.
*
* @author yansongda <me@yansongda.cn>
*/
public function writeApiRequestingLog(Events\ApiRequesting $event)
{
Log::debug("Requesting To {$event->driver} Api", [$event->endpoint, $event->payload]);
}
/**
* writeApiRequestedLog.
*
* @author yansongda <me@yansongda.cn>
*/
public function writeApiRequestedLog(Events\ApiRequested $event)
{
Log::debug("Result Of {$event->driver} Api", $event->result);
}
/**
* writeSignFailedLog.
*
* @author yansongda <me@yansongda.cn>
*/
public function writeSignFailedLog(Events\SignFailed $event)
{
Log::warning("{$event->driver} Sign Verify FAILED", $event->data);
}
/**
* writeRequestReceivedLog.
*
* @author yansongda <me@yansongda.cn>
*/
public function writeRequestReceivedLog(Events\RequestReceived $event)
{
Log::info("Received {$event->driver} Request", $event->data);
}
/**
* writeMethodCalledLog.
*
* @author yansongda <me@yansongda.cn>
*/
public function writeMethodCalledLog(Events\MethodCalled $event)
{
Log::info("{$event->driver} {$event->gateway} Method Has Called", [$event->endpoint, $event->payload]);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Yansongda\Pay;
use Yansongda\Supports\Log as BaseLog;
/**
* @method static void emergency($message, ?array $context = array())
* @method static void alert($message, ?array $context = array())
* @method static void critical($message, ?array $context = array())
* @method static void error($message, ?array $context = array())
* @method static void warning($message, ?array $context = array())
* @method static void notice($message, ?array $context = array())
* @method static void info($message, ?array $context = array())
* @method static void debug($message, ?array $context = array())
* @method static void log($message, ?array $context = array())
*/
class Log
{
/**
* Forward call.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $method
* @param array $args
*
* @return mixed
*/
public static function __callStatic($method, $args)
{
return forward_static_call_array([BaseLog::class, $method], $args);
}
/**
* Forward call.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $method
* @param array $args
*
* @return mixed
*/
public function __call($method, $args)
{
return call_user_func_array([BaseLog::class, $method], $args);
}
}

View File

@@ -0,0 +1,131 @@
<?php
namespace Yansongda\Pay;
use Exception;
use Yansongda\Pay\Contracts\GatewayApplicationInterface;
use Yansongda\Pay\Exceptions\InvalidGatewayException;
use Yansongda\Pay\Gateways\Alipay;
use Yansongda\Pay\Gateways\Wechat;
use Yansongda\Pay\Listeners\KernelLogSubscriber;
use Yansongda\Supports\Config;
use Yansongda\Supports\Log;
use Yansongda\Supports\Logger;
use Yansongda\Supports\Str;
/**
* @method static Alipay alipay(array $config) 支付宝
* @method static Wechat wechat(array $config) 微信
*/
class Pay
{
/**
* Config.
*
* @var Config
*/
protected $config;
/**
* Bootstrap.
*
* @author yansongda <me@yansongda.cn>
*
* @throws Exception
*/
public function __construct(array $config)
{
$this->config = new Config($config);
$this->registerLogService();
$this->registerEventService();
}
/**
* Magic static call.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $method
* @param array $params
*
* @throws InvalidGatewayException
* @throws Exception
*/
public static function __callStatic($method, $params): GatewayApplicationInterface
{
$app = new self(...$params);
return $app->create($method);
}
/**
* Create a instance.
*
* @author yansongda <me@yansongda.cn>
*
* @param string $method
*
* @throws InvalidGatewayException
*/
protected function create($method): GatewayApplicationInterface
{
$gateway = __NAMESPACE__.'\\Gateways\\'.Str::studly($method);
if (class_exists($gateway)) {
return self::make($gateway);
}
throw new InvalidGatewayException("Gateway [{$method}] Not Exists");
}
/**
* Make a gateway.
*
* @author yansongda <me@yansonga.cn>
*
* @param string $gateway
*
* @throws InvalidGatewayException
*/
protected function make($gateway): GatewayApplicationInterface
{
$app = new $gateway($this->config);
if ($app instanceof GatewayApplicationInterface) {
return $app;
}
throw new InvalidGatewayException("Gateway [{$gateway}] Must Be An Instance Of GatewayApplicationInterface");
}
/**
* Register log service.
*
* @author yansongda <me@yansongda.cn>
*
* @throws Exception
*/
protected function registerLogService()
{
$config = $this->config->get('log');
$config['identify'] = 'yansongda.pay';
$logger = new Logger();
$logger->setConfig($config);
Log::setInstance($logger);
}
/**
* Register event service.
*
* @author yansongda <me@yansongda.cn>
*/
protected function registerEventService()
{
Events::setDispatcher(Events::createDispatcher());
Events::addSubscriber(new KernelLogSubscriber());
}
}