├── 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 | [![Php Version](https://img.shields.io/badge/php-%3E=7.4-brightgreen.svg?maxAge=2592000)](https://secure.php.net/) 2 | [![Swoole Version](https://img.shields.io/badge/swoole-%3E=4.5.2-brightgreen.svg?maxAge=2592000)](https://github.com/swoole/swoole-src) 3 | [![Hyperf Version](https://img.shields.io/badge/hyperf-%3E=2.0.1-brightgreen.svg?maxAge=2592000)](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 | ![avatar](wechat.jpg) 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 | ![avatar](./storage/0702A46032714AC6E3412C9A29C5029B.jpg) 107 | 108 | ## 链路追踪 109 | ![avatar](./storage/9A28EDE3E00DB6A665677D48D3A864B3.jpg) 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 | --------------------------------------------------------------------------------