diff --git a/src/Kernel/Traits/HasAttributes.php b/src/Kernel/Traits/HasAttributes.php new file mode 100644 index 0000000000000000000000000000000000000000..1f2acd6d47642e6843b455086b635b1582088256 --- /dev/null +++ b/src/Kernel/Traits/HasAttributes.php @@ -0,0 +1,96 @@ + + */ + protected array $attributes = []; + + /** + * @param array $attributes + */ + public function __construct(array $attributes) + { + $this->attributes = $attributes; + } + + /** + * @return array + */ + public function toArray(): array + { + return $this->attributes; + } + + public function toJson(): string|false + { + return json_encode($this->attributes); + } + + public function has(string $key): bool + { + return array_key_exists($key, $this->attributes); + } + + /** + * @param array $attributes + */ + public function merge(array $attributes): self + { + $this->attributes = array_merge($this->attributes, $attributes); + + return $this; + } + + /** + * @return array $attributes + */ + public function jsonSerialize(): array + { + return $this->attributes; + } + + public function __set(string $attribute, $value): void + { + $this->attributes[$attribute] = $value; + } + + public function __get(string $attribute) + { + return $this->attributes[$attribute] ?? null; + } + + public function offsetExists($offset): bool + { + /** @phpstan-ignore-next-line */ + return array_key_exists($offset, $this->attributes); + } + + public function offsetGet($offset) + { + return $this->attributes[$offset]; + } + + public function offsetSet($offset, $value): void + { + if (null === $offset) { + $this->attributes[] = $value; + } else { + $this->attributes[$offset] = $value; + } + } + + public function offsetUnset($offset): void + { + unset($this->attributes[$offset]); + } +} diff --git a/src/Kernel/Traits/InteractWithHandlers.php b/src/Kernel/Traits/InteractWithHandlers.php new file mode 100644 index 0000000000000000000000000000000000000000..26dccfcaa259fc0c3794d2810118e14beb11c9c4 --- /dev/null +++ b/src/Kernel/Traits/InteractWithHandlers.php @@ -0,0 +1,215 @@ + + */ + protected array $handlers = []; + + /** + * @return array + */ + public function getHandlers(): array + { + return $this->handlers; + } + + /** + * @param callable|string $handler + * @return InteractWithHandlers + * @throws InvalidArgumentException + */ + public function with(callable|string $handler): static + { + return $this->withHandler($handler); + } + + /** + * @param callable|string $handler + * @return InteractWithHandlers + * @throws InvalidArgumentException + */ + public function withHandler(callable|string $handler): static + { + $this->handlers[] = $this->createHandlerItem($handler); + + return $this; + } + + /** + * @param callable|string $handler + * @return array{hash: string, handler: callable} + * + * @throws InvalidArgumentException + */ + public function createHandlerItem(callable|string $handler): array + { + return [ + 'hash' => $this->getHandlerHash($handler), + 'handler' => $this->makeClosure($handler), + ]; + } + + /** + * @param callable|string $handler + * @return string + * @throws InvalidArgumentException + */ + protected function getHandlerHash(callable|string $handler): string + { + switch (true) { + case is_string($handler): + return $handler; + case is_array($handler): + return is_string($handler[0]) ? $handler[0].'::'.$handler[1] : get_class($handler[0]).$handler[1]; + case $handler instanceof Closure: + return spl_object_hash($handler); + default: + throw new InvalidArgumentException('Invalid handler: '.gettype($handler)); + } + } + + /** + * @param callable|string $handler + * @return callable + * @throws InvalidArgumentException + */ + protected function makeClosure(callable|string $handler): callable + { + if (is_callable($handler)) { + return $handler; + } + + if (class_exists($handler) && method_exists($handler, '__invoke')) { + /** + * @psalm-suppress InvalidFunctionCall + * @phpstan-ignore-next-line https://github.com/phpstan/phpstan/issues/5867 + */ + return fn () => (new $handler())(...func_get_args()); + } + + throw new InvalidArgumentException(sprintf('Invalid handler: %s.', $handler)); + } + + /** + * @param callable|string $handler + * @return InteractWithHandlers + * @throws InvalidArgumentException + */ + public function prepend(callable|string $handler): static + { + return $this->prependHandler($handler); + } + + /** + * @param callable|string $handler + * @return InteractWithHandlers + * @throws InvalidArgumentException + */ + public function prependHandler(callable|string $handler): static + { + array_unshift($this->handlers, $this->createHandlerItem($handler)); + + return $this; + } + + /** + * @param callable|string $handler + * @return InteractWithHandlers + * @throws InvalidArgumentException + */ + public function without(callable|string $handler): static + { + return $this->withoutHandler($handler); + } + + /** + * @param callable|string $handler + * @return InteractWithHandlers + * @throws InvalidArgumentException + */ + public function withoutHandler(callable|string $handler): static + { + $index = $this->indexOf($handler); + + if ($index > -1) { + unset($this->handlers[$index]); + } + + return $this; + } + + /** + * @param callable|string $handler + * @return int + * @throws InvalidArgumentException + */ + public function indexOf(callable|string $handler): int + { + foreach ($this->handlers as $index => $item) { + if ($item['hash'] === $this->getHandlerHash($handler)) { + return $index; + } + } + + return -1; + } + + /** + * @param $value + * @param callable|string $handler + * @return InteractWithHandlers + * @throws InvalidArgumentException + */ + public function when($value, callable|string $handler): static + { + if (is_callable($value)) { + $value = call_user_func($value, $this); + } + + if ($value) { + return $this->withHandler($handler); + } + + return $this; + } + + public function handle($result, $payload = null) + { + $next = $result = is_callable($result) ? $result : fn ($p) => $result; + + foreach (array_reverse($this->handlers) as $item) { + $next = fn ($p) => $item['handler']($p, $next) ?? $result($p); + } + + return $next($payload); + } + + /** + * @param callable|string $handler + * @return bool + * @throws InvalidArgumentException + */ + public function has(callable|string $handler): bool + { + return $this->indexOf($handler) > -1; + } +} diff --git a/src/MiniProgram/Application.php b/src/MiniProgram/Application.php index abc22c60e59f42c6b66f9c35e77552e1f3a5d9de..d6b3acf3cfdb6be7b0d55b4cae633e3b0c0e8432 100644 --- a/src/MiniProgram/Application.php +++ b/src/MiniProgram/Application.php @@ -4,6 +4,7 @@ namespace EasyTiktok\MiniProgram; use EasyTiktok\Kernel\ServiceContainer; use EasyTiktok\Kernel\Traits\ResponseCastable; +use EasyTiktok\MiniProgram\Pay\Server as PayServer; /** * Class Application. @@ -12,6 +13,9 @@ use EasyTiktok\Kernel\Traits\ResponseCastable; * @property Auth\Client $auth * @property QrCode\Client $qr_code * @property Server\Encryptor $encryptor + * @property Pay\Client $pay + * @property PayServer $pay_server + * @property Order\Client $order * @author zhaoxiang */ class Application extends ServiceContainer { @@ -25,7 +29,9 @@ class Application extends ServiceContainer { Base\ServiceProvider::class, Auth\ServiceProvider::class, QrCode\ServiceProvider::class, - Server\ServiceProvider::class + Server\ServiceProvider::class, + Pay\ServiceProvider::class, + Order\ServiceProvider::class, ]; /** diff --git a/src/MiniProgram/Order/Client.php b/src/MiniProgram/Order/Client.php new file mode 100644 index 0000000000000000000000000000000000000000..6528fcfbcdeac50d3639461379b34e02e34ba1bd --- /dev/null +++ b/src/MiniProgram/Order/Client.php @@ -0,0 +1,36 @@ +httpPostJson('apps/order/v2/push', $params); + } +} diff --git a/src/MiniProgram/Order/ServiceProvider.php b/src/MiniProgram/Order/ServiceProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..b755db72ef371b762bc498f9a599ec3e4d93c973 --- /dev/null +++ b/src/MiniProgram/Order/ServiceProvider.php @@ -0,0 +1,21 @@ +app['config']['app_id']; + $params = compact('app_id', 'out_order_no', 'total_amount', 'subject', 'body', 'notify_url', 'valid_time', 'cp_extra'); + $params['sign'] = $this->sign($params); + return $this->httpPostJson('apps/ecpay/v1/create_order', $params); + } + + /** + * 发起退款 + * @param string $out_order_no + * @param string $out_refund_no + * @param int $refund_amount + * @param string $reason + * @param string $notify_url + * @param string $msg_page + * @param string $cp_extra + * @return array + * @throws GuzzleException + * @throws HttpException + * @throws InvalidConfigException + */ + public function refundPayOrder(string $out_order_no, string $out_refund_no, int $refund_amount, string $reason, string $notify_url = '', string $msg_page = '', string $cp_extra = ''): array + { + $app_id = $this->app['config']['app_id']; + $params = compact('app_id', 'out_order_no', 'out_refund_no', 'refund_amount', 'reason', 'notify_url', 'msg_page', 'cp_extra'); + $params['sign'] = $this->sign($params); + return $this->httpPostJson('apps/ecpay/v1/create_refund', $params); + } + + /** + * 申请结算 + * @param string $out_settle_no + * @param string $out_order_no + * @param string $notify_url + * @param string $settle_desc + * @param string $cp_extra + * @param string $finish + * @return array + * @throws GuzzleException + * @throws HttpException + * @throws InvalidConfigException + */ + public function settle(string $out_settle_no, string $out_order_no, string $notify_url = '', string $settle_desc = '主动结算', string $cp_extra = '', string $finish = 'true'): array + { + $app_id = $this->app['config']['app_id']; + $params = compact('app_id', 'out_order_no', 'out_settle_no', 'settle_desc', 'notify_url', 'finish', 'cp_extra'); + $params['sign'] = $this->sign($params); + return $this->httpPostJson('apps/ecpay/v1/settle', $params); + } + + /** + * 签名 + * @param array $params + * @return string + */ + protected function sign(array $params): string + { + $need_sign_params = []; + foreach ($params as $k => $v) { + $v = trim(strval($v)); + if (empty($v) || in_array($k, $this->no_need_sign_params)) { + continue; + } + $need_sign_params[] = $v; + } + $need_sign_params[] = $this->app['config']['pay']['salt']; + sort($need_sign_params, SORT_STRING); + return md5(implode('&', $need_sign_params)); + } +} diff --git a/src/MiniProgram/Pay/Message.php b/src/MiniProgram/Pay/Message.php new file mode 100644 index 0000000000000000000000000000000000000000..1b5597da528f0a4780a236eaa42da3f2cdf60f80 --- /dev/null +++ b/src/MiniProgram/Pay/Message.php @@ -0,0 +1,30 @@ +toArray()['type'] ?? ''; + if (empty($type)) { + throw new \RuntimeException('Invalid event type.'); + } + return $type; + } +} diff --git a/src/MiniProgram/Pay/OrderInfo.php b/src/MiniProgram/Pay/OrderInfo.php new file mode 100644 index 0000000000000000000000000000000000000000..3244731d547d1da49d324a3cda00044dc8e94045 --- /dev/null +++ b/src/MiniProgram/Pay/OrderInfo.php @@ -0,0 +1,15 @@ +order_id = $order_id; + $this->order_token = $order_token; + } +} \ No newline at end of file diff --git a/src/MiniProgram/Pay/Server.php b/src/MiniProgram/Pay/Server.php new file mode 100644 index 0000000000000000000000000000000000000000..961a57957c48ff2c4501366cc748d7ae3c69952a --- /dev/null +++ b/src/MiniProgram/Pay/Server.php @@ -0,0 +1,121 @@ +app = $app; + } + + public function serve(): Response + { + $message = $this->getRequestMessage(); + try { + $default_response = new Response( + 200, + [], + strval(json_encode(['err_no' => 0, 'err_tips' => 'success'], JSON_UNESCAPED_UNICODE)) + ); + $response = $this->handle($default_response, $message); + + if (!($response instanceof ResponseInterface)) { + $response = $default_response; + } + + return $response; + } catch (\Exception $e) { + return new Response( + 200, + [], + strval(json_encode(['err_no' => 400, '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) { + return $message->getType() === Message::TYPE_PAY + ? $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) { + return $message->getType() === Message::TYPE_REFUND + ? $handler($message, $next) : $next($message); + }); + + return $this; + } + + /** + * @link https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/ecpay/settlements/callback + * + * @throws InvalidArgumentException + */ + public function handleSettled(callable $handler): static + { + $this->with(function (Message $message, Closure $next) use ($handler) { + return $message->getType() === Message::TYPE_SETTLED + ? $handler($message, $next) : $next($message); + }); + + return $this; + } + + public function setRequest(ServerRequestInterface $request): ServerRequestInterface + { + return $this->request = $request; + } + + public function getRequestMessage(): Message + { + if (empty($this->request)) { + throw new RuntimeException('empty request.'); + } + + $request = $this->request->getBody(); + $attributes = json_decode($request, true); + if (! is_array($attributes)) { + throw new RuntimeException('Invalid request body.'); + } + + // todo验签 + $attributes['msg'] = is_array($attributes['msg']) ? $attributes['msg'] : json_decode($attributes['msg'] ?? '', true); + return new Message($attributes); + } +} diff --git a/src/MiniProgram/Pay/ServiceProvider.php b/src/MiniProgram/Pay/ServiceProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..259e96c32968ae2bdd44133ab4330ed6178ace78 --- /dev/null +++ b/src/MiniProgram/Pay/ServiceProvider.php @@ -0,0 +1,24 @@ +