├── wechat.jpg
├── storage
├── 0702A46032714AC6E3412C9A29C5029B.jpg
└── 9A28EDE3E00DB6A665677D48D3A864B3.jpg
├── .gitignore
├── app
├── Exception
│ ├── PaymentException.php
│ ├── RuntimeException.php
│ ├── InvalidPaymentProxyException.php
│ ├── InvalidMiniProgramProxyException.php
│ ├── BusinessException.php
│ └── Handler
│ │ └── BusinessExceptionHandler.php
├── Factory
│ └── PaymentFactory.php
├── Constants
│ ├── Payment.php
│ └── ErrorCode.php
├── Kernel
│ ├── Rpc
│ │ ├── Payment
│ │ │ ├── Contract
│ │ │ │ ├── OrderInterface.php
│ │ │ │ └── NotifyInterface.php
│ │ │ ├── BaseService.php
│ │ │ ├── OrderService.php
│ │ │ └── NotifyService.php
│ │ ├── MiniProgram
│ │ │ ├── Contract
│ │ │ │ ├── AuthInterface.php
│ │ │ │ └── QrCodeInterface.php
│ │ │ ├── BaseService.php
│ │ │ ├── AuthService.php
│ │ │ └── QrCodeService.php
│ │ └── Response.php
│ ├── Log
│ │ ├── LoggerFactory.php
│ │ └── AppendRequestIdProcessor.php
│ ├── Http
│ │ ├── WorkerStartListener.php
│ │ └── Response.php
│ ├── Payment
│ │ ├── PaymentProxy.php
│ │ └── PaymentFactory.php
│ ├── MiniProgram
│ │ ├── MiniProgramProxy.php
│ │ ├── MiniProgramFactory.php
│ │ └── SessionManager.php
│ ├── Context
│ │ └── Coroutine.php
│ ├── ClassMap
│ │ └── Coroutine.php
│ ├── Functions.php
│ └── Lock
│ │ └── RedisLock.php
├── Model
│ └── Model.php
├── Controller
│ ├── Controller.php
│ ├── IndexController.php
│ └── RpcController.php
├── Listener
│ ├── BootApplicationListener.php
│ ├── DbQueryExecutedListener.php
│ └── QueueHandleListener.php
└── Helper
│ ├── NumberHelper.php
│ ├── DirectoryHelper.php
│ ├── EncryptHelper.php
│ ├── DateHelper.php
│ ├── FileHelper.php
│ └── ArrayHelper.php
├── .phpstorm.meta.php
├── config
├── autoload
│ ├── aspects.php
│ ├── commands.php
│ ├── consul.php
│ ├── processes.php
│ ├── exceptions.php
│ ├── cache.php
│ ├── listeners.php
│ ├── middlewares.php
│ ├── async_queue.php
│ ├── annotations.php
│ ├── aliyun_acm.php
│ ├── redis.php
│ ├── dependencies.php
│ ├── mini_program.php
│ ├── devtool.php
│ ├── payment.php
│ ├── databases.php
│ ├── server.php
│ ├── metric.php
│ ├── logger.php
│ ├── opentracing.php
│ └── services.php
├── container.php
├── config.php
└── routes.php
├── phpstan.neon
├── deploy.test.yml
├── phpunit.xml
├── test
├── bootstrap.php
├── HttpTestCase.php
└── Cases
│ └── ExampleTest.php
├── bin
└── hyperf.php
├── .env.example
├── LICENSE
├── .gitlab-ci.yml
├── Dockerfile
├── .php_cs
├── composer.json
└── README.md
/wechat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AuroraYolo/hyperf-v2-demo/HEAD/wechat.jpg
--------------------------------------------------------------------------------
/storage/0702A46032714AC6E3412C9A29C5029B.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AuroraYolo/hyperf-v2-demo/HEAD/storage/0702A46032714AC6E3412C9A29C5029B.jpg
--------------------------------------------------------------------------------
/storage/9A28EDE3E00DB6A665677D48D3A864B3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AuroraYolo/hyperf-v2-demo/HEAD/storage/9A28EDE3E00DB6A665677D48D3A864B3.jpg
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .buildpath
2 | .settings/
3 | .project
4 | *.patch
5 | .idea/
6 | .git/
7 | runtime/
8 | vendor/
9 | .phpintel/
10 | .env
11 | .DS_Store
12 | *.lock
13 | ./private
14 |
--------------------------------------------------------------------------------
/app/Exception/PaymentException.php:
--------------------------------------------------------------------------------
1 | 'http://127.0.0.1:8500',
14 | ];
15 |
--------------------------------------------------------------------------------
/config/autoload/processes.php:
--------------------------------------------------------------------------------
1 | [
14 | 'http' => [
15 | App\Exception\Handler\BusinessExceptionHandler::class,
16 | ],
17 | ],
18 | ];
19 |
--------------------------------------------------------------------------------
/app/Kernel/Rpc/Payment/Contract/OrderInterface.php:
--------------------------------------------------------------------------------
1 | [
14 | 'driver' => Hyperf\Cache\Driver\RedisDriver::class,
15 | 'packer' => Hyperf\Utils\Packer\PhpSerializerPacker::class,
16 | 'prefix' => 'c:',
17 | ],
18 | ];
19 |
--------------------------------------------------------------------------------
/config/autoload/listeners.php:
--------------------------------------------------------------------------------
1 | [
18 | // MetricMiddleware::class,
19 | // TraceMiddleware::class,
20 | ],
21 | ];
22 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | # Magic behaviour with __get, __set, __call and __callStatic is not exactly static analyser-friendly :)
2 | # Fortunately, You can ingore it by the following config.
3 | #
4 | # vendor/bin/phpstan analyse app --memory-limit 200M -l 0
5 | #
6 | parameters:
7 | bootstrapFiles:
8 | - "test/bootstrap.php"
9 | reportUnmatchedIgnoredErrors: false
10 | ignoreErrors:
11 | - '#Static call to instance method Hyperf\\HttpServer\\Router\\Router::[a-zA-Z0-9\\_]+\(\)#'
12 | - '#Static call to instance method Hyperf\\DbConnection\\Db::[a-zA-Z0-9\\_]+\(\)#'
13 |
--------------------------------------------------------------------------------
/app/Model/Model.php:
--------------------------------------------------------------------------------
1 | get(HyperfLoggerFactory::class)->make();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/config/autoload/async_queue.php:
--------------------------------------------------------------------------------
1 | [
14 | 'driver' => Hyperf\AsyncQueue\Driver\RedisDriver::class,
15 | 'channel' => '{queue}',
16 | 'timeout' => 2,
17 | 'retry_seconds' => 5,
18 | 'handle_timeout' => 10,
19 | 'processes' => 1,
20 | 'concurrent' => [
21 | 'limit' => 2,
22 | ],
23 | ],
24 | ];
25 |
--------------------------------------------------------------------------------
/config/autoload/annotations.php:
--------------------------------------------------------------------------------
1 | [
16 | 'paths' => [
17 | BASE_PATH . '/app',
18 | ],
19 | 'ignore_annotations' => [
20 | 'mixin',
21 | ],
22 | 'class_map' => [
23 | // Coroutine::class => BASE_PATH . '/app/Kernel/ClassMap/Coroutine.php',
24 | ],
25 | ],
26 | ];
27 |
--------------------------------------------------------------------------------
/deploy.test.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 | services:
3 | hyperf:
4 | image: $REGISTRY_URL/$PROJECT_NAME:test
5 | environment:
6 | - "APP_PROJECT=hyperf"
7 | - "APP_ENV=test"
8 | ports:
9 | - 9501:9501
10 | deploy:
11 | replicas: 1
12 | restart_policy:
13 | condition: on-failure
14 | delay: 5s
15 | max_attempts: 5
16 | update_config:
17 | parallelism: 2
18 | delay: 5s
19 | order: start-first
20 | networks:
21 | - hyperf_net
22 | configs:
23 | - source: hyperf_v1.0
24 | target: /opt/www/.env
25 | configs:
26 | hyperf_v1.0:
27 | external: true
28 | networks:
29 | hyperf_net:
30 | external: true
31 |
--------------------------------------------------------------------------------
/config/container.php:
--------------------------------------------------------------------------------
1 | get(StdoutLogger::class));
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | ./test
14 |
15 |
16 |
17 |
18 | ./app
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/config/autoload/aliyun_acm.php:
--------------------------------------------------------------------------------
1 | false,
14 | 'use_standalone_process' => false,
15 | 'interval' => 5,
16 | 'endpoint' => env('ALIYUN_ACM_ENDPOINT', ''),
17 | 'namespace' => env('ALIYUN_ACM_NAMESPACE', ''),
18 | 'data_id' => env('ALIYUN_ACM_DATA_ID', ''),
19 | 'group' => env('ALIYUN_ACM_GROUP', ''),
20 | 'access_key' => env('ALIYUN_ACM_AK', ''),
21 | 'secret_key' => env('ALIYUN_ACM_SK', ''),
22 | 'ecs_ram_role' => env('ALIYUN_ACM_RAM_ROLE', ''),
23 | ];
24 |
--------------------------------------------------------------------------------
/app/Kernel/Log/AppendRequestIdProcessor.php:
--------------------------------------------------------------------------------
1 | get(Hyperf\Contract\ApplicationInterface::class);
27 |
--------------------------------------------------------------------------------
/app/Exception/BusinessException.php:
--------------------------------------------------------------------------------
1 | [
14 | 'host' => env('REDIS_HOST', 'localhost'),
15 | 'auth' => env('REDIS_AUTH', null),
16 | 'port' => (int) env('REDIS_PORT', 6379),
17 | 'db' => (int) env('REDIS_DB', 0),
18 | 'pool' => [
19 | 'min_connections' => 1,
20 | 'max_connections' => 10,
21 | 'connect_timeout' => 10.0,
22 | 'wait_timeout' => 3.0,
23 | 'heartbeat' => -1,
24 | 'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60),
25 | ],
26 | ],
27 | ];
28 |
--------------------------------------------------------------------------------
/bin/hyperf.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | get(\Hyperf\Contract\ApplicationInterface::class);
23 | $application->run();
24 | })();
25 |
--------------------------------------------------------------------------------
/config/config.php:
--------------------------------------------------------------------------------
1 | env('APP_NAME', 'skeleton'),
19 | 'app_env' => env('APP_ENV', 'dev'),
20 | 'scan_cacheable' => env('SCAN_CACHEABLE', true),
21 | StdoutLoggerInterface::class => [
22 | 'log_level' => [
23 | LogLevel::ALERT,
24 | LogLevel::CRITICAL,
25 | LogLevel::DEBUG,
26 | LogLevel::EMERGENCY,
27 | LogLevel::ERROR,
28 | LogLevel::INFO,
29 | LogLevel::NOTICE,
30 | LogLevel::WARNING,
31 | ],
32 | ],
33 | ];
34 |
--------------------------------------------------------------------------------
/config/autoload/dependencies.php:
--------------------------------------------------------------------------------
1 | App\Kernel\Log\LoggerFactory::class,
20 | // Hyperf\Server\Listener\AfterWorkerStartListener::class => App\Kernel\Http\WorkerStartListener::class,
21 | JsonRpcTransporter::class => JsonRpcPoolTransporter::class,
22 | Hyperf\Contract\NormalizerInterface::class => new SerializerFactory(Serializer::class),
23 | // Prometheus\Storage\Adapter::class => Hyperf\Metric\Adapter\Prometheus\RedisStorageFactory::class,
24 | ];
25 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | APP_NAME=biz-skeleton
2 |
3 | # Mysql
4 | DB_DRIVER=mysql
5 | DB_HOST=localhost
6 | DB_PORT=3306
7 | DB_DATABASE=hyperf
8 | DB_USERNAME=root
9 | DB_PASSWORD=
10 | DB_CHARSET=utf8mb4
11 | DB_COLLATION=utf8mb4_unicode_ci
12 | DB_PREFIX=
13 |
14 | # Redis
15 | REDIS_HOST=localhost
16 | REDIS_AUTH=(null)
17 | REDIS_PORT=6379
18 | REDIS_DB=0
19 |
20 |
21 | WECHAT_MINI_PROGRAM_APPID=
22 | WECHAT_MINI_PROGRAM_SECRET=
23 | WECHAT_MINI_PROGRAM_TOKEN=
24 | WECHAT_MINI_PROGRAM_AES_KEY=
25 |
26 | WECHAT_MINI_PROGRAM_APPID_DEFAULT2=
27 | WECHAT_MINI_PROGRAM_SECRET_DEFAULT2=
28 | WECHAT_MINI_PROGRAM_TOKEN_DEFAULT2=
29 | WECHAT_MINI_PROGRAM_AES_KEY_DEFAULT2=
30 |
31 | WECHAT_PAYMENT_SANDBOX=false
32 | WECHAT_PAYMENT_APPID=
33 | WECHAT_PAYMENT_MCH_ID=
34 | WECHAT_PAYMENT_KEY=
35 | WECHAT_PAYMENT_CERT_PATH=
36 | WECHAT_PAYMENT_KEY_PATH=
37 | WECHAT_PAYMENT_NOTIFY_URL=
38 | WECHAT_REFUND_NOTIFY_URL=
39 | WECHAT_THROUGH_TRAIN_REFUND_NOTIFY_URL=
40 | WECHAT_THROUGH_TRAIN_PAYMENT_NOTIFY_URL=
41 | WECHAT_SCENIC_AREA_PAYMENT_NOTIFY_URL=
42 | WECHAT_SCENIC_AREA_REFUND_NOTIFY_URL=
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Komorebi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/config/autoload/mini_program.php:
--------------------------------------------------------------------------------
1 | env('WECHAT_ENABLE_ALL', false),
7 | //多个小程序用参数字段接收需要获取对应小程序的配置字段
8 | 'key' => env('WECHAT_QUERY_KEY', 'channel'),
9 | //服务重试次数
10 | 'maxattempts' => 2,
11 | //存储二维码文件路径
12 | 'qrcode_path' => BASE_PATH . '/storage/',
13 | //重试间隔
14 | 'sleep' => 20,
15 | 'config' => [
16 | //小程序1
17 | 'default' => [
18 | 'app_id' => env('WECHAT_MINI_PROGRAM_APPID', ''),
19 | 'secret' => env('WECHAT_MINI_PROGRAM_SECRET', ''),
20 | 'token' => env('WECHAT_MINI_PROGRAM_TOKEN', ''),
21 | 'aes_key' => env('WECHAT_MINI_PROGRAM_AES_KEY', '')
22 | ],
23 | //小程序2
24 | 'default2' => [
25 | 'app_id' => env('WECHAT_MINI_PROGRAM_APPID_DEFAULT2', ''),
26 | 'secret' => env('WECHAT_MINI_PROGRAM_SECRET_DEFAULT2', ''),
27 | 'token' => env('WECHAT_MINI_PROGRAM_TOKEN_DEFAULT2', ''),
28 | 'aes_key' => env('WECHAT_MINI_PROGRAM_AES_KEY_DEFAULT2', '')
29 | ]
30 | ]
31 | ];
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app/Kernel/Payment/PaymentProxy.php:
--------------------------------------------------------------------------------
1 | config->get('http', []);
20 | $config['handler'] = $container->get(HandlerStackFactory::class)->create();
21 | $this->rebind('http_client', new Client($config));
22 | $cache = $container->get(CacheInterface::class);
23 | $this->rebind('cache', $cache);
24 | $this['guzzle_handler'] = $container->get(HandlerStackFactory::class)->create();
25 | $this->paymentName = $paymentName;
26 | }
27 |
28 | public function __call($name, $arguments)
29 | {
30 | return parent::__call($name, $arguments);
31 | }
32 | }
33 |
34 |
35 |
--------------------------------------------------------------------------------
/app/Kernel/MiniProgram/MiniProgramProxy.php:
--------------------------------------------------------------------------------
1 | config->get('http', []);
20 | $config['handler'] = $container->get(HandlerStackFactory::class)->create();
21 | $this->rebind('http_client', new Client($config));
22 | $cache = $container->get(CacheInterface::class);
23 | $this->rebind('cache', $cache);
24 | $this['guzzle_handler'] = $container->get(HandlerStackFactory::class)->create();
25 | $this->miniProgramName = $miniProgramName;
26 | }
27 |
28 | public function __call($method, $args)
29 | {
30 | return parent::__call($method, $args);
31 | }
32 | }
33 |
34 |
35 |
--------------------------------------------------------------------------------
/config/autoload/devtool.php:
--------------------------------------------------------------------------------
1 | [
14 | 'amqp' => [
15 | 'consumer' => [
16 | 'namespace' => 'App\\Amqp\\Consumer',
17 | ],
18 | 'producer' => [
19 | 'namespace' => 'App\\Amqp\\Producer',
20 | ],
21 | ],
22 | 'aspect' => [
23 | 'namespace' => 'App\\Aspect',
24 | ],
25 | 'command' => [
26 | 'namespace' => 'App\\Command',
27 | ],
28 | 'controller' => [
29 | 'namespace' => 'App\\Controller',
30 | ],
31 | 'job' => [
32 | 'namespace' => 'App\\Job',
33 | ],
34 | 'listener' => [
35 | 'namespace' => 'App\\Listener',
36 | ],
37 | 'middleware' => [
38 | 'namespace' => 'App\\Middleware',
39 | ],
40 | 'Process' => [
41 | 'namespace' => 'App\\Processes',
42 | ],
43 | ],
44 | ];
45 |
--------------------------------------------------------------------------------
/test/HttpTestCase.php:
--------------------------------------------------------------------------------
1 | client = make(Testing\Client::class);
35 | // $this->client = make(Testing\HttpClient::class, ['baseUri' => 'http://127.0.0.1:9501']);
36 | }
37 |
38 | public function __call($name, $arguments)
39 | {
40 | return $this->client->{$name}(...$arguments);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/Controller/Controller.php:
--------------------------------------------------------------------------------
1 | container = $container;
42 | $this->response = $container->get(Response::class);
43 | $this->request = $container->get(RequestInterface::class);
44 | $this->logger = $container->get(LoggerFactory::class)->get();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/Kernel/Rpc/MiniProgram/Contract/QrCodeInterface.php:
--------------------------------------------------------------------------------
1 | get('payment.payment');
20 | foreach ($paymentConfig as $name => $item) {
21 | $this->proxies[$name] = make(PaymentProxy::class, [
22 | 'paymentName' => $name,
23 | 'config' => $item,
24 | 'container' => $container
25 | ]);
26 | }
27 | }
28 |
29 | /**
30 | * @param string $paymentName
31 | *
32 | * @return PaymentProxy
33 | */
34 | public function get(string $paymentName = 'default') : ?PaymentProxy
35 | {
36 | $proxy = $this->proxies[$paymentName] ?? NULL;
37 | if (!$proxy instanceof PaymentProxy) {
38 | throw new InvalidPaymentProxyException('Invalid Payment proxy.');
39 | }
40 | return $proxy;
41 | }
42 | }
43 |
44 |
45 |
--------------------------------------------------------------------------------
/test/Cases/ExampleTest.php:
--------------------------------------------------------------------------------
1 | assertTrue(true);
25 |
26 | $res = $this->get('/');
27 |
28 | $this->assertSame(0, $res['code']);
29 | $this->assertSame('Hello Hyperf.', $res['data']['message']);
30 | $this->assertSame('GET', $res['data']['method']);
31 | $this->assertSame('Hyperf', $res['data']['user']);
32 |
33 | $res = $this->get('/', ['user' => 'limx']);
34 |
35 | $this->assertSame(0, $res['code']);
36 | $this->assertSame('limx', $res['data']['user']);
37 |
38 | $res = $this->post('/', [
39 | 'user' => 'limx',
40 | ]);
41 | $this->assertSame('Hello Hyperf.', $res['data']['message']);
42 | $this->assertSame('POST', $res['data']['method']);
43 | $this->assertSame('limx', $res['data']['user']);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/Kernel/MiniProgram/MiniProgramFactory.php:
--------------------------------------------------------------------------------
1 | get('mini_program.config');
19 | foreach ($miniProgramConfig as $name => $item) {
20 | $this->proxies[$name] = make(MiniProgramProxy::class, [
21 | 'miniProgramName' => $name,
22 | 'config' => $item,
23 | 'container' => $container
24 | ]);
25 | }
26 | }
27 |
28 | /**
29 | * @param string $miniProgramName
30 | *
31 | * @return MiniProgramProxy
32 | */
33 | public function get(string $miniProgramName = 'default') : ?MiniProgramProxy
34 | {
35 | $proxy = $this->proxies[$miniProgramName] ?? NULL;
36 | if (!$proxy instanceof MiniProgramProxy) {
37 | throw new InvalidMiniProgramProxyException('Invalid MiniProgram proxy.');
38 | }
39 | return $proxy;
40 | }
41 | }
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/Kernel/Rpc/MiniProgram/BaseService.php:
--------------------------------------------------------------------------------
1 | container = $container;
41 | $this->logger = $container->get(LoggerFactory::class)->get('easywechat', 'miniprogram');
42 | $this->maxAttempts = config('mini_program.maxattempts');
43 | $this->sleep = config('mini_program.sleep');
44 | $this->qrCodePath = config('mini_program.qrcode_path');
45 | }
46 |
47 | public function send(Response $response)
48 | {
49 | return $response;
50 | }
51 | }
52 |
53 |
54 |
--------------------------------------------------------------------------------
/app/Kernel/Rpc/Payment/Contract/NotifyInterface.php:
--------------------------------------------------------------------------------
1 | get($channel);
22 | return $redis->setex(md5(self::SESSION_PREFIX . $channel . $id), 5 * 30, $session);
23 | }
24 |
25 | /**
26 | * @param string $channel
27 | * @param string $id
28 | *
29 | * @return bool|mixed|string
30 | */
31 | public static function get(string $channel, string $id)
32 | {
33 | $redis = di(RedisFactory::class)->get($channel);
34 | return $redis->get(md5(self::SESSION_PREFIX . $channel . $id));
35 | }
36 |
37 | /**
38 | * @param string $channel
39 | * @param string $id
40 | *
41 | * @return int
42 | */
43 | public static function del(string $channel, string $id)
44 | {
45 | $redis = di(RedisFactory::class)->get($channel);
46 | return $redis->del(md5(self::SESSION_PREFIX . $channel . $id));
47 | }
48 | }
49 |
50 |
51 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | # usermod -aG docker gitlab-runner
2 |
3 | stages:
4 | - build
5 | - deploy
6 |
7 | variables:
8 | PROJECT_NAME: hyperf
9 | REGISTRY_URL: registry-docker.org
10 |
11 | build_test_docker:
12 | stage: build
13 | before_script:
14 | # - git submodule sync --recursive
15 | # - git submodule update --init --recursive
16 | script:
17 | - docker build . -t $PROJECT_NAME
18 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:test
19 | - docker push $REGISTRY_URL/$PROJECT_NAME:test
20 | only:
21 | - test
22 | tags:
23 | - builder
24 |
25 | deploy_test_docker:
26 | stage: deploy
27 | script:
28 | - docker stack deploy -c deploy.test.yml --with-registry-auth $PROJECT_NAME
29 | only:
30 | - test
31 | tags:
32 | - test
33 |
34 | build_docker:
35 | stage: build
36 | before_script:
37 | # - git submodule sync --recursive
38 | # - git submodule update --init --recursive
39 | script:
40 | - docker build . -t $PROJECT_NAME
41 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME
42 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:latest
43 | - docker push $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME
44 | - docker push $REGISTRY_URL/$PROJECT_NAME:latest
45 | only:
46 | - tags
47 | tags:
48 | - builder
49 |
50 | deploy_docker:
51 | stage: deploy
52 | script:
53 | - echo SUCCESS
54 | only:
55 | - tags
56 | tags:
57 | - builder
58 |
--------------------------------------------------------------------------------
/app/Controller/IndexController.php:
--------------------------------------------------------------------------------
1 | get('default')->set('1','1');
24 |
25 | $user = $this->request->input('user', 'Hyperf');
26 | $method = $this->request->getMethod();
27 | return $this->response->success([
28 | 'user' => $user,
29 | 'method' => $method,
30 | 'message' => 'Hello Hyperf.',
31 | ]);
32 | }
33 |
34 | public function miniprogram()
35 | {
36 | $channel = $this->request->input('channel');
37 | ($this->container->get(MiniProgramFactory::class)->get($channel)->auth->session(('asdsadsadnjasnd')));
38 | }
39 |
40 | public function log()
41 | {
42 | // $this->logger->info('11111111');
43 | retry(2, function ()
44 | {
45 | echo '111111111' . "\r\n";
46 | throw new \Exception('呵呵');
47 | }, 10);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/Kernel/Rpc/Response.php:
--------------------------------------------------------------------------------
1 | code = $code;
21 | $this->data = $data;
22 | $this->msg = $msg;
23 | }
24 |
25 | /**
26 | * @param string $code
27 | */
28 | public function setCode(string $code) : void
29 | {
30 | $this->code = $code;
31 | }
32 |
33 | /**
34 | * @param array $data
35 | */
36 | public function setData(array $data) : void
37 | {
38 | $this->data = $data;
39 | }
40 |
41 | /**
42 | * @param string $msg
43 | */
44 | public function setMsg(string $msg) : void
45 | {
46 | $this->msg = $msg;
47 | }
48 |
49 | /**
50 | * @return string
51 | */
52 | public function getCode() : string
53 | {
54 | return $this->code;
55 | }
56 |
57 | /**
58 | * @return array
59 | */
60 | public function getData() : array
61 | {
62 | return $this->data;
63 | }
64 |
65 | /**
66 | * @return string
67 | */
68 | public function getMsg() : string
69 | {
70 | return $this->msg;
71 | }
72 | }
73 |
74 |
75 |
--------------------------------------------------------------------------------
/config/routes.php:
--------------------------------------------------------------------------------
1 | get(Prometheus\CollectorRegistry::class);
35 | $renderer = new Prometheus\RenderTextFormat();
36 | return $renderer->render($registry->getMetricFamilySamples());
37 | });
38 |
--------------------------------------------------------------------------------
/config/autoload/payment.php:
--------------------------------------------------------------------------------
1 | [
6 | 'default' => [
7 | 'sandbox' => env('WECHAT_PAYMENT_SANDBOX', false),
8 | 'app_id' => env('WECHAT_PAYMENT_APPID', ''),
9 | 'mch_id' => env('WECHAT_PAYMENT_MCH_ID', ''),
10 | 'key' => env('WECHAT_PAYMENT_KEY', BASE_PATH . '/private/payment/default/apiclient_cert.pem'),
11 | 'cert_path' => env('WECHAT_PAYMENT_CERT_PATH', BASE_PATH . '/private/payment/default/apiclient_key.pem'),
12 | 'key_path' => env('WECHAT_PAYMENT_KEY_PATH', ''),
13 | 'notify_url' => env('WECHAT_PAYMENT_NOTIFY_URL', ''),
14 | 'refund_notify_url' => env('WECHAT_REFUND_NOTIFY_URL', ''),
15 | ],
16 | 'default1' => [
17 | 'sandbox' => env('WECHAT_PAYMENT_SANDBOX', false),
18 | 'app_id' => env('WECHAT_PAYMENT_APPID', ''),
19 | 'mch_id' => env('WECHAT_PAYMENT_MCH_ID', ''),
20 | 'key' => env('WECHAT_PAYMENT_KEY', BASE_PATH . '/private/payment/default1/apiclient_cert.pem'),
21 | 'cert_path' => env('WECHAT_PAYMENT_CERT_PATH', BASE_PATH . '/private/payment/default1/apiclient_key.pem'),
22 | 'key_path' => env('WECHAT_PAYMENT_KEY_PATH', ''),
23 | 'notify_url' => env('WECHAT_PAYMENT_NOTIFY_URL', ''),
24 | 'refund_notify_url' => env('WECHAT_REFUND_NOTIFY_URL', ''),
25 | ]
26 | ],
27 | //服务重试次数
28 | 'maxattempts' => 3,
29 | //重试休眠时间
30 | 'sleep' => 20
31 |
32 | ];
33 |
34 |
--------------------------------------------------------------------------------
/app/Listener/BootApplicationListener.php:
--------------------------------------------------------------------------------
1 | container = $container;
23 | $this->logger = $this->container->get(StdoutLoggerInterface::class);
24 | $this->config = $this->container->get(ConfigInterface::class);
25 | }
26 |
27 | public function listen() : array
28 | {
29 | return [
30 | BootApplication::class,
31 | ];
32 | }
33 |
34 | public function process(object $event)
35 | {
36 | $isMiniProgram = $this->config->get('mini_program');
37 | if ($isMiniProgram) {
38 | $enable = $isMiniProgram['enable_all'];
39 | $key = $isMiniProgram['key'];
40 | if (empty($key) && $enable) {
41 | $this->logger->error(sprintf('MiniProgram {Key} Not Empty Or Must String!❌'));
42 | exit(SIGTERM);
43 | }
44 | $config = $isMiniProgram['config'];
45 | foreach ($config as $name => $item) {
46 | $this->logger->info(sprintf('MiniProgram [%s] Config ✅.', $name));
47 | }
48 | }
49 | }
50 | }
51 |
52 |
53 |
--------------------------------------------------------------------------------
/app/Listener/DbQueryExecutedListener.php:
--------------------------------------------------------------------------------
1 | logger = $container->get(LoggerFactory::class)->get('sql');
36 | }
37 |
38 | public function listen(): array
39 | {
40 | return [
41 | QueryExecuted::class,
42 | ];
43 | }
44 |
45 | /**
46 | * @param QueryExecuted $event
47 | */
48 | public function process(object $event)
49 | {
50 | if ($event instanceof QueryExecuted) {
51 | $sql = $event->sql;
52 | if (! Arr::isAssoc($event->bindings)) {
53 | foreach ($event->bindings as $key => $value) {
54 | $sql = Str::replaceFirst('?', "'{$value}'", $sql);
55 | }
56 | }
57 |
58 | $this->logger->info(sprintf('[%s:%s] %s', $event->connectionName, $event->time, $sql));
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/config/autoload/databases.php:
--------------------------------------------------------------------------------
1 | [
14 | 'driver' => env('DB_DRIVER', 'mysql'),
15 | 'host' => env('DB_HOST', 'localhost'),
16 | 'port' => env('DB_PORT', 3306),
17 | 'database' => env('DB_DATABASE', 'hyperf'),
18 | 'username' => env('DB_USERNAME', 'root'),
19 | 'password' => env('DB_PASSWORD', ''),
20 | 'charset' => env('DB_CHARSET', 'utf8mb4'),
21 | 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
22 | 'prefix' => env('DB_PREFIX', ''),
23 | 'pool' => [
24 | 'min_connections' => 1,
25 | 'max_connections' => 10,
26 | 'connect_timeout' => 10.0,
27 | 'wait_timeout' => 3.0,
28 | 'heartbeat' => -1,
29 | 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60),
30 | ],
31 | 'cache' => [
32 | 'handler' => Hyperf\ModelCache\Handler\RedisHandler::class,
33 | 'cache_key' => '{mc:%s:m:%s}:%s:%s',
34 | 'prefix' => 'default',
35 | 'ttl' => 3600 * 24,
36 | 'empty_model_ttl' => 600,
37 | 'load_script' => true,
38 | ],
39 | 'commands' => [
40 | 'gen:model' => [
41 | 'path' => 'app/Model',
42 | 'force_casts' => true,
43 | 'inheritance' => 'Model',
44 | 'uses' => '',
45 | 'refresh_fillable' => true,
46 | 'table_mapping' => [],
47 | ],
48 | ],
49 | ],
50 | ];
51 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Default Dockerfile
2 | #
3 | # @link https://www.hyperf.io
4 | # @document https://doc.hyperf.io
5 | # @contact group@hyperf.io
6 | # @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
7 |
8 | FROM hyperf/hyperf:7.2-alpine-v3.9-cli
9 | LABEL maintainer="Hyperf Developers " version="1.0" license="MIT" app.name="Hyperf"
10 |
11 | ##
12 | # ---------- env settings ----------
13 | ##
14 | # --build-arg timezone=Asia/Shanghai
15 | ARG timezone
16 |
17 | ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \
18 | APP_ENV=prod \
19 | SCAN_CACHEABLE=(true)
20 |
21 | # update
22 | RUN set -ex \
23 | && apk update \
24 | # install composer
25 | && cd /tmp \
26 | && wget https://mirrors.aliyun.com/composer/composer.phar \
27 | && chmod u+x composer.phar \
28 | && mv composer.phar /usr/local/bin/composer \
29 | # show php version and extensions
30 | && php -v \
31 | && php -m \
32 | && php --ri swoole \
33 | # ---------- some config ----------
34 | && cd /etc/php7 \
35 | # - config PHP
36 | && { \
37 | echo "upload_max_filesize=100M"; \
38 | echo "post_max_size=108M"; \
39 | echo "memory_limit=1024M"; \
40 | echo "date.timezone=${TIMEZONE}"; \
41 | } | tee conf.d/99_overrides.ini \
42 | # - config timezone
43 | && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \
44 | && echo "${TIMEZONE}" > /etc/timezone \
45 | # ---------- clear works ----------
46 | && rm -rf /var/cache/apk/* /tmp/* /usr/share/man \
47 | && echo -e "\033[42;37m Build Completed :).\033[0m\n"
48 |
49 | WORKDIR /opt/www
50 |
51 | # Composer Cache
52 | # COPY ./composer.* /opt/www/
53 | # RUN composer install --no-dev --no-scripts
54 |
55 | COPY . /opt/www
56 | RUN composer install --no-dev -o && php bin/hyperf.php
57 |
58 | EXPOSE 9501
59 |
60 | ENTRYPOINT ["php", "/opt/www/bin/hyperf.php", "start"]
61 |
--------------------------------------------------------------------------------
/app/Exception/Handler/BusinessExceptionHandler.php:
--------------------------------------------------------------------------------
1 | container = $container;
44 | $this->response = $container->get(Response::class);
45 | $this->logger = $container->get(StdoutLoggerInterface::class);
46 | }
47 |
48 | public function handle(Throwable $throwable, ResponseInterface $response)
49 | {
50 | if ($throwable instanceof HttpException) {
51 | return $this->response->handleException($throwable);
52 | }
53 |
54 | if ($throwable instanceof BusinessException) {
55 | $this->logger->warning(format_throwable($throwable));
56 |
57 | return $this->response->fail($throwable->getCode(), $throwable->getMessage());
58 | }
59 |
60 | $this->logger->error(format_throwable($throwable));
61 |
62 | return $this->response->fail(ErrorCode::SERVER_ERROR, 'Server Error');
63 | }
64 |
65 | public function isValid(Throwable $throwable): bool
66 | {
67 | return true;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/Kernel/Context/Coroutine.php:
--------------------------------------------------------------------------------
1 | container = $container;
41 | $this->logger = $container->get(StdoutLoggerInterface::class);
42 | if ($container->has(FormatterInterface::class)) {
43 | $this->formatter = $container->get(FormatterInterface::class);
44 | }
45 | }
46 |
47 | /**
48 | * @return int Returns the coroutine ID of the coroutine just created.
49 | * Returns -1 when coroutine create failed.
50 | */
51 | public function create(callable $callable): int
52 | {
53 | $id = Utils\Coroutine::id();
54 | $result = SwooleCoroutine::create(function () use ($callable, $id) {
55 | try {
56 | Utils\Context::copy($id);
57 | call($callable);
58 | } catch (Throwable $throwable) {
59 | if ($this->formatter) {
60 | $this->logger->warning($this->formatter->format($throwable));
61 | } else {
62 | $this->logger->warning((string) $throwable);
63 | }
64 | }
65 | });
66 | return is_int($result) ? $result : -1;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/Kernel/Rpc/Payment/BaseService.php:
--------------------------------------------------------------------------------
1 | container = $container;
37 | $this->logger = $container->get(LoggerFactory::class)->get('easywechat', 'payment');
38 | $this->maxAttempts = config('payment.maxattempts');
39 | $this->sleep = config('payment.sleep');
40 | }
41 |
42 | public function send($entity)
43 | {
44 | return $entity;
45 | }
46 |
47 | /**
48 | * @param \Hyperf\HttpServer\Contract\RequestInterface $request
49 | *
50 | * @return \Symfony\Component\HttpFoundation\Request
51 | */
52 | protected function buildSymfonyRequest(RequestInterface $request) : Request
53 | {
54 | $get = $request->getQueryParams();
55 | $post = $request->getParsedBody();
56 | $cookie = $request->getCookieParams();
57 | $files = $request->getUploadedFiles();
58 | $server = $request->getServerParams();
59 | $uploadFiles = $request->getUploadedFiles() ?? [];
60 | $xml = $request->getBody()->getContents();
61 | /** @var \Hyperf\HttpMessage\Upload\UploadedFile $v */
62 | foreach ($uploadFiles as $k => $v) {
63 | $files[$k] = $v->toArray();
64 | }
65 | return new Request($get, $post, [], $cookie, $files, $server, $xml);
66 | }
67 | }
68 |
69 |
70 |
--------------------------------------------------------------------------------
/app/Kernel/ClassMap/Coroutine.php:
--------------------------------------------------------------------------------
1 | get(BCoroutine::class)->create($callable);
67 | }
68 |
69 | public static function inCoroutine(): bool
70 | {
71 | return Coroutine::id() > 0;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/config/autoload/server.php:
--------------------------------------------------------------------------------
1 | SWOOLE_PROCESS,
19 | 'servers' => [
20 | [
21 | 'name' => 'http',
22 | 'type' => Server::SERVER_HTTP,
23 | 'host' => '0.0.0.0',
24 | 'port' => 9501,
25 | 'sock_type' => SWOOLE_SOCK_TCP,
26 | 'callbacks' => [
27 | SwooleEvent::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'],
28 | ],
29 | ],
30 | [
31 | 'name' => 'jsonrpc',
32 | 'type' => Server::SERVER_BASE,
33 | 'host' => '0.0.0.0',
34 | 'port' => 9504,
35 | 'sock_type' => SWOOLE_SOCK_TCP,
36 | 'callbacks' => [
37 | SwooleEvent::ON_RECEIVE => [Hyperf\JsonRpc\TcpServer::class, 'onReceive'],
38 | ],
39 | 'settings' => [
40 | 'open_length_check' => true,
41 | 'package_length_type' => 'N',
42 | 'package_length_offset' => 0,
43 | 'package_body_offset' => 4,
44 | 'package_max_length' => 1024 * 1024 * 2
45 | ],
46 | ],
47 | ],
48 | 'settings' => [
49 | 'enable_coroutine' => true,
50 | 'worker_num' => 4,
51 | 'pid_file' => BASE_PATH . '/runtime/hyperf.pid',
52 | 'open_tcp_nodelay' => true,
53 | 'max_coroutine' => 100000,
54 | 'open_http2_protocol' => true,
55 | 'max_request' => 0,
56 | 'socket_buffer_size' => 2 * 1024 * 1024,
57 | 'package_max_length' => 2 * 1024 * 1024,
58 | ],
59 | 'callbacks' => [
60 | SwooleEvent::ON_BEFORE_START => [Hyperf\Framework\Bootstrap\ServerStartCallback::class, 'beforeStart'],
61 | SwooleEvent::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'],
62 | SwooleEvent::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'],
63 | SwooleEvent::ON_WORKER_EXIT => [Hyperf\Framework\Bootstrap\WorkerExitCallback::class, 'onWorkerExit'],
64 | ],
65 | ];
66 |
--------------------------------------------------------------------------------
/app/Listener/QueueHandleListener.php:
--------------------------------------------------------------------------------
1 | logger = $container->get(LoggerFactory::class)->get('queue');
35 | }
36 |
37 | public function listen(): array
38 | {
39 | return [
40 | AfterHandle::class,
41 | BeforeHandle::class,
42 | FailedHandle::class,
43 | RetryHandle::class,
44 | ];
45 | }
46 |
47 | public function process(object $event)
48 | {
49 | if ($event instanceof Event && $event->message->job()) {
50 | $job = $event->message->job();
51 | $jobClass = get_class($job);
52 | if ($job instanceof AnnotationJob) {
53 | $jobClass = sprintf('Job[%s@%s]', $job->class, $job->method);
54 | }
55 | $date = date('Y-m-d H:i:s');
56 |
57 | switch (true) {
58 | case $event instanceof BeforeHandle:
59 | $this->logger->info(sprintf('[%s] Processing %s.', $date, $jobClass));
60 | break;
61 | case $event instanceof AfterHandle:
62 | $this->logger->info(sprintf('[%s] Processed %s.', $date, $jobClass));
63 | break;
64 | case $event instanceof FailedHandle:
65 | $this->logger->error(sprintf('[%s] Failed %s.', $date, $jobClass));
66 | $this->logger->error(format_throwable($event->getThrowable()));
67 | break;
68 | case $event instanceof RetryHandle:
69 | $this->logger->warning(sprintf('[%s] Retried %s.', $date, $jobClass));
70 | break;
71 | }
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/config/autoload/metric.php:
--------------------------------------------------------------------------------
1 | env('METRIC_DRIVER', 'prometheus'),
17 | 'use_standalone_process' => env('METRIC_USE_STANDALONE_PROCESS', true),
18 | 'enable_default_metric' => env('METRIC_ENABLE_DEFAULT_METRIC', true),
19 | 'default_metric_interval' => env('DEFAULT_METRIC_INTERVAL', 5),
20 | 'metric' => [
21 | 'prometheus' => [
22 | 'driver' => Hyperf\Metric\Adapter\Prometheus\MetricFactory::class,
23 | 'mode' => Constants::SCRAPE_MODE,
24 | 'namespace' => env('APP_NAME', 'skeleton'),
25 | 'scrape_host' => env('PROMETHEUS_SCRAPE_HOST', '0.0.0.0'),
26 | 'scrape_port' => env('PROMETHEUS_SCRAPE_PORT', '9502'),
27 | 'scrape_path' => env('PROMETHEUS_SCRAPE_PATH', '/metrics'),
28 | 'push_host' => env('PROMETHEUS_PUSH_HOST', '0.0.0.0'),
29 | 'push_port' => env('PROMETHEUS_PUSH_PORT', '9091'),
30 | 'push_interval' => env('PROMETHEUS_PUSH_INTERVAL', 5),
31 | ],
32 | 'statsd' => [
33 | 'driver' => Hyperf\Metric\Adapter\StatsD\MetricFactory::class,
34 | 'namespace' => env('APP_NAME', 'skeleton'),
35 | 'udp_host' => env('STATSD_UDP_HOST', '127.0.0.1'),
36 | 'udp_port' => env('STATSD_UDP_PORT', '8125'),
37 | 'enable_batch' => env('STATSD_ENABLE_BATCH', true),
38 | 'push_interval' => env('STATSD_PUSH_INTERVAL', 5),
39 | 'sample_rate' => env('STATSD_SAMPLE_RATE', 1.0),
40 | ],
41 | 'influxdb' => [
42 | 'driver' => Hyperf\Metric\Adapter\InfluxDB\MetricFactory::class,
43 | 'namespace' => env('APP_NAME', 'skeleton'),
44 | 'host' => env('INFLUXDB_HOST', '127.0.0.1'),
45 | 'port' => env('INFLUXDB_PORT', '8086'),
46 | 'username' => env('INFLUXDB_USERNAME', ''),
47 | 'password' => env('INFLUXDB_PASSWORD', ''),
48 | 'dbname' => env('INFLUXDB_DBNAME', true),
49 | 'push_interval' => env('INFLUXDB_PUSH_INTERVAL', 5),
50 | 'auto_create_db' => env('INFLUXDB_AUTO_CREATE_DB', true),
51 | ],
52 | 'noop' => [
53 | 'driver' => Hyperf\Metric\Adapter\NoOp\MetricFactory::class,
54 | ],
55 | ],
56 | ];
57 | */
58 |
--------------------------------------------------------------------------------
/config/autoload/logger.php:
--------------------------------------------------------------------------------
1 | [
18 | 'handler' => [
19 | 'class' => Monolog\Handler\RotatingFileHandler::class,
20 | 'constructor' => [
21 | 'filename' => BASE_PATH . '/runtime/logs/hyperf.log',
22 | 'level' => Monolog\Logger::DEBUG,
23 | ],
24 | ],
25 | 'formatter' => [
26 | 'class' => Monolog\Formatter\LineFormatter::class,
27 | 'constructor' => [
28 | 'format' => NULL,
29 | 'dateFormat' => 'Y-m-d H:i:s',
30 | 'allowInlineLineBreaks' => true,
31 | ],
32 | ],
33 | 'processors' => [
34 | [
35 | 'class' => Log\AppendRequestIdProcessor::class,
36 | ],
37 | ],
38 | ],
39 | 'miniprogram' => [
40 | 'handler' => [
41 | 'class' => Monolog\Handler\RotatingFileHandler::class,
42 | 'constructor' => [
43 | 'filename' => BASE_PATH . '/runtime/logs/hyperf.log',
44 | 'level' => Monolog\Logger::DEBUG,
45 | ],
46 | ],
47 | 'formatter' => [
48 | 'class' => Monolog\Formatter\LineFormatter::class,
49 | 'constructor' => [
50 | 'format' => NULL,
51 | 'dateFormat' => 'Y-m-d H:i:s',
52 | 'allowInlineLineBreaks' => true,
53 | ],
54 | ],
55 | 'processors' => [
56 | [
57 | 'class' => Log\AppendRequestIdProcessor::class,
58 | ],
59 | ],
60 | ],
61 | 'payment' => [
62 | 'handler' => [
63 | 'class' => Monolog\Handler\RotatingFileHandler::class,
64 | 'constructor' => [
65 | 'filename' => BASE_PATH . '/runtime/logs/hyperf.log',
66 | 'level' => Monolog\Logger::DEBUG,
67 | ],
68 | ],
69 | 'formatter' => [
70 | 'class' => Monolog\Formatter\LineFormatter::class,
71 | 'constructor' => [
72 | 'format' => NULL,
73 | 'dateFormat' => 'Y-m-d H:i:s',
74 | 'allowInlineLineBreaks' => true,
75 | ],
76 | ],
77 | 'processors' => [
78 | [
79 | 'class' => Log\AppendRequestIdProcessor::class,
80 | ],
81 |
82 | ]
83 | ]
84 | ];
85 |
--------------------------------------------------------------------------------
/config/autoload/opentracing.php:
--------------------------------------------------------------------------------
1 | env('TRACER_DRIVER', 'jaeger'),
17 | 'enable' => [
18 | 'guzzle' => env('TRACER_ENABLE_GUZZLE', true),
19 | 'redis' => env('TRACER_ENABLE_REDIS', true),
20 | 'db' => env('TRACER_ENABLE_DB', true),
21 | 'method' => env('TRACER_ENABLE_METHOD', true),
22 | ],
23 | 'tracer' => [
24 | 'zipkin' => [
25 | 'driver' => Hyperf\Tracer\Adapter\ZipkinTracerFactory::class,
26 | 'app' => [
27 | 'name' => env('APP_NAME', 'skeleton'),
28 | // Hyperf will detect the system info automatically as the value if ipv4, ipv6, port is null
29 | 'ipv4' => '127.0.0.1',
30 | 'ipv6' => null,
31 | 'port' => 9501,
32 | ],
33 | 'options' => [
34 | 'endpoint_url' => env('ZIPKIN_ENDPOINT_URL', 'http://localhost:9411/api/v2/spans'),
35 | 'timeout' => env('ZIPKIN_TIMEOUT', 1),
36 | ],
37 | 'sampler' => BinarySampler::createAsAlwaysSample(),
38 | ],
39 | 'jaeger' => [
40 | 'driver' => Hyperf\Tracer\Adapter\JaegerTracerFactory::class,
41 | 'name' => env('APP_NAME', 'skeleton'),
42 | 'options' => [
43 | /*
44 | * You can uncomment the sampler lines to use custom strategy.
45 | *
46 | * For more available configurations,
47 | * @see https://github.com/jonahgeorge/jaeger-client-php
48 | */
49 | 'sampler' => [
50 | 'type' => SAMPLER_TYPE_CONST,
51 | 'param' => true,
52 | ],
53 | 'local_agent' => [
54 | 'reporting_host' => env('JAEGER_REPORTING_HOST', 'localhost'),
55 | 'reporting_port' => env('JAEGER_REPORTING_PORT', 5775),
56 | ],
57 | ],
58 | ],
59 | ],
60 | 'tags' => [
61 | 'http_client' => [
62 | 'http.url' => 'http.url',
63 | 'http.method' => 'http.method',
64 | 'http.status_code' => 'http.status_code',
65 | ],
66 | 'redis' => [
67 | 'arguments' => 'arguments',
68 | 'result' => 'result',
69 | ],
70 | 'db' => [
71 | 'db.query' => 'db.query',
72 | 'db.statement' => 'db.statement',
73 | 'db.query_time' => 'db.query_time',
74 | ],
75 | ],
76 | ];
77 |
--------------------------------------------------------------------------------
/app/Kernel/Functions.php:
--------------------------------------------------------------------------------
1 | get($id);
30 | }
31 |
32 | return $container;
33 | }
34 | }
35 |
36 | if (! function_exists('format_throwable')) {
37 | /**
38 | * Format a throwable to string.
39 | */
40 | function format_throwable(Throwable $throwable): string
41 | {
42 | return di()->get(FormatterInterface::class)->format($throwable);
43 | }
44 | }
45 |
46 | if (! function_exists('queue_push')) {
47 | /**
48 | * Push a job to async queue.
49 | */
50 | function queue_push(JobInterface $job, int $delay = 0, string $key = 'default'): bool
51 | {
52 | $driver = di()->get(DriverFactory::class)->get($key);
53 | return $driver->push($job, $delay);
54 | }
55 | }
56 | if (!function_exists('verifyIp')) {
57 | function verifyIp($realip) {
58 | return filter_var($realip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
59 | }
60 | }
61 | if (!function_exists('getClientIp')) {
62 | function getClientIp() {
63 | try {
64 | /**
65 | * @var ServerRequestInterface $request
66 | */
67 | $request = Context::get(ServerRequestInterface::class);
68 | $ip_addr = $request->getHeaderLine('x-forwarded-for');
69 | if (verifyIp($ip_addr)) {
70 | return $ip_addr;
71 | }
72 | $ip_addr = $request->getHeaderLine('remote-host');
73 | if (verifyIp($ip_addr)) {
74 | return $ip_addr;
75 | }
76 | $ip_addr = $request->getHeaderLine('x-real-ip');
77 | if (verifyIp($ip_addr)) {
78 | return $ip_addr;
79 | }
80 | $ip_addr = $request->getServerParams()['remote_addr'] ?? '0.0.0.0';
81 | if (verifyIp($ip_addr)) {
82 | return $ip_addr;
83 | }
84 | } catch (Throwable $e) {
85 | return '0.0.0.0';
86 | }
87 | return '0.0.0.0';
88 | }
89 | }
90 |
91 |
--------------------------------------------------------------------------------
/.php_cs:
--------------------------------------------------------------------------------
1 | setRiskyAllowed(true)
14 | ->setRules([
15 | '@PSR2' => true,
16 | '@Symfony' => true,
17 | '@DoctrineAnnotation' => true,
18 | '@PhpCsFixer' => true,
19 | 'header_comment' => [
20 | 'commentType' => 'PHPDoc',
21 | 'header' => $header,
22 | 'separate' => 'none',
23 | 'location' => 'after_declare_strict',
24 | ],
25 | 'array_syntax' => [
26 | 'syntax' => 'short'
27 | ],
28 | 'list_syntax' => [
29 | 'syntax' => 'short'
30 | ],
31 | 'concat_space' => [
32 | 'spacing' => 'one'
33 | ],
34 | 'blank_line_before_statement' => [
35 | 'statements' => [
36 | 'declare',
37 | ],
38 | ],
39 | 'general_phpdoc_annotation_remove' => [
40 | 'annotations' => [
41 | 'author'
42 | ],
43 | ],
44 | 'ordered_imports' => [
45 | 'imports_order' => [
46 | 'class', 'function', 'const',
47 | ],
48 | 'sort_algorithm' => 'alpha',
49 | ],
50 | 'single_line_comment_style' => [
51 | 'comment_types' => [
52 | ],
53 | ],
54 | 'yoda_style' => [
55 | 'always_move_variable' => false,
56 | 'equal' => false,
57 | 'identical' => false,
58 | ],
59 | 'phpdoc_align' => [
60 | 'align' => 'left',
61 | ],
62 | 'multiline_whitespace_before_semicolons' => [
63 | 'strategy' => 'no_multi_line',
64 | ],
65 | 'constant_case' => [
66 | 'case' => 'lower',
67 | ],
68 | 'class_attributes_separation' => true,
69 | 'combine_consecutive_unsets' => true,
70 | 'declare_strict_types' => true,
71 | 'linebreak_after_opening_tag' => true,
72 | 'lowercase_static_reference' => true,
73 | 'no_useless_else' => true,
74 | 'no_unused_imports' => true,
75 | 'not_operator_with_successor_space' => true,
76 | 'not_operator_with_space' => false,
77 | 'ordered_class_elements' => true,
78 | 'php_unit_strict' => false,
79 | 'phpdoc_separation' => false,
80 | 'single_quote' => true,
81 | 'standardize_not_equals' => true,
82 | 'multiline_comment_opening_closing' => true,
83 | ])
84 | ->setFinder(
85 | PhpCsFixer\Finder::create()
86 | ->exclude('public')
87 | ->exclude('runtime')
88 | ->exclude('vendor')
89 | ->in(__DIR__)
90 | )
91 | ->setUsingCache(false);
92 |
--------------------------------------------------------------------------------
/app/Kernel/Http/Response.php:
--------------------------------------------------------------------------------
1 | container = $container;
38 | $this->response = $container->get(ResponseInterface::class);
39 | }
40 |
41 | public function success($data = []) : PsrResponseInterface
42 | {
43 | return $this->response->json([
44 | 'code' => 0,
45 | 'data' => $data,
46 | ]);
47 | }
48 |
49 | public function fail($code, $message = '') : PsrResponseInterface
50 | {
51 | return $this->response->json([
52 | 'code' => $code,
53 | 'message' => $message,
54 | ]);
55 | }
56 |
57 | public function redirect($url, $status = 302) : PsrResponseInterface
58 | {
59 | return $this->response()
60 | ->withAddedHeader('Location', (string)$url)
61 | ->withStatus($status);
62 | }
63 |
64 | public function cookie(Cookie $cookie)
65 | {
66 | $response = $this->response()->withCookie($cookie);
67 | Context::set(PsrResponseInterface::class, $response);
68 | return $this;
69 | }
70 |
71 | public function handleException(HttpException $throwable) : PsrResponseInterface
72 | {
73 | return $this->response()
74 | ->withAddedHeader('Server', 'Hyperf')
75 | ->withStatus($throwable->getStatusCode())
76 | ->withBody(new SwooleStream($throwable->getMessage()));
77 | }
78 |
79 | public function response() : PsrResponseInterface
80 | {
81 | return Context::get(PsrResponseInterface::class);
82 | }
83 |
84 | /**
85 | * @param string $xml
86 | * @param int $statusCode
87 | *
88 | * @return \Psr\Http\Message\ResponseInterface
89 | */
90 | public function toWechatXML(string $xml, int $statusCode = 200) : PsrResponseInterface
91 | {
92 | return $this->response()
93 | ->withStatus($statusCode)
94 | ->withAddedHeader('content-type', 'application/xml; charset=utf-8')
95 | ->withBody(new SwooleStream($xml));
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/config/autoload/services.php:
--------------------------------------------------------------------------------
1 | value(function ()
11 | {
12 | $consumers = [];
13 | // 这里示例自动创建代理消费者类的配置形式,顾存在 name 和 service 两个配置项,这里的做法不是唯一的,仅说明可以通过 PHP 代码来生成配置
14 | // 下面的 FooServiceInterface 和 BarServiceInterface 仅示例多服务,并不是在文档示例中真实存在的
15 | $services = [
16 | 'AuthService' => AuthInterface::class,
17 | 'QrCodeService' => QrCodeInterface::class,
18 | 'OrderService' => OrderInterface::class,
19 | 'NotifyService' => NotifyInterface::class
20 | ];
21 | foreach ($services as $name => $interface) {
22 | $consumers[] = [
23 | 'name' => $name,
24 | 'service' => $interface,
25 | 'registry' => [
26 | 'protocol' => 'consul',
27 | 'address' => 'http://127.0.0.1:8500',
28 | ],
29 | 'id' => $interface,
30 | // 服务提供者的服务协议,可选,默认值为 jsonrpc-http
31 | // 可选 jsonrpc-http jsonrpc jsonrpc-tcp-length-check
32 | 'protocol' => 'jsonrpc-tcp-length-check',
33 | // 负载均衡算法,可选,默认值为 random
34 | 'load_balancer' => 'random',
35 | // 这个消费者要从哪个服务中心获取节点信息,如不配置则不会从服务中心获取节点信息
36 | // 如果没有指定上面的 registry 配置,即为直接对指定的节点进行消费,通过下面的 nodes 参数来配置服务提供者的节点信息
37 | 'nodes' => [
38 | ['host' => '127.0.0.1', 'port' => 9504],
39 | ],
40 | // 配置项,会影响到 Packer 和 Transporter
41 | 'options' => [
42 | 'connect_timeout' => 5.0,
43 | 'recv_timeout' => 5.0,
44 | 'settings' => [
45 | // 根据协议不同,区分配置
46 | // 'open_eof_split' => true,
47 | // 'package_eof' => "\r\n",
48 | 'open_length_check' => true,
49 | 'package_length_type' => 'N',
50 | 'package_length_offset' => 0,
51 | 'package_body_offset' => 4,
52 | ],
53 | // 当使用 JsonRpcPoolTransporter 时会用到以下配置
54 | 'pool' => [
55 | 'min_connections' => 1,
56 | 'max_connections' => 50,
57 | 'connect_timeout' => 10.0,
58 | 'wait_timeout' => 3.0,
59 | 'heartbeat' => -1,
60 | 'max_idle_time' => 60.0,
61 | ],
62 | ]
63 | ];
64 | }
65 | return $consumers;
66 | }),
67 | ];
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hyperf/biz-skeleton",
3 | "type": "project",
4 | "keywords": [
5 | "php",
6 | "swoole",
7 | "framework",
8 | "hyperf",
9 | "microservice",
10 | "middleware"
11 | ],
12 | "description": "A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices and middlewares.",
13 | "license": "MIT",
14 | "require": {
15 | "php": ">=7.4",
16 | "ext-json": "*",
17 | "ext-openssl": "*",
18 | "ext-pdo": "*",
19 | "ext-pdo_mysql": "*",
20 | "ext-redis": "*",
21 | "ext-swoole": ">=4.5",
22 | "hyperf/async-queue": "2.0.*",
23 | "hyperf/cache": "2.0.*",
24 | "hyperf/command": "2.0.*",
25 | "hyperf/config": "2.0.*",
26 | "hyperf/config-aliyun-acm": "^2.0",
27 | "hyperf/constants": "2.0.*",
28 | "hyperf/consul": "^2.0",
29 | "hyperf/contract": "2.0.*",
30 | "hyperf/database": "2.0.*",
31 | "hyperf/db-connection": "2.0.*",
32 | "hyperf/di": "2.0.*",
33 | "hyperf/dispatcher": "2.0.*",
34 | "hyperf/event": "2.0.*",
35 | "hyperf/exception-handler": "2.0.*",
36 | "hyperf/framework": "2.0.*",
37 | "hyperf/guzzle": "2.0.*",
38 | "hyperf/http-server": "2.0.*",
39 | "hyperf/json-rpc": "^2.0",
40 | "hyperf/logger": "2.0.*",
41 | "hyperf/metric": "^2.0",
42 | "hyperf/model-cache": "2.0.*",
43 | "hyperf/pool": "2.0.*",
44 | "hyperf/process": "2.0.*",
45 | "hyperf/redis": "2.0.*",
46 | "hyperf/retry": "^2.0",
47 | "hyperf/rpc-client": "^2.0",
48 | "hyperf/rpc-server": "^2.0",
49 | "hyperf/server": "2.0.*",
50 | "hyperf/service-governance": "^2.0",
51 | "hyperf/tracer": "^2.0",
52 | "hyperf/utils": "2.0.*",
53 | "jonahgeorge/jaeger-client-php": "^0.6.0",
54 | "overtrue/wechat": "~4.0",
55 | "symfony/property-access": "^5.1",
56 | "symfony/serializer": "^5.1"
57 | },
58 | "require-dev": {
59 | "friendsofphp/php-cs-fixer": "^2.14",
60 | "hyperf/devtool": "2.0.*",
61 | "hyperf/testing": "2.0.*",
62 | "mockery/mockery": "^1.0",
63 | "phpstan/phpstan": "^0.12.18",
64 | "swoole/ide-helper": "dev-master",
65 | "symfony/var-dumper": "^5.1"
66 | },
67 | "autoload": {
68 | "psr-4": {
69 | "App\\": "app/"
70 | },
71 | "files": [
72 | "app/Kernel/Functions.php"
73 | ]
74 | },
75 | "autoload-dev": {
76 | "psr-4": {
77 | "HyperfTest\\": "test/"
78 | }
79 | },
80 | "minimum-stability": "dev",
81 | "prefer-stable": true,
82 | "config": {
83 | "sort-packages": true
84 | },
85 | "extra": [],
86 | "scripts": {
87 | "post-root-package-install": [
88 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
89 | ],
90 | "post-autoload-dump": [
91 | "rm -rf runtime/container"
92 | ],
93 | "analyse": "phpstan analyse --memory-limit 300M -l 0 -c phpstan.neon ./app ./config",
94 | "cs-fix": "php-cs-fixer fix $1",
95 | "start": "php ./bin/hyperf.php start",
96 | "test": "co-phpunit -c phpunit.xml --colors=always"
97 | },
98 | "repositories": {
99 | "packagist": {
100 | "type": "composer",
101 | "url": "https://mirrors.aliyun.com/composer"
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/app/Helper/NumberHelper.php:
--------------------------------------------------------------------------------
1 | = 1024 && $i < 5; $i++) {
22 | $size /= 1024;
23 | }
24 |
25 | return round($size, $dec) . $delimiter . ($units[$i] ?? Consts::UNKNOWN);
26 | }
27 |
28 | /**
29 | * 值是否在某范围内
30 | *
31 | * @param int|float $val 值
32 | * @param int|float $min 小值
33 | * @param int|float $max 大值
34 | *
35 | * @return bool
36 | */
37 | public static function inRange($val, $min, $max): bool
38 | {
39 | $val = floatval($val);
40 | $min = floatval($min);
41 | $max = floatval($max);
42 | return $val >= $min && $val <= $max;
43 | }
44 |
45 | /**
46 | * 对数列求和,忽略非数值.
47 | *
48 | * @param mixed ...$vals
49 | *
50 | * @return float
51 | */
52 | public static function sum(...$vals): float
53 | {
54 | $res = 0;
55 | foreach ($vals as $val) {
56 | if (is_numeric($val)) {
57 | $res += floatval($val);
58 | }
59 | }
60 |
61 | return $res;
62 | }
63 |
64 | /**
65 | * 对数列求平均值,忽略非数值.
66 | *
67 | * @param mixed ...$vals
68 | *
69 | * @return float
70 | */
71 | public static function average(...$vals): float
72 | {
73 | $res = 0;
74 | $count = 0;
75 | $total = 0;
76 | foreach ($vals as $val) {
77 | if (is_numeric($val)) {
78 | $total += floatval($val);
79 | $count++;
80 | }
81 | }
82 |
83 | if ($count > 0) {
84 | $res = $total / $count;
85 | }
86 |
87 | return $res;
88 | }
89 |
90 | /**
91 | * 获取地理距离/米.
92 | * 参数分别为两点的经度和纬度.lat:-90~90,lng:-180~180.
93 | *
94 | * @param float $lng1 起点经度
95 | * @param float $lat1 起点纬度
96 | * @param float $lng2 终点经度
97 | * @param float $lat2 终点纬度
98 | *
99 | * @return float
100 | */
101 | public static function geoDistance(float $lng1 = 0, float $lat1 = 0, float $lng2 = 0, float $lat2 = 0): float
102 | {
103 | $earthRadius = 6371000.0;
104 | $lat1 = ($lat1 * pi()) / 180;
105 | $lng1 = ($lng1 * pi()) / 180;
106 | $lat2 = ($lat2 * pi()) / 180;
107 | $lng2 = ($lng2 * pi()) / 180;
108 |
109 | $calcLongitude = $lng2 - $lng1;
110 | $calcLatitude = $lat2 - $lat1;
111 | $stepOne = pow(sin($calcLatitude / 2), 2) + cos($lat1) * cos($lat2) * pow(sin($calcLongitude / 2), 2);
112 | $stepTwo = 2 * asin(min(1, sqrt($stepOne)));
113 | return $earthRadius * $stepTwo;
114 | }
115 |
116 | /**
117 | * 数值格式化
118 | *
119 | * @param float|int $number 要格式化的数字
120 | * @param int $decimals 小数位数
121 | * @param string $decPoint 小数点
122 | * @param string $thousandssep 千分位符号
123 | *
124 | * @return string
125 | */
126 | public static function numberFormat($number, int $decimals = 2, string $decPoint = '.', string $thousandssep = ''): string
127 | {
128 | return number_format($number, $decimals, $decPoint, $thousandssep);
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/app/Controller/RpcController.php:
--------------------------------------------------------------------------------
1 | request->input('channel', 'default');
19 | $code = $this->request->input('code');
20 | $client = $this->container->get(AuthInterface::class);
21 | $value = $client->session($channel, $code);
22 | return $this->response->success($value);
23 | }
24 |
25 | /**
26 | * @return \Psr\Http\Message\ResponseInterface
27 | */
28 | public function phone()
29 | {
30 | $encryptedData = $this->request->input('encryptedData', '');
31 | $openid = $this->request->input('openid', '');
32 | $iv = $this->request->input('iv', '');
33 | $channel = $this->request->input('channel', 'default');
34 | $client = $this->container->get(AuthInterface::class);
35 | $sessionKey = SessionManager::get($channel, $openid);
36 | $value = $client->decryptData($channel, $sessionKey, $iv, $encryptedData);
37 | return $this->response->success($value);
38 | }
39 |
40 | public function getFewQrCode()
41 | {
42 | $channel = $this->request->input('channel', 'default');
43 | $path = $this->request->input('path', '/pages/codeBus/pages/order/index');
44 | $client = $this->container->get(QrCodeInterface::class);
45 | $value = $client->get($channel, $path);
46 | return $this->response->success($value);
47 | }
48 |
49 | public function getUnlimitQrCode()
50 | {
51 | $channel = $this->request->input('channel', 'default');
52 | $path = $this->request->input('path', '/pages/codeBus/pages/order/index');
53 | $client = $this->container->get(QrCodeInterface::class);
54 | $value = $client->getUnlimit($channel, $path);
55 | return $this->response->success($value);
56 | }
57 |
58 | public function getQrCode()
59 | {
60 | $channel = $this->request->input('channel', 'default');
61 | $path = $this->request->input('path', '/pages/codeBus/pages/order/index');
62 | $client = $this->container->get(QrCodeInterface::class);
63 | $value = $client->getQrCode($channel, $path);
64 | return $this->response->success($value);
65 | }
66 |
67 | public function pay()
68 | {
69 | $channel = $this->request->input('channel', 'default');
70 | $client = $this->container->get(OrderInterface::class);
71 | $value = $client->unify($channel, [
72 | 'body' => '测试测试',
73 | 'out_trade_no' => '2020235235235',
74 | 'total_fee' => 1,
75 | 'spbill_create_ip' => \getClientIp(),
76 | 'notify_url' => config('payment.payment.default.notify_url'),
77 | 'trade_type' => 'JSAPI',
78 | 'openid' => 'oLtqX5PkCnFBTw_C1WUSxDwFgB50',
79 | ]);
80 | return $this->response->success($value);
81 | }
82 |
83 | public function notify()
84 | {
85 | $channel = $this->request->input('channel', 'default');
86 | $client = $this->container->get(NotifyInterface::class);
87 | $value = $client->handlePaidNotify($channel,$this->request);
88 | return $this->response->toWechatXML($value->getContent());
89 | }
90 | }
91 |
92 |
93 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://secure.php.net/)
2 | [](https://github.com/swoole/swoole-src)
3 | [](https://github.com/hyperf/hyperf)
4 | ## 项目会及时更新了
5 |
6 | # 介绍
7 | 此框架是基于Swoole4.5+Hyperf2.0开发的Easywechat的一些案例,所有的服务都是基于jsonrpc来调度,jsonrpc服务注册进consul服务管理中心.可以支持多个小程序,目前暂时完成了微信小程序登录授权,集成了微信支付和获取二维码的操作。服务提供重试机制,链路追踪和服务监控。可以根据配置合理配置重试机制。
8 |
9 | ## TODO
10 | 集成easywechat的所有功能
11 |
12 | ## 疑问
13 | 可以联系我微信
14 | 
15 | ## 联系方式
16 | qq群658446650
17 |
18 | ## 启动
19 | ```bash
20 | composer dump-autoload -o
21 |
22 | php bin/hyperf start
23 | ```
24 |
25 | ## API访问
26 | ```
27 | http://127.0.0.1:9501/rpc/session?channel=default&code= 获取会话session
28 | http://127.0.0.1:9501/rpc/phone? 解密手机号
29 | http://127.0.0.1:9501/rpc/getFewQrCode? 获取少量二维码
30 | http://127.0.0.1:9501/rpc/getUnlimitQrCode? 获取多量二维码
31 | http://127.0.0.1:9501/rpc/getQrCode? 获取小程序二维码(自定义尺寸)
32 | http://127.0.0.1:9501/rpc/pay 发起微信支付请求
33 | ```
34 |
35 | ## 功能(所有的功能都是基于easywechat文档的API封装的)
36 | - 小程序登录
37 | - 小程序码
38 | - 微信支付
39 |
40 | ## 配置
41 | ```php
42 | [
43 | //是否支持多个小程序
44 | 'enable_all' => env('WECHAT_ENABLE_ALL', false),
45 | //多个小程序用参数字段接收需要获取对应小程序的配置字段
46 | 'key' => env('WECHAT_QUERY_KEY', 'channel'),
47 | //服务重试次数
48 | 'maxattempts' => 2,
49 | //存储二维码文件路径
50 | 'qrcode_path' => BASE_PATH . '/storage/',
51 | //重试间隔
52 | 'sleep' => 20,
53 | 'config' => [
54 | //小程序1的配置
55 | 'default' => [
56 | 'app_id' => env('WECHAT_MINI_PROGRAM_APPID', ''),
57 | 'secret' => env('WECHAT_MINI_PROGRAM_SECRET', ''),
58 | 'token' => env('WECHAT_MINI_PROGRAM_TOKEN', ''),
59 | 'aes_key' => env('WECHAT_MINI_PROGRAM_AES_KEY', '')
60 | ],
61 | //小程序2的配置
62 | 'default2' => [
63 | 'app_id' => env('WECHAT_MINI_PROGRAM_APPID_DEFAULT2', ''),
64 | 'secret' => env('WECHAT_MINI_PROGRAM_SECRET_DEFAULT2', ''),
65 | 'token' => env('WECHAT_MINI_PROGRAM_TOKEN_DEFAULT2', ''),
66 | 'aes_key' => env('WECHAT_MINI_PROGRAM_AES_KEY_DEFAULT2', '')
67 | ]
68 | ]
69 | ];
70 | ```
71 | ```php
72 |
73 | //支付配置
74 | return [
75 | 'payment' => [
76 | 'default' => [
77 | 'sandbox' => env('WECHAT_PAYMENT_SANDBOX', false),//沙箱测试
78 | 'app_id' => env('WECHAT_PAYMENT_APPID', ''),//APPID
79 | 'mch_id' => env('WECHAT_PAYMENT_MCH_ID', ''), //商户ID
80 | 'key' => env('WECHAT_PAYMENT_KEY', BASE_PATH . '/private/payment/default/apiclient_cert.pem'),
81 | 'cert_path' => env('WECHAT_PAYMENT_CERT_PATH', BASE_PATH . '/private/payment/default/apiclient_key.pem'),
82 | 'key_path' => env('WECHAT_PAYMENT_KEY_PATH', ''),
83 | 'notify_url' => env('WECHAT_PAYMENT_NOTIFY_URL', ''), //支付回调地址
84 | 'refund_notify_url' => env('WECHAT_REFUND_NOTIFY_URL', ''), //退款回调地址
85 | ],
86 | 'default1' => [
87 | 'sandbox' => env('WECHAT_PAYMENT_SANDBOX', false),
88 | 'app_id' => env('WECHAT_PAYMENT_APPID', ''),
89 | 'mch_id' => env('WECHAT_PAYMENT_MCH_ID', ''),
90 | 'key' => env('WECHAT_PAYMENT_KEY', BASE_PATH . '/private/payment/default1/apiclient_cert.pem'),
91 | 'cert_path' => env('WECHAT_PAYMENT_CERT_PATH', BASE_PATH . '/private/payment/default1/apiclient_key.pem'),
92 | 'key_path' => env('WECHAT_PAYMENT_KEY_PATH', ''),
93 | 'notify_url' => env('WECHAT_PAYMENT_NOTIFY_URL', ''),
94 | 'refund_notify_url' => env('WECHAT_REFUND_NOTIFY_URL', ''),
95 | ]
96 | ],
97 | //服务重试次数
98 | 'maxattempts' => 3,
99 | //重试休眠时间
100 | 'sleep' => 20
101 |
102 | ];
103 | ```
104 |
105 | ## 服务监控
106 | 
107 |
108 | ## 链路追踪
109 | 
110 |
111 |
--------------------------------------------------------------------------------
/app/Kernel/Rpc/MiniProgram/AuthService.php:
--------------------------------------------------------------------------------
1 | logger->debug(sprintf('>>>>>
32 | MiniProgram => Auth => Session
33 | Channel:小程序通道[%s] Code[%s]
34 | <<<<<',
35 | $channel, $code));
36 | $response = make(Response::class);
37 | try {
38 | $session = retry($this->maxAttempts, function () use ($channel, $code)
39 | {
40 | return $this->container->get(MiniProgramFactory::class)->get($channel)->auth->session($code);
41 | }, $this->sleep);
42 | if (!is_array($session) || !isset($session['openid'])) {
43 | throw new RuntimeException($session['errmsg'], $session['errcode']);
44 | }
45 | SessionManager::set($channel, $session['openid'], $session['session_key']);
46 | $response->setCode(Response::RPC_RETURN_SUCCESS_CODE);
47 | $response->setData($session);
48 | $response->setMsg(Response::RPC_RETURN_MESSAGE_OK);
49 | } catch (\Throwable $exception) {
50 | $this->logger->error(sprintf("
51 | >>>>>
52 | EasyWechat:小程序通道[%s] Code[%s]授权发生错误,
53 | 错误消息:{{%s}}
54 | 错误行号:{{%s}}
55 | 错误文件:{{%s}}
56 | <<<<<
57 | ", $channel, $code, $exception->getMessage(), $exception->getLine(), $exception->getFile()));
58 | $response->setCode(Response::RPC_RETURN_FAIL_CODE);
59 | $response->setData([]);
60 | $response->setMsg($exception->getMessage());
61 | }
62 | finally {
63 | return $this->send($response);
64 | }
65 | }
66 |
67 | /**
68 | * #解密用户手机号
69 | *
70 | * @param string $channel
71 | * @param string $sessionKey
72 | * @param string $iv
73 | * @param string $encrypted
74 | *
75 | * @return Response
76 | */
77 | public function decryptData(string $channel, string $sessionKey, string $iv, string $encrypted) : Response
78 | {
79 | $this->logger->debug(sprintf('>>>>>
80 | MiniProgram => Auth => decryptData
81 | Channel:小程序通道[%s] SessionKey[%s] Iv[%s] Encrypted[%s]
82 | <<<<<',
83 | $channel, $sessionKey, $iv, $encrypted));
84 | $response = make(Response::class);
85 | try {
86 | $decryptData = retry($this->maxAttempts, function () use ($channel, $sessionKey, $iv, $encrypted)
87 | {
88 | return $this->container->get(MiniProgramFactory::class)->get($channel)->encryptor->decryptData($sessionKey, $iv, $encrypted);
89 | }, $this->sleep);
90 | $response->setCode(Response::RPC_RETURN_SUCCESS_CODE);
91 | $response->setData($decryptData);
92 | $response->setMsg(Response::RPC_RETURN_MESSAGE_OK);
93 | } catch (\Throwable $throwable) {
94 | $this->logger->error(sprintf("
95 | >>>>>
96 | EasyWechat:小程序通道[%s] {sessionkey}[%s] {iv}[%s] {encrypted}[%s]获取手机号发生错误,
97 | 错误消息:{{%s}}
98 | 错误行号:{{%s}}
99 | 错误文件:{{%s}}
100 | <<<<<
101 | ", $channel, $sessionKey, $iv, $encrypted, $throwable->getMessage(), $throwable->getLine(), $throwable->getFile()));
102 | $response->setCode(Response::RPC_RETURN_FAIL_CODE);
103 | $response->setData([]);
104 | $response->setMsg($throwable->getMessage());
105 | }
106 | finally {
107 | return $this->send($response);
108 | }
109 | }
110 | }
111 |
112 |
113 |
--------------------------------------------------------------------------------
/app/Kernel/Lock/RedisLock.php:
--------------------------------------------------------------------------------
1 | redis = $redis;
40 | }
41 |
42 | /**
43 | *
44 | */
45 | private function __clone()
46 | {
47 | }
48 |
49 | /**
50 | * 上锁
51 | *
52 | * @param string $name 锁名字
53 | * @param int $expire 锁有效期
54 | * @param int $retryTimes 重试次数
55 | * @param float|int $sleep 重试休息微秒
56 | *
57 | * @return mixed
58 | */
59 | public function lock(string $name, int $expire = 5, int $retryTimes = 10, float $sleep = 10000)
60 | {
61 | $lock = false;
62 | $retryTimes = max($retryTimes, 1);
63 | $key = self::REDIS_LOCK_KEY_PREFIX . $name;
64 | while ($retryTimes-- > 0) {
65 | $kVal = microtime(true) + $expire;
66 | $lock = $this->getLock($key, $expire, $kVal); //上锁
67 | if ($lock) {
68 | $this->lockedNames[$key] = $kVal;
69 | break;
70 | }
71 | if (\Hyperf\Utils\Coroutine::inCoroutine()) {
72 | Coroutine::sleep((float)$sleep / 1000);
73 | } else {
74 | usleep($sleep);
75 | }
76 | }
77 | return $lock;
78 | }
79 |
80 | /**
81 | * 获取锁
82 | *
83 | * @param $key
84 | * @param $expire
85 | * @param $value
86 | *
87 | * @return mixed
88 | */
89 | private function getLock($key, $expire, $value)
90 | {
91 | $script = <<execLuaScript($script, [$key, $value, $expire]);
105 | }
106 |
107 | /**
108 | * 解锁
109 | *
110 | * @param string $name
111 | *
112 | * @return mixed
113 | */
114 | public function unlock(string $name)
115 | {
116 | $script = <<lockedNames[$key])) {
129 | $val = $this->lockedNames[$key];
130 | return $this->execLuaScript($script, [$key, $val]);
131 | }
132 | return false;
133 | }
134 |
135 | /**
136 | * 执行lua脚本
137 | *
138 | * @param string $script
139 | * @param array $params
140 | * @param int $keyNum
141 | *
142 | * @return mixed
143 | */
144 | private function execLuaScript($script, array $params, $keyNum = 1)
145 | {
146 | $hash = $this->redis->script('load', $script);
147 | return $this->redis->evalSha($hash, $params, $keyNum);
148 | }
149 |
150 | /**
151 | * 获取锁并执行
152 | *
153 | * @param callable $func
154 | * @param string $name
155 | * @param int $expire
156 | * @param int $retryTimes
157 | * @param int $sleep
158 | *
159 | * @return bool
160 | * @throws \Exception
161 | */
162 | public function run(callable $func, string $name, int $expire = 5, int $retryTimes = 10, $sleep = 10000)
163 | {
164 | if ($this->lock($name, $expire, $retryTimes, $sleep)) {
165 | try {
166 | call_user_func($func);
167 | } catch (\Exception $e) {
168 | throw $e;
169 | } finally {
170 | $this->unlock($name);
171 | }
172 | return true;
173 | } else {
174 | return false;
175 | }
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/app/Kernel/Rpc/Payment/OrderService.php:
--------------------------------------------------------------------------------
1 | logger->debug(sprintf('>>>>>
25 | Payment => Order => unify
26 | 统一下单:
27 | Channel:微信商户通道[%s] Params[%s]
28 | <<<<<',
29 | $channel, Json::encode($params)));
30 | $return = $order = NULL;
31 | if (empty($params['out_trade_no']) || empty($params['total_fee'] || empty($params['openid']))) {
32 | throw new PaymentException('参数不正确!');
33 | }
34 | $key = 'order_pay_' . $channel . '_' . $params['out_trade_no'];
35 | $redis = $this->container->get(RedisFactory::class)->get('default');
36 | $redisLock = make(RedisLock::class, [
37 | $redis
38 | ]);
39 | if ($redisLock->lock($key) === 0) {
40 | throw new PaymentException('请勿重复发起支付!');
41 | }
42 | try {
43 | //todo 可以自己在这里检测该订单是否被支付
44 | $order = retry($this->maxAttempts, function () use ($channel, $params, $isContract)
45 | {
46 | return $this->container->get(PaymentFactory::class)->get($channel)->order->unify($params, $isContract);
47 | }, $this->sleep);
48 | $redisLock->unlock($key);
49 | if ($order['return_code'] === 'SUCCESS' && $order['return_msg'] === 'OK') {
50 | // 二次验签
51 | $return = [
52 | 'appId' => config("payment.payment.{$channel}.app_id"),
53 | 'timeStamp' => time(),
54 | 'nonceStr' => $order['nonce_str'],
55 | 'package' => 'prepay_id=' . $order['prepay_id'],
56 | 'signType' => 'MD5',
57 | ];
58 | $return['paySign'] = generate_sign($return, config("payment.payment.{$channel}.key"));
59 | }
60 | } catch (\Throwable $exception) {
61 | $redisLock->unlock($key);
62 | $this->logger->error(sprintf("
63 | >>>>>
64 | Payment:微信商户通道[%s] 统一下单[%s]发生错误,
65 | 错误消息:{{%s}}
66 | 错误行号:{{%s}}
67 | 错误文件:{{%s}}
68 | <<<<<
69 | ", $channel, Json::encode($params), $exception->getMessage(), $exception->getLine(), $exception->getFile()));
70 | }
71 | finally {
72 | return $this->send($return);
73 | }
74 | }
75 |
76 | /**
77 | * @param string $channel
78 | * @param string $number
79 | *
80 | * @return mixed
81 | */
82 | public function queryByOutTradeNumber(string $channel, string $number)
83 | {
84 | $this->logger->debug(sprintf('>>>>>
85 | Payment => Order => queryByOutTradeNumber
86 | 统一下单:
87 | Channel:微信商户通道[%s] OrderNo[%s]
88 | <<<<<',
89 | $channel, $number));
90 | $order = NULL;
91 | try {
92 | //todo 用户可以在这里查询自己的订单库
93 | $order = retry($this->maxAttempts, function () use ($channel, $number)
94 | {
95 | return $this->container->get(PaymentFactory::class)->get($channel)->order->queryByOutTradeNumber($number);
96 | }, $this->sleep);
97 | } catch (\Throwable $exception) {
98 | $this->logger->error(sprintf("
99 | >>>>>
100 | Payment:微信商户通道[%s] 查询订单[%s]发生错误,
101 | 错误消息:{{%s}}
102 | 错误行号:{{%s}}
103 | 错误文件:{{%s}}
104 | <<<<<
105 | ", $channel, $number, $exception->getMessage(), $exception->getLine(), $exception->getFile()));
106 | }
107 | finally {
108 | return $this->send($order);
109 | }
110 | }
111 |
112 | /**
113 | * @param string $channel
114 | * @param string $transactionId
115 | *
116 | * @return mixed
117 | */
118 | public function queryByTransactionId(string $channel, string $transactionId)
119 | {
120 | $this->logger->debug(sprintf('>>>>>
121 | Payment => Order => queryByTransactionId
122 | 统一下单:
123 | Channel:微信商户通道[%s] WechatOrderNo[%s]
124 | <<<<<',
125 | $channel, $transactionId));
126 | $order = NULL;
127 | try {
128 | //todo 用户可以在这里查询自己的订单库
129 | $order = retry($this->maxAttempts, function () use ($channel, $transactionId)
130 | {
131 | return $this->container->get(PaymentFactory::class)->get($channel)->order->queryByTransactionId($transactionId);
132 | }, $this->sleep);
133 | } catch (\Throwable $exception) {
134 | $this->logger->error(sprintf("
135 | >>>>>
136 | Payment:微信商户通道[%s] 查询订单[%s]发生错误,
137 | 错误消息:{{%s}}
138 | 错误行号:{{%s}}
139 | 错误文件:{{%s}}
140 | <<<<<
141 | ", $channel, $transactionId, $exception->getMessage(), $exception->getLine(), $exception->getFile()));
142 | }
143 | finally {
144 | return $this->send($order);
145 | }
146 | }
147 |
148 | public function close(string $channel, string $tradeNo)
149 | {
150 |
151 | }
152 |
153 |
154 | }
155 |
156 |
157 |
--------------------------------------------------------------------------------
/app/Kernel/Rpc/Payment/NotifyService.php:
--------------------------------------------------------------------------------
1 | buildSymfonyRequest($request);
36 | $this->container->get(PaymentFactory::class)->get($channel)['request'] = $symfonyRequest;
37 | $response = $this->container->get(PaymentFactory::class)->get($channel)->handlePaidNotify(function ($message, $fail) use ($channel)
38 | {
39 | $this->logger->debug(sprintf('
40 | >>>>>
41 | Payment => Notify => HandlePaidNotify
42 | 支付回调:
43 | Channel:微信商户通道[%s] Message:[%s]
44 | <<<<<
45 | ', $channel, Json::encode($message)));
46 |
47 | $client = $this->container->get(OrderInterface::class);
48 | $query = $client->queryByOutTradeNumber($channel, $message['out_trade_no']);
49 | if ($query['return_code'] === Payment::FAIL_TEXT) {
50 | $this->logger->error(sprintf('
51 | >>>>>
52 | Channel:[%s] 查询微信订单API错误
53 | 订单号:[%s]
54 | 错误原因:[%s]
55 | <<<<<
56 | ', $channel, $message['out_trade_no'], $query['return_msg']));
57 | return false;
58 | }
59 | $this->logger->info(sprintf('
60 | >>>>>
61 | Channel:[%s]
62 | 订单号:[%s]
63 | OPENID:[%s]
64 | 订单金额:[%s]
65 | 支付结果:[%s]
66 | 错误代码:[%s]
67 | 错误代码描述:[%s]
68 | <<<<<
69 | ', $message['out_trade_no'],
70 | $message['openid'],
71 | $message['total_fee'],
72 | $message['result_code'],
73 | $message['err_code'] ?? '',
74 | $message['err_code_des'] ?? ''));
75 | if ($message['result_code'] === Payment::SUCCESS_TEXT) {
76 | if (Arr::get($message, 'result_code') === Payment::SUCCESS_TEXT) {
77 | return true;
78 | } else {
79 | if (Arr::get($message, 'result_code') === Payment::FAIL_TEXT) {
80 | return false;
81 | }
82 | }
83 | } else {
84 | return $fail('通信失败,请稍后再通知!');
85 | }
86 | });
87 | } catch (\Throwable $exception) {
88 | $this->logger->error(sprintf('>>>>>
89 | 微信支付回调错误:
90 | Channel:[%s]
91 | 错误消息:{{%s}}
92 | 错误行号:{{%s}}
93 | 错误文件:{{%s}}
94 | <<<<<', $channel, $exception->getMessage(), $exception->getLine(), $exception->getFile()));
95 | }
96 | finally {
97 | return $this->send($response instanceof Response ? $response : NULL);
98 | }
99 | }
100 |
101 | public function handleRefundedNotify(string $channel, RequestInterface $request)
102 | {
103 | $response = NULL;
104 | try {
105 | $symfonyRequest = $this->buildSymfonyRequest($request);
106 | $this->container->get(PaymentFactory::class)->get($channel)['request'] = $symfonyRequest;
107 | $response = $this->container->get(PaymentFactory::class)->get($channel)->handleRefundedNotify(function ($message, $reqInfo, $fail) use ($channel)
108 | {
109 | $this->logger->debug(sprintf('
110 | >>>>>
111 | Payment => Notify => HandlePaidNotify
112 | 退款回调:
113 | Channel:微信商户通道[%s] Message:[%s]
114 | <<<<<
115 | ', $channel, Json::encode($message)));
116 | if(isset($message['return_code'])&&$message['return_code']==='SUCCESS'){
117 | if (!is_array($reqInfo) && !isset($reqInfo['out_refund_no'])) {
118 | return $fail('参数格式校验错误');
119 | }
120 | //TODO 处理回调逻辑
121 | $this->logger->info(sprintf(''));
122 | }
123 |
124 |
125 | });
126 | } catch (\Throwable $exception) {
127 | $this->logger->error(sprintf('>>>>>
128 | 微信退款回调错误:
129 | Channel:[%s]
130 | 错误消息:{{%s}}
131 | 错误行号:{{%s}}
132 | 错误文件:{{%s}}
133 | <<<<<', $channel, $exception->getMessage(), $exception->getLine(), $exception->getFile()));
134 | }
135 | finally {
136 | return $this->send($response instanceof Response ? $response : NULL);
137 | }
138 | }
139 |
140 | public function handleScannedNotify(string $channel, RequestInterface $request)
141 | {
142 | }
143 |
144 | }
145 |
146 |
147 |
--------------------------------------------------------------------------------
/app/Kernel/Rpc/MiniProgram/QrCodeService.php:
--------------------------------------------------------------------------------
1 | logger->debug(sprintf('>>>>>
28 | MiniProgram => QrCode => decryptData
29 | Channel:小程序通道[%s] Path[%s] Optional[%s] FileName[%s]
30 | <<<<<',
31 | $channel, $path, Json::encode($optional), $fileName));
32 | $rpcResponse = make(Response::class);
33 | try {
34 | $response = retry($this->maxAttempts, function () use ($channel, $path, $optional)
35 | {
36 | return $this->container->get(MiniProgramFactory::class)->get($channel)->app_code->get($path, $optional);
37 | }, $this->sleep);
38 | if ($response instanceof StreamResponse) {
39 | if ($fileName !== '' && $fileName !== NULL) {
40 | $fileName = $response->save($this->qrCodePath, (string)$fileName);
41 | } else {
42 | $fileName = $response->save($this->qrCodePath, StringHelper::randString(10, 0));
43 | }
44 | }
45 | $rpcResponse->setCode(Response::RPC_RETURN_SUCCESS_CODE);
46 | $rpcResponse->setData(['file_name' => $fileName]);
47 | $rpcResponse->setMsg(Response::RPC_RETURN_MESSAGE_OK);
48 | } catch (Throwable $throwable) {
49 | $this->logger->error(sprintf("
50 | >>>>>
51 | EasyWechat:小程序通道[%s] {path}[%s] {optional}[%s] 获取小程序码(数量较少)发生错误,
52 | 错误消息:{{%s}}
53 | 错误行号:{{%s}}
54 | 错误文件:{{%s}}
55 | <<<<<
56 | ", $channel, $path, Json::encode($optional), $throwable->getMessage(), $throwable->getLine(), $throwable->getFile()));
57 | $rpcResponse->setCode(Response::RPC_RETURN_FAIL_CODE);
58 | $rpcResponse->setData(['file_name' => '']);
59 | $rpcResponse->setMsg($throwable->getMessage());
60 | }
61 | finally {
62 | return $this->send($rpcResponse);
63 | }
64 | }
65 |
66 | /**
67 | * @inheritDoc
68 | */
69 | public function getUnlimit(string $channel, string $scene, array $optional = [], string $fileName = '') : Response
70 | {
71 | $this->logger->debug(sprintf('>>>>>
72 | MiniProgram => QrCode => getUnlimit
73 | Channel:小程序通道[%s] Scene[%s] Optional[%s] FileName[%s]
74 | <<<<<',
75 | $channel, $scene, Json::encode($optional), $fileName));
76 | $rpcResponse = make(Response::class);
77 | try {
78 | $response = retry($this->maxAttempts, function () use ($channel, $scene, $optional)
79 | {
80 | return $this->container->get(MiniProgramFactory::class)->get($channel)->app_code->getUnlimit($scene, $optional);
81 | }, $this->sleep);
82 | if ($response instanceof StreamResponse) {
83 | if ($fileName !== '' && $fileName !== NULL) {
84 | $fileName = $response->save($this->qrCodePath, (string)$fileName);
85 | } else {
86 | $fileName = $response->save($this->qrCodePath, StringHelper::randString(10, 0));
87 | }
88 | }
89 | $rpcResponse->setCode(Response::RPC_RETURN_SUCCESS_CODE);
90 | $rpcResponse->setData(['file_name' => $fileName]);
91 | $rpcResponse->setMsg(Response::RPC_RETURN_MESSAGE_OK);
92 | } catch (Throwable $throwable) {
93 | $this->logger->error(sprintf("
94 | >>>>>
95 | EasyWechat:小程序通道[%s] {scene}[%s] {optional}[%s] 获取小程序码(数量较多)发生错误,
96 | 错误消息:{{%s}}
97 | 错误行号:{{%s}}
98 | 错误文件:{{%s}}
99 | <<<<<
100 | ", $channel, $scene, Json::encode($optional), $throwable->getMessage(), $throwable->getLine(), $throwable->getFile()));
101 | $rpcResponse->setCode(Response::RPC_RETURN_FAIL_CODE);
102 | $rpcResponse->setData(['file_name' => '']);
103 | $rpcResponse->setMsg($throwable->getMessage());
104 | }
105 | finally {
106 | return $this->send($rpcResponse);
107 | }
108 | }
109 |
110 | /**
111 | * @inheritDoc
112 | */
113 | public function getQrCode(string $channel, string $path, int $width = NULL, string $fileName = '') : Response
114 | {
115 | $this->logger->debug(sprintf('>>>>>
116 | MiniProgram => QrCode => getQrCode
117 | Channel:小程序通道[%s] Path[%s] Width[%s] FileName[%s]
118 | <<<<<',
119 | $channel, $path, $width, $fileName));
120 | $rpcResponse = make(Response::class);
121 | try {
122 | $response = retry($this->maxAttempts, function () use ($channel, $path, $width)
123 | {
124 | return $this->container->get(MiniProgramFactory::class)->get($channel)->app_code->getQrCode($path, $width);
125 | }, $this->sleep);
126 | if ($response instanceof StreamResponse) {
127 | if ($fileName !== '' && $fileName !== NULL) {
128 | $fileName = $response->save($this->qrCodePath, (string)$fileName);
129 | } else {
130 | $fileName = $response->save($this->qrCodePath, StringHelper::randString(10, 0));
131 | }
132 | }
133 | $rpcResponse->setCode(Response::RPC_RETURN_SUCCESS_CODE);
134 | $rpcResponse->setData(['file_name' => $fileName]);
135 | $rpcResponse->setMsg(Response::RPC_RETURN_MESSAGE_OK);
136 | } catch (Throwable $throwable) {
137 | $this->logger->error(sprintf("
138 | >>>>>
139 | EasyWechat:小程序通道[%s] {path}[%s] {width}[%s] 获取小程序码发生错误,
140 | 错误消息:{{%s}}
141 | 错误行号:{{%s}}
142 | 错误文件:{{%s}}
143 | <<<<<
144 | ", $channel, $path, $width, $throwable->getMessage(), $throwable->getLine(), $throwable->getFile()));
145 | $rpcResponse->setCode(Response::RPC_RETURN_FAIL_CODE);
146 | $rpcResponse->setData(['file_name' => '']);
147 | $rpcResponse->setMsg($throwable->getMessage());
148 | }
149 | finally {
150 | return $this->send($rpcResponse);
151 | }
152 | }
153 | }
154 |
155 |
156 |
--------------------------------------------------------------------------------
/app/Helper/DirectoryHelper.php:
--------------------------------------------------------------------------------
1 | $file) {
204 | $fpath = $file->getRealPath();
205 | if ($file->isDir()) {
206 | array_push($dirs, $fpath);
207 | } else {
208 | //先删除文件
209 | @unlink($fpath);
210 | }
211 | }
212 |
213 | //再删除目录
214 | rsort($dirs);
215 | foreach ($dirs as $dir) {
216 | @rmdir($dir);
217 | }
218 |
219 | unset($objects, $object, $dirs);
220 | return true;
221 | }
222 |
223 | /**
224 | * 格式化路径字符串(路径后面加/)
225 | *
226 | * @param string $dir
227 | *
228 | * @return string
229 | */
230 | public static function formatDir(string $dir): string
231 | {
232 | if ($dir == '') {
233 | return '';
234 | }
235 |
236 | $order = [
237 | '\\',
238 | "'",
239 | '#',
240 | '=',
241 | '`',
242 | '$',
243 | '%',
244 | '&',
245 | ';',
246 | '|'
247 | ];
248 | $replace = [
249 | '/',
250 | '',
251 | '',
252 | '',
253 | '',
254 | '',
255 | '',
256 | '',
257 | '',
258 | ];
259 |
260 | $dir = str_replace($order, $replace, $dir);
261 | return rtrim(preg_replace(RegularHelper::$patternDoubleSlash, '/', $dir), ' / ') . '/';
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/app/Helper/EncryptHelper.php:
--------------------------------------------------------------------------------
1 | 26) {
104 | $expTime = intval(substr($res, 0, 10));
105 | if (($expTime == 0 || $expTime - $now > 0) && substr($res, 10, 16) == substr(md5(substr($res, 26) . $keyb), 0, 16)) {
106 | return [substr($res, 26), $expTime];
107 | }
108 | }
109 | return ['', 0];
110 | }
111 | }
112 |
113 | /**
114 | * 简单加密
115 | *
116 | * @param string $data 数据
117 | * @param string $key 密钥
118 | *
119 | * @return string
120 | */
121 | public static function easyEncrypt(string $data, string $key) : string
122 | {
123 | if ($data == '') {
124 | return '';
125 | }
126 |
127 | $key = md5($key);
128 | $dataLen = strlen($data);
129 | $keyLen = strlen($key);
130 | $x = 0;
131 | $str = $char = '';
132 | for ($i = 0; $i < $dataLen; $i++) {
133 | if ($x == $keyLen) {
134 | $x = 0;
135 | }
136 |
137 | $str .= chr(ord($data[$i]) + (ord($key[$x])) % 256);
138 | $x++;
139 | }
140 |
141 | return substr($key, 0, Consts::DYNAMIC_KEY_LEN) . self::base64UrlEncode($str);
142 | }
143 |
144 | /**
145 | * 简单解密
146 | *
147 | * @param string $data 数据
148 | * @param string $key 密钥
149 | *
150 | * @return string
151 | */
152 | public static function easyDecrypt(string $data, string $key) : string
153 | {
154 | if (strlen($data) < Consts::DYNAMIC_KEY_LEN) {
155 | return '';
156 | }
157 |
158 | $key = md5($key);
159 | if (substr($key, 0, Consts::DYNAMIC_KEY_LEN) != substr($data, 0, Consts::DYNAMIC_KEY_LEN)) {
160 | return '';
161 | }
162 |
163 | $data = self::base64UrlDecode(substr($data, Consts::DYNAMIC_KEY_LEN));
164 | if (empty($data)) {
165 | return '';
166 | }
167 |
168 | $dataLen = strlen($data);
169 | $keyLen = strlen($key);
170 | $x = 0;
171 | $str = $char = '';
172 | for ($i = 0; $i < $dataLen; $i++) {
173 | if ($x == $keyLen) {
174 | $x = 0;
175 | }
176 |
177 | $c = ord($data[$i]);
178 | $k = ord($key[$x]);
179 | if ($c < $k) {
180 | $str .= chr(($c + 256) - $k);
181 | } else {
182 | $str .= chr($c - $k);
183 | }
184 |
185 | $x++;
186 | }
187 |
188 | return $str;
189 | }
190 |
191 | /**
192 | * MurmurHash3算法函数
193 | *
194 | * @param string $data 要哈希的数据
195 | * @param int $seed 随机种子(仅素数)
196 | * @param bool $unsign 是否返回无符号值;为true时返回11位无符号整数,为false时返回10位有符号整数
197 | *
198 | * @return float|int
199 | */
200 | public static function murmurhash3Int(string $data, int $seed = 3, bool $unsign = true)
201 | {
202 | $key = array_values(unpack('C*', $data));
203 | $klen = count($key);
204 | $h1 = abs($seed);
205 | for ($i = 0, $bytes = $klen - ($remainder = $klen & 3); $i < $bytes;) {
206 | $k1 = $key[$i] | ($key[++$i] << 8) | ($key[++$i] << 16) | ($key[++$i] << 24);
207 | ++$i;
208 | $k1 = (((($k1 & 0xffff) * 0xcc9e2d51) + ((((($k1 >= 0 ? $k1 >> 16 : (($k1 & 0x7fffffff) >> 16) | 0x8000)) * 0xcc9e2d51) & 0xffff) << 16))) & 0xffffffff;
209 | $k1 = $k1 << 15 | ($k1 >= 0 ? $k1 >> 17 : (($k1 & 0x7fffffff) >> 17) | 0x4000);
210 | $k1 = (((($k1 & 0xffff) * 0x1b873593) + ((((($k1 >= 0 ? $k1 >> 16 : (($k1 & 0x7fffffff) >> 16) | 0x8000)) * 0x1b873593) & 0xffff) << 16))) & 0xffffffff;
211 | $h1 ^= $k1;
212 | $h1 = $h1 << 13 | ($h1 >= 0 ? $h1 >> 19 : (($h1 & 0x7fffffff) >> 19) | 0x1000);
213 | $h1b = (((($h1 & 0xffff) * 5) + ((((($h1 >= 0 ? $h1 >> 16 : (($h1 & 0x7fffffff) >> 16) | 0x8000)) * 5) & 0xffff) << 16))) & 0xffffffff;
214 | $h1 = ((($h1b & 0xffff) + 0x6b64) + ((((($h1b >= 0 ? $h1b >> 16 : (($h1b & 0x7fffffff) >> 16) | 0x8000)) + 0xe654) & 0xffff) << 16));
215 | }
216 | $k1 = 0;
217 | switch ($remainder) {
218 | case 3:
219 | $k1 ^= $key[$i + 2] << 16;
220 | break;
221 | case 2:
222 | $k1 ^= $key[$i + 1] << 8;
223 | break;
224 | case 1:
225 | $k1 ^= $key[$i];
226 | $k1 = ((($k1 & 0xffff) * 0xcc9e2d51) + ((((($k1 >= 0 ? $k1 >> 16 : (($k1 & 0x7fffffff) >> 16) | 0x8000)) * 0xcc9e2d51) & 0xffff) << 16)) & 0xffffffff;
227 | $k1 = $k1 << 15 | ($k1 >= 0 ? $k1 >> 17 : (($k1 & 0x7fffffff) >> 17) | 0x4000);
228 | $k1 = ((($k1 & 0xffff) * 0x1b873593) + ((((($k1 >= 0 ? $k1 >> 16 : (($k1 & 0x7fffffff) >> 16) | 0x8000)) * 0x1b873593) & 0xffff) << 16)) & 0xffffffff;
229 | $h1 ^= $k1;
230 | break;
231 | }
232 | $h1 ^= $klen;
233 | $h1 ^= ($h1 >= 0 ? $h1 >> 16 : (($h1 & 0x7fffffff) >> 16) | 0x8000);
234 | $h1 = ((($h1 & 0xffff) * 0x85ebca6b) + ((((($h1 >= 0 ? $h1 >> 16 : (($h1 & 0x7fffffff) >> 16) | 0x8000)) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff;
235 | $h1 ^= ($h1 >= 0 ? $h1 >> 13 : (($h1 & 0x7fffffff) >> 13) | 0x40000);
236 | $h1 = (((($h1 & 0xffff) * 0xc2b2ae35) + ((((($h1 >= 0 ? $h1 >> 16 : (($h1 & 0x7fffffff) >> 16) | 0x8000)) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff;
237 | $h1 ^= ($h1 >= 0 ? $h1 >> 16 : (($h1 & 0x7fffffff) >> 16) | 0x8000);
238 |
239 | if ($unsign) {
240 | $h1 = ($h1 >= 0) ? bcadd('1' . str_repeat('0', 10), $h1) : abs($h1);
241 | }
242 |
243 | return $h1;
244 | }
245 |
246 | }
247 |
--------------------------------------------------------------------------------
/app/Helper/DateHelper.php:
--------------------------------------------------------------------------------
1 | 31, 3 => 31, 4 => 30, 5 => 31, 6 => 30, 7 => 31, 8 => 31, 9 => 30, 10 => 31, 11 => 30, 12 => 31];
54 |
55 | if ($month <= 0) {
56 | $month = date('n');
57 | }
58 |
59 | if ($year <= 0) {
60 | $year = date('Y');
61 | }
62 |
63 | if (array_key_exists($month, $monthsMap)) {
64 | return $monthsMap[$month];
65 | } elseif ($month > 12) {
66 | return 0;
67 | } else {
68 | if ($year % 100 === 0) {
69 | if ($year % 400 === 0) {
70 | return 29;
71 | } else {
72 | return 28;
73 | }
74 | } else {
75 | if ($year % 4 === 0) {
76 | return 29;
77 | } else {
78 | return 28;
79 | }
80 | }
81 | }
82 | }
83 |
84 | /**
85 | * 将秒数转换为时间字符串
86 | * 如:
87 | * 10 将转换为 00:10,
88 | * 120 将转换为 02:00,
89 | * 3601 将转换为 01:00:01
90 | *
91 | * @param int $second
92 | *
93 | * @return string
94 | */
95 | public static function second2time(int $second = 0): string
96 | {
97 | if ($second <= 0) {
98 | return '';
99 | }
100 |
101 | $hours = floor($second / 3600);
102 | $hours = $hours ? str_pad($hours, 2, '0', STR_PAD_LEFT) : 0;
103 | $second = $second % 3600;
104 | $minutes = floor($second / 60);
105 | $minutes = str_pad($minutes, 2, '0', STR_PAD_LEFT);
106 | $seconds = $second % 60;
107 | $seconds = str_pad($seconds, 2, '0', STR_PAD_LEFT);
108 |
109 | return implode(':', $hours ? compact('hours', 'minutes', 'seconds') : compact('minutes', 'seconds'));
110 | }
111 |
112 | /**
113 | * 获取时间戳的微秒部分,单位/微秒.
114 | * @return float
115 | */
116 | public static function getMicrosecond(): float
117 | {
118 | [$usec,] = explode(" ", microtime());
119 | return (float)$usec * pow(10, 6);
120 | }
121 |
122 | /**
123 | * 获取时间戳,单位/毫秒.
124 | * @return float
125 | */
126 | public static function getMillitime(): float
127 | {
128 | [$t1, $t2] = explode(' ', microtime());
129 | return (float)sprintf('%.0f', (floatval($t1) + floatval($t2)) * 1000);
130 | }
131 |
132 | /**
133 | * 根据时间获取星座
134 | *
135 | * @param int|string $datetime 时间戳或Y-m-d格式日期
136 | *
137 | * @return string
138 | */
139 | public static function getXingZuo($datetime): string
140 | {
141 | $res = '';
142 | if (is_numeric($datetime) && strlen($datetime) == 10) {
143 | $datetime = date('Y-m-d H:i:s', $datetime);
144 | } else {
145 | $datetime = strval($datetime);
146 | }
147 |
148 | if (!ValidateHelper::isDate2time($datetime)) {
149 | return $res;
150 | }
151 |
152 | $month = substr($datetime, 5, 2); //取出月份
153 | $day = intval(substr($datetime, 8, 2)); //取出日期
154 | switch ($month) {
155 | case "01":
156 | if ($day < 21) {
157 | $res = '摩羯';
158 | } else {
159 | $res = '水瓶';
160 | }
161 | break;
162 | case "02":
163 | if ($day < 20) {
164 | $res = '水瓶';
165 | } else {
166 | $res = '双鱼';
167 | }
168 | break;
169 | case "03":
170 | if ($day < 21) {
171 | $res = '双鱼';
172 | } else {
173 | $res = '白羊';
174 | }
175 | break;
176 | case "04":
177 | if ($day < 20) {
178 | $res = '白羊';
179 | } else {
180 | $res = '金牛';
181 | }
182 | break;
183 | case "05":
184 | if ($day < 21) {
185 | $res = '金牛';
186 | } else {
187 | $res = '双子';
188 | }
189 | break;
190 | case "06":
191 | if ($day < 22) {
192 | $res = '双子';
193 | } else {
194 | $res = '巨蟹';
195 | }
196 | break;
197 | case "07":
198 | if ($day < 23) {
199 | $res = '巨蟹';
200 | } else {
201 | $res = '狮子';
202 | }
203 | break;
204 | case "08":
205 | if ($day < 23) {
206 | $res = '狮子';
207 | } else {
208 | $res = '处女';
209 | }
210 | break;
211 | case "09":
212 | if ($day < 23) {
213 | $res = '处女';
214 | } else {
215 | $res = '天秤';
216 | }
217 | break;
218 | case "10":
219 | if ($day < 24) {
220 | $res = '天秤';
221 | } else {
222 | $res = '天蝎';
223 | }
224 | break;
225 | case "11":
226 | if ($day < 22) {
227 | $res = '天蝎';
228 | } else {
229 | $res = '射手';
230 | }
231 | break;
232 | case "12":
233 | if ($day < 22) {
234 | $res = '射手';
235 | } else {
236 | $res = '摩羯';
237 | }
238 | break;
239 | }
240 |
241 | return $res;
242 | }
243 |
244 | /**
245 | * 根据时间获取生肖
246 | *
247 | * @param int|string $datetime 时间戳或Y-m-d格式日期
248 | *
249 | * @return string
250 | */
251 | public static function getShengXiao($datetime): string
252 | {
253 | $res = '';
254 | if (is_numeric($datetime) && strlen($datetime) == 10) {
255 | $datetime = date('Y-m-d H:i:s', $datetime);
256 | } else {
257 | $datetime = strval($datetime);
258 | }
259 |
260 | if (!ValidateHelper::isDate2time($datetime)) {
261 | return $res;
262 | }
263 |
264 | $startYear = 1901;
265 | $endYear = intval(substr($datetime, 0, 4));
266 | $x = ($startYear - $endYear) % 12;
267 |
268 | switch ($x) {
269 | case 1:
270 | case -11:
271 | $res = "鼠";
272 | break;
273 | case 0:
274 | $res = "牛";
275 | break;
276 | case 11:
277 | case -1:
278 | $res = "虎";
279 | break;
280 | case 10:
281 | case -2:
282 | $res = "兔";
283 | break;
284 | case 9:
285 | case -3:
286 | $res = "龙";
287 | break;
288 | case 8:
289 | case -4:
290 | $res = "蛇";
291 | break;
292 | case 7:
293 | case -5:
294 | $res = "马";
295 | break;
296 | case 6:
297 | case -6:
298 | $res = "羊";
299 | break;
300 | case 5:
301 | case -7:
302 | $res = "猴";
303 | break;
304 | case 4:
305 | case -8:
306 | $res = "鸡";
307 | break;
308 | case 3:
309 | case -9:
310 | $res = "狗";
311 | break;
312 | case 2:
313 | case -10:
314 | $res = "猪";
315 | break;
316 | }
317 |
318 | return $res;
319 | }
320 |
321 | /**
322 | * 根据时间获取农历年份(天干地支)
323 | *
324 | * @param int|string $datetime 时间戳或Y-m-d格式日期
325 | *
326 | * @return string
327 | */
328 | public static function getLunarYear($datetime): string
329 | {
330 | $res = '';
331 | if (is_numeric($datetime) && strlen($datetime) == 10) {
332 | $datetime = date('Y-m-d H:i:s', $datetime);
333 | } else {
334 | $datetime = strval($datetime);
335 | }
336 |
337 | if (!ValidateHelper::isDate2time($datetime)) {
338 | return $res;
339 | }
340 |
341 | //天干
342 | $sky = ['庚', '辛', '壬', '癸', '甲', '乙', '丙', '丁', '戊', '己'];
343 | //地支
344 | $earth = ['申', '酉', '戌', '亥', '子', '丑', '寅', '卯', '辰', '巳', '午', '未'];
345 |
346 | $year = intval(substr($datetime, 0, 4));
347 | $diff = $year - 1900 + 40;
348 | $res = $sky[$diff % 10] . $earth[$diff % 12];
349 | return $res;
350 | }
351 |
352 | /**
353 | * @param string $beginDate
354 | * @param string $endDate
355 | *
356 | * @param string $format
357 | *
358 | * @return array
359 | */
360 | public static function createDate(string $beginDate, string $endDate, string $format = 'Ymd')
361 | {
362 | $i = 0;
363 | $arr = [];
364 | $beginTime = strtotime($beginDate);
365 | $endTime = strtotime($endDate);
366 | while ($beginTime <= $endTime) {
367 | $arr[$i] = date($format, $beginTime);
368 | $beginTime = strtotime('+1 day', $beginTime);
369 | $i++;
370 | }
371 | return $arr;
372 | }
373 |
374 | /**
375 | * @param null|string $key
376 | *
377 | * @return null|string
378 | */
379 | public static function getChineseWeek(string $key = NULL)
380 | {
381 | $array = [
382 | 'Sunday' => "周日",
383 | "Monday" => "周一",
384 | "Tuesday" => "周二",
385 | "Wednesday" => "周三",
386 | "Thursday" => "周四",
387 | "Friday" => "周五",
388 | "Saturday" => "周六"
389 | ];
390 | return isset($array[$key]) ? $array[$key] : NULL;
391 | }
392 | }
393 |
--------------------------------------------------------------------------------
/app/Helper/FileHelper.php:
--------------------------------------------------------------------------------
1 | open($destination, $exist ? \ZIPARCHIVE::OVERWRITE : \ZIPARCHIVE::CREATE) !== true) {
104 | return false;
105 | }
106 |
107 | //add the files
108 | $desPath = dirname($destination);
109 | foreach ($validFiles as $file) {
110 | $localname = str_replace($desPath, '', $file);
111 | $zip->addFile($file, $localname);
112 | }
113 |
114 | //close the zip -- done!
115 | @$zip->close();
116 |
117 | //check to make sure the file exists
118 | return file_exists($destination);
119 | }
120 | return false;
121 | }
122 |
123 | /**
124 | * 将图片文件转换为base64编码
125 | *
126 | * @param string $file 图片文件路径
127 | *
128 | * @return string
129 | */
130 | public static function img2Base64(string $file): string
131 | {
132 | $res = '';
133 | if (empty($file) || !file_exists($file)) {
134 | return $res;
135 | }
136 |
137 | $imgInfo = getimagesize($file); //取得图片的大小,类型等
138 | $fp = fopen($file, 'r');
139 | if ($fp) {
140 | $fileContent = chunk_split(base64_encode(fread($fp, filesize($file)))); //base64编码
141 | $imgType = 'jpg';
142 | $typeNum = $imgInfo[2] ?? '';
143 | switch ($typeNum) {
144 | case 1:
145 | $imgType = 'gif';
146 | break;
147 | case 2:
148 | $imgType = 'jpg';
149 | break;
150 | case 3:
151 | $imgType = 'png';
152 | break;
153 | case 18:
154 | $imgType = 'webp';
155 | break;
156 | }
157 | $res = 'data:image/' . $imgType . ';base64,' . $fileContent; //合成图片的base64编码
158 | @fclose($fp);
159 | }
160 |
161 | return $res;
162 | }
163 |
164 | /**
165 | * 获取所有的文件MIME键值数组
166 | * @return array
167 | */
168 | public static function getAllMimes()
169 | {
170 | return [
171 | '323' => 'text/h323',
172 | '3gp' => 'video/3gpp',
173 | '7z' => 'application/x-7z-compressed',
174 | 'acx' => 'application/internet-property-stream',
175 | 'ai' => 'application/postscript',
176 | 'aif' => 'audio/x-aiff',
177 | 'aifc' => 'audio/x-aiff',
178 | 'aiff' => 'audio/x-aiff',
179 | 'apk' => 'application/vnd.android.package-archive',
180 | 'asf' => 'video/x-ms-asf',
181 | 'asr' => 'video/x-ms-asf',
182 | 'asx' => 'video/x-ms-asf',
183 | 'au' => 'audio/basic',
184 | 'avi' => 'video/x-msvideo',
185 | 'axs' => 'application/olescript',
186 | 'bas' => 'text/plain',
187 | 'bcpio' => 'application/x-bcpio',
188 | 'bin' => 'application/octet-stream',
189 | 'bmp' => 'image/bmp',
190 | 'bz' => 'application/x-bzip',
191 | 'bz2' => 'application/x-bzip2',
192 | 'c' => 'text/plain',
193 | 'cat' => 'application/vnd.ms-pkiseccat',
194 | 'cdf' => 'application/x-cdf',
195 | 'cer' => 'application/x-x509-ca-cert',
196 | 'class' => 'application/octet-stream',
197 | 'clp' => 'application/x-msclip',
198 | 'cmx' => 'image/x-cmx',
199 | 'cod' => 'image/cis-cod',
200 | 'conf' => 'text/plain',
201 | 'cpio' => 'application/x-cpio',
202 | 'crd' => 'application/x-mscardfile',
203 | 'crl' => 'application/pkix-crl',
204 | 'crt' => 'application/x-x509-ca-cert',
205 | 'csh' => 'application/x-csh',
206 | 'css' => 'text/css',
207 | 'csv' => 'text/csv',
208 | 'dcr' => 'application/x-director',
209 | 'der' => 'application/x-x509-ca-cert',
210 | 'dir' => 'application/x-director',
211 | 'dll' => 'application/x-msdownload',
212 | 'dms' => 'application/octet-stream',
213 | 'doc' => 'application/msword',
214 | 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
215 | 'dot' => 'application/msword',
216 | 'dvi' => 'application/x-dvi',
217 | 'dxr' => 'application/x-director',
218 | 'eps' => 'application/postscript',
219 | 'epub' => 'application/epub+zip',
220 | 'etx' => 'text/x-setext',
221 | 'evy' => 'application/envoy',
222 | 'exe' => 'application/octet-stream',
223 | 'fif' => 'application/fractals',
224 | 'flr' => 'x-world/x-vrml',
225 | 'flv' => 'video/x-flv',
226 | 'gif' => 'image/gif',
227 | 'gtar' => 'application/x-gtar',
228 | 'gz' => 'application/x-gzip',
229 | 'h' => 'text/plain',
230 | 'hdf' => 'application/x-hdf',
231 | 'hlp' => 'application/winhlp',
232 | 'hqx' => 'application/mac-binhex40',
233 | 'hta' => 'application/hta',
234 | 'htc' => 'text/x-component',
235 | 'htm' => 'text/html',
236 | 'html' => 'text/html',
237 | 'htt' => 'text/webviewhtml',
238 | 'ico' => 'image/x-icon',
239 | 'ief' => 'image/ief',
240 | 'iii' => 'application/x-iphone',
241 | 'ins' => 'application/x-internet-signup',
242 | 'isp' => 'application/x-internet-signup',
243 | 'jar' => 'application/java-archive',
244 | 'java' => 'text/plain',
245 | 'jfif' => 'image/pipeg',
246 | 'jpe' => 'image/jpeg',
247 | 'jpeg' => 'image/jpeg',
248 | 'jpg' => 'image/jpeg',
249 | 'js' => 'application/x-javascript',
250 | 'json' => 'application/json',
251 | 'latex' => 'application/x-latex',
252 | 'lha' => 'application/octet-stream',
253 | 'log' => 'text/plain',
254 | 'lsf' => 'video/x-la-asf',
255 | 'lsx' => 'video/x-la-asf',
256 | 'lzh' => 'application/octet-stream',
257 | 'm13' => 'application/x-msmediaview',
258 | 'm14' => 'application/x-msmediaview',
259 | 'm3u' => 'audio/x-mpegurl',
260 | 'man' => 'application/x-troff-man',
261 | 'mdb' => 'application/x-msaccess',
262 | 'me' => 'application/x-troff-me',
263 | 'mht' => 'message/rfc822',
264 | 'mhtml' => 'message/rfc822',
265 | 'mid' => 'audio/mid',
266 | 'mny' => 'application/x-msmoney',
267 | 'mov' => 'video/quicktime',
268 | 'movie' => 'video/x-sgi-movie',
269 | 'mp2' => 'video/mpeg',
270 | 'mp3' => 'audio/mpeg',
271 | 'mp4' => 'video/mp4',
272 | 'mpa' => 'video/mpeg',
273 | 'mpe' => 'video/mpeg',
274 | 'mpeg' => 'video/mpeg',
275 | 'mpg' => 'video/mpeg',
276 | 'mpp' => 'application/vnd.ms-project',
277 | 'mpv2' => 'video/mpeg',
278 | 'ms' => 'application/x-troff-ms',
279 | 'mvb' => 'application/x-msmediaview',
280 | 'nws' => 'message/rfc822',
281 | 'oda' => 'application/oda',
282 | 'p10' => 'application/pkcs10',
283 | 'p12' => 'application/x-pkcs12',
284 | 'p7b' => 'application/x-pkcs7-certificates',
285 | 'p7c' => 'application/x-pkcs7-mime',
286 | 'p7m' => 'application/x-pkcs7-mime',
287 | 'p7r' => 'application/x-pkcs7-certreqresp',
288 | 'p7s' => 'application/x-pkcs7-signature',
289 | 'pbm' => 'image/x-portable-bitmap',
290 | 'pdf' => 'application/pdf',
291 | 'pfx' => 'application/x-pkcs12',
292 | 'pgm' => 'image/x-portable-graymap',
293 | 'pko' => 'application/ynd.ms-pkipko',
294 | 'pma' => 'application/x-perfmon',
295 | 'pmc' => 'application/x-perfmon',
296 | 'pml' => 'application/x-perfmon',
297 | 'pmr' => 'application/x-perfmon',
298 | 'pmw' => 'application/x-perfmon',
299 | 'png' => 'image/png',
300 | 'pnm' => 'image/x-portable-anymap',
301 | 'pot' => 'application/vnd.ms-powerpoint',
302 | 'ppm' => 'image/x-portable-pixmap',
303 | 'pps' => 'application/vnd.ms-powerpoint',
304 | 'ppt' => 'application/vnd.ms-powerpoint',
305 | 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
306 | 'prf' => 'application/pics-rules',
307 | 'ps' => 'application/postscript',
308 | 'pub' => 'application/x-mspublisher',
309 | 'qt' => 'video/quicktime',
310 | 'ra' => 'audio/x-pn-realaudio',
311 | 'ram' => 'audio/x-pn-realaudio',
312 | 'rar' => 'application/x-rar-compressed',
313 | 'ras' => 'image/x-cmu-raster',
314 | 'rgb' => 'image/x-rgb',
315 | 'rmi' => 'audio/mid',
316 | 'rmvb' => 'audio/x-pn-realaudio',
317 | 'roff' => 'application/x-troff',
318 | 'rtf' => 'application/rtf',
319 | 'rtx' => 'text/richtext',
320 | 'scd' => 'application/x-msschedule',
321 | 'sct' => 'text/scriptlet',
322 | 'setpay' => 'application/set-payment-initiation',
323 | 'setreg' => 'application/set-registration-initiation',
324 | 'sh' => 'application/x-sh',
325 | 'shar' => 'application/x-shar',
326 | 'sit' => 'application/x-stuffit',
327 | 'snd' => 'audio/basic',
328 | 'spc' => 'application/x-pkcs7-certificates',
329 | 'spl' => 'application/futuresplash',
330 | 'src' => 'application/x-wais-source',
331 | 'sst' => 'application/vnd.ms-pkicertstore',
332 | 'stl' => 'application/vnd.ms-pkistl',
333 | 'stm' => 'text/html',
334 | 'sv4cpio' => 'application/x-sv4cpio',
335 | 'sv4crc' => 'application/x-sv4crc',
336 | 'svg' => 'image/svg+xml',
337 | 'swf' => 'application/x-shockwave-flash',
338 | 't' => 'application/x-troff',
339 | 'tar' => 'application/x-tar',
340 | 'tcl' => 'application/x-tcl',
341 | 'tex' => 'application/x-tex',
342 | 'texi' => 'application/x-texinfo',
343 | 'texinfo' => 'application/x-texinfo',
344 | 'tgz' => 'application/x-compressed',
345 | 'tif' => 'image/tiff',
346 | 'tiff' => 'image/tiff',
347 | 'tr' => 'application/x-troff',
348 | 'trm' => 'application/x-msterminal',
349 | 'tsv' => 'text/tab-separated-values',
350 | 'txt' => 'text/plain',
351 | 'uls' => 'text/iuls',
352 | 'ustar' => 'application/x-ustar',
353 | 'vcf' => 'text/x-vcard',
354 | 'vrml' => 'x-world/x-vrml',
355 | 'wav' => 'audio/x-wav',
356 | 'wcm' => 'application/vnd.ms-works',
357 | 'wdb' => 'application/vnd.ms-works',
358 | 'weba' => 'audio/webm',
359 | 'webm' => 'video/webm',
360 | 'webp' => 'image/webp',
361 | 'wks' => 'application/vnd.ms-works',
362 | 'wma' => 'audio/x-ms-wma',
363 | 'wmf' => 'application/x-msmetafile',
364 | 'wmv' => 'audio/x-ms-wmv',
365 | 'wps' => 'application/vnd.ms-works',
366 | 'wri' => 'application/x-mswrite',
367 | 'wrl' => 'x-world/x-vrml',
368 | 'wrz' => 'x-world/x-vrml',
369 | 'xaf' => 'x-world/x-vrml',
370 | 'xbm' => 'image/x-xbitmap',
371 | 'xhtml' => 'application/xhtml+xml',
372 | 'xla' => 'application/vnd.ms-excel',
373 | 'xlc' => 'application/vnd.ms-excel',
374 | 'xlm' => 'application/vnd.ms-excel',
375 | 'xls' => 'application/vnd.ms-excel',
376 | 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
377 | 'xlt' => 'application/vnd.ms-excel',
378 | 'xlw' => 'application/vnd.ms-excel',
379 | 'xml' => 'text/plain',
380 | 'xof' => 'x-world/x-vrml',
381 | 'xpm' => 'image/x-xpixmap',
382 | 'xwd' => 'image/x-xwindowdump',
383 | 'z' => 'application/x-compress',
384 | 'zip' => 'application/zip',
385 | ];
386 | }
387 |
388 | /**
389 | * 获取文件的mime类型
390 | *
391 | * @param string $file 文件路径
392 | *
393 | * @return string
394 | */
395 | public static function getFileMime(string $file): string
396 | {
397 | $allMimes = self::getAllMimes();
398 | $ext = self::getFileExt($file);
399 | $res = $allMimes[$ext] ?? '';
400 |
401 | return $res;
402 | }
403 |
404 | /**
405 | * 把整个文件读入一个数组中,每行作为一个元素.
406 | *
407 | * @param string $path
408 | *
409 | * @return array
410 | */
411 | public static function readInArray(string $path): array
412 | {
413 | if (!is_file($path)) {
414 | return [];
415 | }
416 |
417 | return file($path, FILE_IGNORE_NEW_LINES);
418 | }
419 | }
420 |
--------------------------------------------------------------------------------
/app/Helper/ArrayHelper.php:
--------------------------------------------------------------------------------
1 | $v) {
47 | $hash = md5(serialize($v));
48 | if (!in_array($hash, $hasArr)) {
49 | array_push($hasArr, $hash);
50 | if ($keepKey) {
51 | $res[$k] = $v;
52 | } else {
53 | $res[] = $v;
54 | }
55 | }
56 | }
57 | unset($hasArr);
58 | return $res;
59 | }
60 |
61 | /**
62 | * 取多维数组的最底层值
63 | *
64 | * @param array $arr
65 | * @param array $vals 结果
66 | *
67 | * @return array
68 | */
69 | public static function multiArrayValues(array $arr, &$vals = []): array
70 | {
71 | foreach ($arr as $v) {
72 | if (is_array($v)) {
73 | self::multiArrayValues($v, $vals);
74 | } else {
75 | array_push($vals, $v);
76 | }
77 | }
78 | return $vals;
79 | }
80 |
81 | /**
82 | * 对数组元素递归求值
83 | *
84 | * @param array $arr
85 | * @param callable $fn 回调函数
86 | *
87 | * @return array
88 | */
89 | public static function mapRecursive(array $arr, callable $fn): array
90 | {
91 | $res = [];
92 | foreach ($arr as $k => $v) {
93 | $res[$k] = is_array($v) ? (self::mapRecursive($v, $fn)) : call_user_func($fn, $v);
94 | }
95 | return $res;
96 | }
97 |
98 | /**
99 | * 对象转数组
100 | *
101 | * @param mixed $val
102 | *
103 | * @return array
104 | */
105 | public static function object2Array($val): array
106 | {
107 | $arr = is_object($val) ? get_object_vars($val) : $val;
108 | if (is_array($arr)) {
109 | foreach ($arr as $k => $item) {
110 | if (is_array($item) && !empty($item)) {
111 | $arr[$k] = array_map(__METHOD__, $item);
112 | }
113 | }
114 | } else {
115 | $arr = (array)$arr;
116 | }
117 | return $arr;
118 | }
119 |
120 | /**
121 | * 数组转对象
122 | *
123 | * @param array $arr
124 | *
125 | * @return object
126 | */
127 | public static function array2Object(array $arr): object
128 | {
129 | foreach ($arr as $k => $item) {
130 | if (is_array($item)) {
131 | $arr[$k] = empty($item) ? new \stdClass() : call_user_func(__METHOD__, $item);
132 | }
133 | }
134 | return (object)$arr;
135 | }
136 |
137 | /**
138 | * 从数组中剪切元素,将改变原数组,并返回剪切的元素数组.
139 | *
140 | * @param array $arr 原数组
141 | * @param mixed ...$keys 要剪切的元素键,一个或多个
142 | *
143 | * @return array
144 | */
145 | public static function cutItems(array &$arr, ...$keys): array
146 | {
147 | $res = [];
148 | foreach ($keys as $key) {
149 | if (isset($arr[$key])) {
150 | array_push($res, $arr[$key]);
151 | unset($arr[$key]);
152 | } else {
153 | array_push($res, null);
154 | }
155 | }
156 | return $res;
157 | }
158 |
159 | /**
160 | * 数组元素组合(按元素值组合)
161 | *
162 | * @param array $arr 数组
163 | * @param int $len 组合长度(从数组中取几个元素来组合)
164 | * @param string $separator 分隔符
165 | *
166 | * @return array
167 | */
168 | private static function _combinationValue(array $arr, int $len, string $separator = ''): array
169 | {
170 | $res = [];
171 | if ($len <= 1) {
172 | return $arr;
173 | } elseif ($len >= count($arr)) {
174 | array_push($res, implode($separator, $arr));
175 | return $res;
176 | }
177 | $firstItem = array_shift($arr);
178 | $newArr = array_values($arr);
179 | $list1 = self::_combinationValue($newArr, $len - 1, $separator);
180 | foreach ($list1 as $item) {
181 | $str = strval($firstItem) . $separator . strval($item);
182 | array_push($res, $str);
183 | }
184 | $list2 = self::_combinationValue($newArr, $len, $separator);
185 | foreach ($list2 as $item) {
186 | array_push($res, strval($item));
187 | }
188 | return $res;
189 | }
190 |
191 | /**
192 | * 数组元素组合(按元素值和位置组合)
193 | *
194 | * @param array $arr 数组
195 | * @param string $separator
196 | *
197 | * @return array
198 | */
199 | private static function _combinationPosition(array $arr, string $separator = ''): array
200 | {
201 | $len = count($arr);
202 | $res = self::combinationAll($arr, $separator);
203 | if ($len >= 2) {
204 | foreach ($arr as $k => $item) {
205 | $newArr = $arr;
206 | self::cutItems($newArr, $k);
207 | $newRes = self::_combinationPosition($newArr, $separator);
208 | if (!empty($newRes)) {
209 | $res = array_merge($res, $newRes);
210 | }
211 | }
212 | }
213 | return $res;
214 | }
215 |
216 | /**
217 | * 数组全排列,f(n)=n!.
218 | *
219 | * @param array $arr 要排列组合的数组
220 | * @param string $separator 分隔符
221 | *
222 | * @return array
223 | */
224 | public static function combinationAll(array $arr, string $separator = '')
225 | {
226 | $len = count($arr);
227 | if ($len == 0) {
228 | return [];
229 | } elseif ($len == 1) {
230 | return $arr;
231 | }
232 | //保证初始数组是有序的
233 | sort($arr);
234 | $last = $len - 1; //尾部元素下标
235 | $x = $last;
236 | $res = [];
237 | array_push($res, implode($separator, $arr)); //第一种组合
238 | while (true) {
239 | $y = $x--; //相邻的两个元素
240 | if ($arr[$x] < $arr[$y]) { //如果前一个元素的值小于后一个元素的值
241 | $z = $last;
242 | while ($arr[$x] > $arr[$z]) { //从尾部开始,找到第一个大于 $x 元素的值
243 | $z--;
244 | }
245 | /* 交换 $x 和 $z 元素的值 */
246 | [$arr[$x], $arr[$z]] = [$arr[$z], $arr[$x]];
247 | /* 将 $y 之后的元素全部逆向排列 */
248 | for ($i = $last; $i > $y; $i--, $y++) {
249 | [$arr[$i], $arr[$y]] = [$arr[$y], $arr[$i]];
250 | }
251 | array_push($res, implode($separator, $arr));
252 | $x = $last;
253 | }
254 | if ($x == 0) { //全部组合完毕
255 | break;
256 | }
257 | }
258 | return $res;
259 | }
260 |
261 | /**
262 | * 以字符串形式,排列组合数组的元素,全部可能的组合.
263 | *
264 | * @param array $arr 要排列组合的数组
265 | * @param string $separator 分隔符
266 | * @param bool $unique 组合中的元素是否唯一.设为true时,只考虑元素值而忽略元素位置,则[a,b]与[b,a]是相同的组合;设为false时,同时考虑元素值和元素位置,则[a,b]与[b,a]是不同的组合.
267 | *
268 | * @return array
269 | */
270 | public static function combinationFull(array $arr, string $separator = '', bool $unique = true): array
271 | {
272 | $res = [];
273 | $len = count($arr);
274 | if ($unique) {
275 | for ($i = 1; $i <= $len; $i++) {
276 | $news = self::_combinationValue($arr, $i, $separator);
277 | if (!empty($news)) {
278 | $res = array_merge($res, $news);
279 | }
280 | }
281 | } else {
282 | $news = self::_combinationPosition($arr, $separator);
283 | if (!empty($news)) {
284 | $res = array_merge($res, $news);
285 | }
286 | $res = array_unique($res);
287 | sort($res);
288 | }
289 | return $res;
290 | }
291 |
292 | /**
293 | * 从数组中搜索对应元素(单个).若匹配,返回该元素;否则返回false.
294 | *
295 | * @param array $arr 要搜索的数据数组
296 | * @param array $conditions 条件数组
297 | * @param bool $delSource 若匹配,是否删除原数组的该元素
298 | *
299 | * @return bool|mixed
300 | */
301 | public static function searchItem(array &$arr, array $conditions, bool $delSource = false)
302 | {
303 | if (empty($arr) || empty($conditions)) {
304 | return false;
305 | }
306 | $condLen = count($conditions);
307 | foreach ($arr as $i => $item) {
308 | $chk = 0;
309 | foreach ($conditions as $k => $v) {
310 | if (is_bool($v) && $v) {
311 | $chk++;
312 | } elseif (isset($item[$k]) && $item[$k] == $v) {
313 | $chk++;
314 | }
315 | }
316 | //条件完全匹配
317 | if ($chk == $condLen) {
318 | if ($delSource) {
319 | unset($arr[$i]);
320 | }
321 | return $item;
322 | }
323 | }
324 | return false;
325 | }
326 |
327 | /**
328 | * 从数组中搜索对应元素(多个).若匹配,返回新数组,包含一个以上元素;否则返回空数组.
329 | *
330 | * @param array $arr 要搜索的数据数组
331 | * @param array $conditions 条件数组
332 | * @param bool $delSource 若匹配,是否删除原数组的该元素
333 | *
334 | * @return array
335 | */
336 | public static function searchMutil(array &$arr, array $conditions, bool $delSource = false): array
337 | {
338 | $res = [];
339 | if (empty($arr) || empty($conditions)) {
340 | return $res;
341 | }
342 | $condLen = count($conditions);
343 | foreach ($arr as $i => $item) {
344 | $chk = 0;
345 | foreach ($conditions as $k => $v) {
346 | if (is_bool($v) && $v) {
347 | $chk++;
348 | } elseif (isset($item[$k]) && $item[$k] == $v) {
349 | $chk++;
350 | }
351 | }
352 | //条件完全匹配
353 | if ($chk == $condLen) {
354 | if ($delSource) {
355 | unset($arr[$i]);
356 | }
357 | array_push($res, $item);
358 | }
359 | }
360 | return $res;
361 | }
362 |
363 | /**
364 | * 二维数组按指定的键值排序.若元素的键值不存在,则返回空数组.
365 | *
366 | * @param array $arr
367 | * @param string $key 排序的键
368 | * @param string $sort 排序方式:desc/asc
369 | * @param bool $keepKey 是否保留外层键值
370 | *
371 | * @return array
372 | */
373 | public static function sortByField(array $arr, string $key, string $sort = 'desc', bool $keepKey = false): array
374 | {
375 | $res = [];
376 | $values = [];
377 | $sort = strtolower(trim($sort));
378 | foreach ($arr as $k => $v) {
379 | if (!isset($v[$key])) {
380 | return [];
381 | }
382 | $values[$k] = $v[$key];
383 | }
384 | if ($sort === 'asc') {
385 | asort($values);
386 | } else {
387 | arsort($values);
388 | }
389 | reset($values);
390 | foreach ($values as $k => $v) {
391 | if ($keepKey) {
392 | $res[$k] = $arr[$k];
393 | } else {
394 | $res[] = $arr[$k];
395 | }
396 | }
397 | return $res;
398 | }
399 |
400 | /**
401 | * 数组按照多字段排序
402 | *
403 | * @param array $arr 多维数组
404 | * @param array ...$sorts 多个排序信息.其中的元素必须是数组,形如['field', SORT_ASC],或者['field'];若没有排序类型,则默认 SORT_DESC .
405 | *
406 | * @return array
407 | */
408 | public static function sortByMultiFields(array $arr, array ...$sorts): array
409 | {
410 | if (empty($arr)) {
411 | return [];
412 | }
413 | if (!empty($sorts)) {
414 | $sortConditions = [];
415 | foreach ($sorts as $sortInfo) {
416 | //$sortInfo必须形如['field', SORT_ASC],或者['field']
417 | $file = strval(current($sortInfo));
418 | $sort = intval($sortInfo[1] ?? SORT_DESC);
419 | $tmpArr = [];
420 | foreach ($arr as $k => $item) {
421 | //排序字段不存在
422 | if (empty($file) || !isset($item[$file])) {
423 | return [];
424 | }
425 | $tmpArr[$k] = $item[$file];
426 | }
427 | array_push($sortConditions, $tmpArr, $sort);
428 | }
429 | array_push($sortConditions, $arr);
430 | array_multisort(...$sortConditions);
431 | return end($sortConditions);
432 | }
433 | return $arr;
434 | }
435 |
436 | /**
437 | * 交换2个元素的值
438 | *
439 | * @param array $arr
440 | * @param int|string $keya 键a
441 | * @param int|string $keyb 键b
442 | *
443 | * @return bool
444 | */
445 | public static function swapItem(array &$arr, $keya, $keyb): bool
446 | {
447 | $keya = strval($keya);
448 | $keyb = strval($keyb);
449 | if (isset($arr[$keya]) && isset($arr[$keyb])) {
450 | [$arr[$keya], $arr[$keyb]] = [$arr[$keyb], $arr[$keya]];
451 | return true;
452 | }
453 | return false;
454 | }
455 |
456 | /**
457 | * 设置数组带点的键值.
458 | * 若键为空,则会替换原数组为[$value].
459 | *
460 | * @param array $arr 原数组
461 | * @param mixed $key 键,可带点的多级,如row.usr.name
462 | * @param mixed $value 值
463 | */
464 | public static function setDotKey(array &$arr, $key, $value): void
465 | {
466 | if (is_null($key) || $key == '') {
467 | $arr = is_array($value) ? $value : (array)$value;
468 | return;
469 | }
470 | $keyStr = strval($key);
471 | if (ValidateHelper::isInteger($keyStr) || strpos($keyStr, '.') === false) {
472 | $arr[$keyStr] = $value;
473 | return;
474 | }
475 | $keys = explode('.', $keyStr);
476 | while (count($keys) > 1) {
477 | $key = array_shift($keys);
478 | if (!array_key_exists($key, $arr)) {
479 | $arr[$key] = [];
480 | } elseif (!is_array($arr[$key])) {
481 | $arr[$key] = (array)$arr[$key];
482 | }
483 | $arr = &$arr[$key];
484 | }
485 | $arr[array_shift($keys)] = $value;
486 | }
487 |
488 | /**
489 | * 获取数组带点的键值.
490 | *
491 | * @param array $arr 数组
492 | * @param mixed $key 键,可带点的多级,如row.usr.name
493 | * @param mixed $default 默认值
494 | *
495 | * @return mixed|null
496 | */
497 | public static function getDotKey(array $arr, $key = null, $default = null)
498 | {
499 | if (is_null($key) || $key == '') {
500 | return $arr;
501 | }
502 | $keyStr = strval($key);
503 | if (ValidateHelper::isInteger($keyStr) || strpos($keyStr, '.') === false) {
504 | return $arr[$keyStr] ?? $default;
505 | }
506 | $keys = explode('.', $keyStr);
507 | foreach ($keys as $key) {
508 | if (is_array($arr) && array_key_exists($key, $arr)) {
509 | $arr = $arr[$key];
510 | } else {
511 | return $default;
512 | }
513 | }
514 | return $arr;
515 | }
516 |
517 | /**
518 | * 数组是否存在带点的键
519 | *
520 | * @param array $arr
521 | * @param mixed $key 键,可带点的多级,如row.usr.name
522 | *
523 | * @return bool
524 | */
525 | public static function hasDotKey(array $arr, $key = null): bool
526 | {
527 | if (is_null($key) || $key == '') {
528 | return false;
529 | }
530 | $keyStr = strval($key);
531 | if (ValidateHelper::isInteger($keyStr) || strpos($keyStr, '.') === false) {
532 | return array_key_exists($keyStr, $arr);
533 | }
534 | $keys = explode('.', $keyStr);
535 | foreach ($keys as $key) {
536 | if (is_null($key) || $key == '') {
537 | return false;
538 | } elseif (!is_array($arr) || !array_key_exists($key, $arr)) {
539 | return false;
540 | }
541 | $arr = $arr[$key];
542 | }
543 | return true;
544 | }
545 | }
546 |
--------------------------------------------------------------------------------