lizhen_gitee 6 ماه پیش
کامیت
67f6c907b9
100فایلهای تغییر یافته به همراه9282 افزوده شده و 0 حذف شده
  1. 5 0
      .dockerignore
  2. 37 0
      .env.example
  3. 54 0
      .github/workflows/Dockerfile
  4. 12 0
      .github/workflows/build.yml
  5. 25 0
      .github/workflows/release.yml
  6. 19 0
      .gitignore
  7. 57 0
      .gitlab-ci.yml
  8. 106 0
      .php-cs-fixer.php
  9. 12 0
      .phpstorm.meta.php
  10. 54 0
      Dockerfile
  11. 21 0
      LICENSE
  12. 63 0
      README.md
  13. 30 0
      app/Controller/AbstractController.php
  14. 57 0
      app/Controller/Api/Notify/PaymentController.php
  15. 337 0
      app/Controller/Api/v1/CommonController.php
  16. 174 0
      app/Controller/Api/v1/DemoController.php
  17. 122 0
      app/Controller/Api/v1/HomeController.php
  18. 184 0
      app/Controller/Api/v1/LiveController.php
  19. 660 0
      app/Controller/Api/v1/OrderController.php
  20. 41 0
      app/Controller/Api/v1/SystemController.php
  21. 328 0
      app/Controller/Api/v1/UserController.php
  22. 94 0
      app/Controller/Api/v1/WechatController.php
  23. 58 0
      app/Exception/Handler/AppExceptionHandler.php
  24. 44 0
      app/Exception/Handler/ValidationExceptionHandler.php
  25. 45 0
      app/Job/DemoJob.php
  26. 16 0
      app/Kernel/Log.php
  27. 13 0
      app/Kernel/StdoutLogInterface.php
  28. 66 0
      app/Listener/DbQueryExecutedListener.php
  29. 41 0
      app/Listener/MqttListener.php
  30. 35 0
      app/Listener/ResumeExitCoordinatorListener.php
  31. 16 0
      app/Master/Enum/PassportEnum.php
  32. 27 0
      app/Master/Enum/RedisKeyEnum.php
  33. 20 0
      app/Master/Framework/Extend/Module.php
  34. 207 0
      app/Master/Framework/Helper/common.php
  35. 90 0
      app/Master/Framework/Library/AliCloud/AliSms.php
  36. 135 0
      app/Master/Framework/Library/Easywechat/EasyModule.php
  37. 135 0
      app/Master/Framework/Library/Easywechat/MiniApp.php
  38. 56 0
      app/Master/Framework/Library/Easywechat/OfficialService.php
  39. 186 0
      app/Master/Framework/Library/Easywechat/PayService.php
  40. 27 0
      app/Master/Framework/Library/Extend/Core.php
  41. 23 0
      app/Master/Framework/Library/Extend/Module.php
  42. 170 0
      app/Master/Framework/Library/GeTui/Push.php
  43. 265 0
      app/Master/Framework/Library/Google/Maps.php
  44. 126 0
      app/Master/Framework/Library/Library.php
  45. 119 0
      app/Master/Framework/Library/Mqtt/MqttClient.php
  46. 38 0
      app/Master/Framework/Library/Mqtt/Subscribe.php
  47. 328 0
      app/Master/Framework/Library/Tencent/GetUserSig.php
  48. 170 0
      app/Master/Framework/Library/Tencent/TencentIm.php
  49. 52 0
      app/Master/Framework/Library/Twilio/Sms.php
  50. 166 0
      app/Middleware/ApiAgent.php
  51. 184 0
      app/Middleware/ApiAgentBak.php
  52. 63 0
      app/Middleware/ApiSign.php
  53. 186 0
      app/Middleware/DriverAgent.php
  54. 63 0
      app/Middleware/DriverSign.php
  55. 44 0
      app/Model/Arts/AgreementModel.php
  56. 33 0
      app/Model/Arts/CarSeatModel.php
  57. 34 0
      app/Model/Arts/CityModel.php
  58. 51 0
      app/Model/Arts/CouponModel.php
  59. 28 0
      app/Model/Arts/DemoModel.php
  60. 59 0
      app/Model/Arts/DriverCarModel.php
  61. 56 0
      app/Model/Arts/DriverLicenseModel.php
  62. 144 0
      app/Model/Arts/DriverMessageModel.php
  63. 79 0
      app/Model/Arts/DriverMessageReadModel.php
  64. 177 0
      app/Model/Arts/DriverModel.php
  65. 67 0
      app/Model/Arts/DriverMoneyLogModel.php
  66. 175 0
      app/Model/Arts/DriverOnlineLogModel.php
  67. 91 0
      app/Model/Arts/DriverWalletModel.php
  68. 37 0
      app/Model/Arts/LiveGiftModel.php
  69. 86 0
      app/Model/Arts/LiveRoomPkModel.php
  70. 144 0
      app/Model/Arts/MessageModel.php
  71. 79 0
      app/Model/Arts/MessageReadModel.php
  72. 36 0
      app/Model/Arts/MobileAreaCodeModel.php
  73. 177 0
      app/Model/Arts/OrderDriverModel.php
  74. 437 0
      app/Model/Arts/OrderModel.php
  75. 31 0
      app/Model/Arts/OrderPointModel.php
  76. 43 0
      app/Model/Arts/QuestionAnswerModel.php
  77. 43 0
      app/Model/Arts/QuestionReasonModel.php
  78. 31 0
      app/Model/Arts/RaiseAmountModel.php
  79. 138 0
      app/Model/Arts/SmsCodeModel.php
  80. 90 0
      app/Model/Arts/UserAddressModel.php
  81. 118 0
      app/Model/Arts/UserCouponModel.php
  82. 51 0
      app/Model/Arts/UserModel.php
  83. 67 0
      app/Model/Arts/UserMoneyLogModel.php
  84. 88 0
      app/Model/Arts/UserWalletModel.php
  85. 51 0
      app/Model/Arts/VersionAppModel.php
  86. 49 0
      app/Model/Framework/AdminSetupModel.php
  87. 293 0
      app/Model/Model.php
  88. 60 0
      app/Process/MqttProcess.php
  89. 28 0
      app/Request/Api/v1/Common/AgreementRequest.php
  90. 30 0
      app/Request/Api/v1/Common/DirectionsRequest.php
  91. 28 0
      app/Request/Api/v1/Common/GeocodeRequest.php
  92. 71 0
      app/Request/Api/v1/Common/MessageRequest.php
  93. 31 0
      app/Request/Api/v1/Common/PlaceAutoSearchRequest.php
  94. 28 0
      app/Request/Api/v1/Common/PlaceDetailsRequest.php
  95. 28 0
      app/Request/Api/v1/Common/PlaceSearchRequest.php
  96. 28 0
      app/Request/Api/v1/Common/VersionRequest.php
  97. 75 0
      app/Request/Api/v1/DemoIndexRequest.php
  98. 29 0
      app/Request/Api/v1/Order/AddMoneyRequest.php
  99. 29 0
      app/Request/Api/v1/Order/CancelRequest.php
  100. 28 0
      app/Request/Api/v1/Order/DetailRequest.php

+ 5 - 0
.dockerignore

@@ -0,0 +1,5 @@
+**
+!app/
+!bin/
+!config/
+!composer.*

+ 37 - 0
.env.example

@@ -0,0 +1,37 @@
+APP_NAME=skeleton
+APP_ENV=dev
+APP_DEBUG=true
+APP_URL=http://127.0.0.1
+CDN_URL=
+
+HTTP_HOST=0.0.0.0
+HTTP_PORT=9501
+
+DB_DRIVER=mysql
+DB_HOST=localhost
+DB_PORT=3306
+DB_DATABASE=hyperf
+DB_USERNAME=root
+DB_PASSWORD=
+DB_PREFIX=
+
+REDIS_HOST=localhost
+REDIS_AUTH=(null)
+REDIS_PORT=6379
+REDIS_PREFIX=c:
+REDIS_DB=0
+
+MQTT_CLIENT_HOST=127.0.0.1
+MQTT_CLIENT_PORT=1883
+MQTT_CLIENT_USERNAME=
+MQTT_CLIENT_PASSWORD=
+MQTT_CLIENT_CLIENT_ID=hyperf
+
+WECHAT_PAY_MCH_ID=
+WECHAT_PAY_SECRET_KEY_V3=
+WECHAT_PAY_SECRET_KEY_V2=
+
+WECHAT_MINI_APP_APPID=
+WECHAT_MINI_APP_SECRET=
+WECHAT_MINI_APP_TOKEN=
+WECHAT_MINI_APP_AES_KEY=

+ 54 - 0
.github/workflows/Dockerfile

@@ -0,0 +1,54 @@
+# Default Dockerfile
+#
+# @link     https://www.hyperf.io
+# @document https://hyperf.wiki
+# @contact  group@hyperf.io
+# @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+
+FROM hyperf/hyperf:8.1-alpine-v3.18-swoole
+LABEL maintainer="Hyperf Developers <group@hyperf.io>" version="1.0" license="MIT" app.name="Hyperf"
+
+##
+# ---------- env settings ----------
+##
+# --build-arg timezone=Asia/Shanghai
+ARG timezone
+
+ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \
+    APP_ENV=prod \
+    SCAN_CACHEABLE=(true)
+
+# update
+RUN set -ex \
+    # show php version and extensions
+    && php -v \
+    && php -m \
+    && php --ri swoole \
+    #  ---------- some config ----------
+    && cd /etc/php* \
+    # - config PHP
+    && { \
+        echo "upload_max_filesize=128M"; \
+        echo "post_max_size=128M"; \
+        echo "memory_limit=1G"; \
+        echo "date.timezone=${TIMEZONE}"; \
+    } | tee conf.d/99_overrides.ini \
+    # - config timezone
+    && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \
+    && echo "${TIMEZONE}" > /etc/timezone \
+    # ---------- clear works ----------
+    && rm -rf /var/cache/apk/* /tmp/* /usr/share/man \
+    && echo -e "\033[42;37m Build Completed :).\033[0m\n"
+
+WORKDIR /opt/www
+
+# Composer Cache
+# COPY ./composer.* /opt/www/
+# RUN composer install --no-dev --no-scripts
+
+COPY . /opt/www
+RUN print "\n" | composer install -o && php bin/hyperf.php
+
+EXPOSE 9501
+
+ENTRYPOINT ["php", "/opt/www/bin/hyperf.php", "start"]

+ 12 - 0
.github/workflows/build.yml

@@ -0,0 +1,12 @@
+name: Build Docker
+
+on: [push, pull_request]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v2
+      - name: Build
+        run: cp -rf .github/workflows/Dockerfile . && docker build -t hyperf .

+ 25 - 0
.github/workflows/release.yml

@@ -0,0 +1,25 @@
+on:
+  push:
+    # Sequence of patterns matched against refs/tags
+    tags:
+      - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
+
+name: Release
+
+jobs:
+  release:
+    name: Release
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v2
+      - name: Create Release
+        id: create_release
+        uses: actions/create-release@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          tag_name: ${{ github.ref }}
+          release_name: Release ${{ github.ref }}
+          draft: false
+          prerelease: false

+ 19 - 0
.gitignore

@@ -0,0 +1,19 @@
+.buildpath
+.settings/
+.project
+*.patch
+.idea/
+.git/
+runtime/*
+vendor/
+composer.lock
+.phpintel/
+.env
+.DS_Store
+.phpunit*
+*.cache
+.vscode/
+*.sql
+*.zip
+*.tar.gz
+*.rar

+ 57 - 0
.gitlab-ci.yml

@@ -0,0 +1,57 @@
+# usermod -aG docker gitlab-runner
+
+stages:
+  - build
+  - deploy
+
+variables:
+  PROJECT_NAME: hyperf
+  REGISTRY_URL: registry-docker.org
+
+build_test_docker:
+  stage: build
+  before_script:
+#    - git submodule sync --recursive
+#    - git submodule update --init --recursive
+  script:
+    - docker build . -t $PROJECT_NAME
+    - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:test
+    - docker push $REGISTRY_URL/$PROJECT_NAME:test
+  only:
+    - test
+  tags:
+    - builder
+
+deploy_test_docker:
+  stage: deploy
+  script:
+    - docker stack deploy -c deploy.test.yml --with-registry-auth $PROJECT_NAME
+  only:
+    - test
+  tags:
+    - test
+
+build_docker:
+  stage: build
+  before_script:
+#    - git submodule sync --recursive
+#    - git submodule update --init --recursive
+  script:
+    - docker build . -t $PROJECT_NAME
+    - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME
+    - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:latest
+    - docker push $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME
+    - docker push $REGISTRY_URL/$PROJECT_NAME:latest
+  only:
+    - tags
+  tags:
+    - builder
+
+deploy_docker:
+  stage: deploy
+  script:
+    - echo SUCCESS
+  only:
+    - tags
+  tags:
+    - builder

+ 106 - 0
.php-cs-fixer.php

@@ -0,0 +1,106 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+$header = <<<'EOF'
+This file is part of Hyperf.
+
+@link     https://www.hyperf.io
+@document https://hyperf.wiki
+@contact  group@hyperf.io
+@license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+EOF;
+
+return (new PhpCsFixer\Config())
+    ->setRiskyAllowed(true)
+    ->setRules([
+        '@PSR2' => true,
+        '@Symfony' => true,
+        '@DoctrineAnnotation' => true,
+        '@PhpCsFixer' => true,
+        'header_comment' => [
+            'comment_type' => 'PHPDoc',
+            'header' => $header,
+            'separate' => 'none',
+            'location' => 'after_declare_strict',
+        ],
+        'array_syntax' => [
+            'syntax' => 'short',
+        ],
+        'list_syntax' => [
+            'syntax' => 'short',
+        ],
+        'concat_space' => [
+            'spacing' => 'one',
+        ],
+        'global_namespace_import' => [
+            'import_classes' => true,
+            'import_constants' => true,
+            'import_functions' => null,
+        ],
+        'blank_line_before_statement' => [
+            'statements' => [
+                'declare',
+            ],
+        ],
+        'general_phpdoc_annotation_remove' => [
+            'annotations' => [
+                'author',
+            ],
+        ],
+        'ordered_imports' => [
+            'imports_order' => [
+                'class', 'function', 'const',
+            ],
+            'sort_algorithm' => 'alpha',
+        ],
+        'single_line_comment_style' => [
+            'comment_types' => [
+            ],
+        ],
+        'yoda_style' => [
+            'always_move_variable' => false,
+            'equal' => false,
+            'identical' => false,
+        ],
+        'phpdoc_align' => [
+            'align' => 'left',
+        ],
+        'multiline_whitespace_before_semicolons' => [
+            'strategy' => 'no_multi_line',
+        ],
+        'constant_case' => [
+            'case' => 'lower',
+        ],
+        'class_attributes_separation' => true,
+        'combine_consecutive_unsets' => true,
+        'declare_strict_types' => true,
+        'linebreak_after_opening_tag' => true,
+        'lowercase_static_reference' => true,
+        'no_useless_else' => true,
+        'no_unused_imports' => true,
+        'not_operator_with_successor_space' => true,
+        'not_operator_with_space' => false,
+        'ordered_class_elements' => true,
+        'php_unit_strict' => false,
+        'phpdoc_separation' => false,
+        'single_quote' => true,
+        'standardize_not_equals' => true,
+        'multiline_comment_opening_closing' => true,
+        'single_line_empty_body' => false,
+    ])
+    ->setFinder(
+        PhpCsFixer\Finder::create()
+            ->exclude('public')
+            ->exclude('runtime')
+            ->exclude('vendor')
+            ->in(__DIR__)
+    )
+    ->setUsingCache(false);

+ 12 - 0
.phpstorm.meta.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace PHPSTORM_META {
+    // Reflect
+    override(\Psr\Container\ContainerInterface::get(0), map(['' => '@']));
+    override(\Hyperf\Context\Context::get(0), map(['' => '@']));
+    override(\make(0), map(['' => '@']));
+    override(\di(0), map(['' => '@']));
+    override(\Hyperf\Support\make(0), map(['' => '@']));
+    override(\Hyperf\Support\optional(0), type(0));
+    override(\Hyperf\Tappable\tap(0), type(0));
+}

+ 54 - 0
Dockerfile

@@ -0,0 +1,54 @@
+# Default Dockerfile
+#
+# @link     https://www.hyperf.io
+# @document https://hyperf.wiki
+# @contact  group@hyperf.io
+# @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+
+FROM hyperf/hyperf:8.1-alpine-v3.18-swoole
+LABEL maintainer="Hyperf Developers <group@hyperf.io>" version="1.0" license="MIT" app.name="Hyperf"
+
+##
+# ---------- env settings ----------
+##
+# --build-arg timezone=Asia/Shanghai
+ARG timezone
+
+ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \
+    APP_ENV=prod \
+    SCAN_CACHEABLE=(true)
+
+# update
+RUN set -ex \
+    # show php version and extensions
+    && php -v \
+    && php -m \
+    && php --ri swoole \
+    #  ---------- some config ----------
+    && cd /etc/php* \
+    # - config PHP
+    && { \
+        echo "upload_max_filesize=128M"; \
+        echo "post_max_size=128M"; \
+        echo "memory_limit=1G"; \
+        echo "date.timezone=${TIMEZONE}"; \
+    } | tee conf.d/99_overrides.ini \
+    # - config timezone
+    && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \
+    && echo "${TIMEZONE}" > /etc/timezone \
+    # ---------- clear works ----------
+    && rm -rf /var/cache/apk/* /tmp/* /usr/share/man \
+    && echo -e "\033[42;37m Build Completed :).\033[0m\n"
+
+WORKDIR /opt/www
+
+# Composer Cache
+# COPY ./composer.* /opt/www/
+# RUN composer install --no-dev --no-scripts
+
+COPY . /opt/www
+RUN composer install --no-dev -o && php bin/hyperf.php
+
+EXPOSE 9501
+
+ENTRYPOINT ["php", "/opt/www/bin/hyperf.php", "start"]

+ 21 - 0
LICENSE

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

+ 63 - 0
README.md

@@ -0,0 +1,63 @@
+# Introduction
+
+This is a skeleton application using the Hyperf framework. This application is meant to be used as a starting place for those looking to get their feet wet with Hyperf Framework.
+
+# Requirements
+
+Hyperf has some requirements for the system environment, it can only run under Linux and Mac environment, but due to the development of Docker virtualization technology, Docker for Windows can also be used as the running environment under Windows.
+
+The various versions of Dockerfile have been prepared for you in the [hyperf/hyperf-docker](https://github.com/hyperf/hyperf-docker) project, or directly based on the already built [hyperf/hyperf](https://hub.docker.com/r/hyperf/hyperf) Image to run.
+
+When you don't want to use Docker as the basis for your running environment, you need to make sure that your operating environment meets the following requirements:  
+
+ - PHP >= 8.1
+ - Any of the following network engines
+   - Swoole PHP extension >= 5.0,with `swoole.use_shortname` set to `Off` in your `php.ini`
+   - Swow PHP extension >= 1.3
+ - JSON PHP extension
+ - Pcntl PHP extension
+ - OpenSSL PHP extension (If you need to use the HTTPS)
+ - PDO PHP extension (If you need to use the MySQL Client)
+ - Redis PHP extension (If you need to use the Redis Client)
+ - Protobuf PHP extension (If you need to use the gRPC Server or Client)
+
+# Installation using Composer
+
+The easiest way to create a new Hyperf project is to use [Composer](https://getcomposer.org/). If you don't have it already installed, then please install as per [the documentation](https://getcomposer.org/download/).
+
+To create your new Hyperf project:
+
+```bash
+composer create-project hyperf/hyperf-skeleton path/to/install
+```
+
+If your development environment is based on Docker you can use the official Composer image to create a new Hyperf project:
+
+```bash
+docker run --rm -it -v $(pwd):/app composer create-project --ignore-platform-reqs hyperf/hyperf-skeleton path/to/install
+```
+
+# Getting started
+
+Once installed, you can run the server immediately using the command below.
+
+```bash
+cd path/to/install
+php bin/hyperf.php start
+```
+
+Or if in a Docker based environment you can use the `docker-compose.yml` provided by the template:
+
+```bash
+cd path/to/install
+docker-compose up
+```
+
+This will start the cli-server on port `9501`, and bind it to all network interfaces. You can then visit the site at `http://localhost:9501/` which will bring up Hyperf default home page.
+
+## Hints
+
+- A nice tip is to rename `hyperf-skeleton` of files like `composer.json` and `docker-compose.yml` to your actual project name.
+- Take a look at `config/routes.php` and `app/Controller/IndexController.php` to see an example of a HTTP entrypoint.
+
+**Remember:** you can always replace the contents of this README.md file to something that fits your project description.

+ 30 - 0
app/Controller/AbstractController.php

@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+
+namespace App\Controller;
+
+use Hyperf\Di\Annotation\Inject;
+use Hyperf\HttpServer\Contract\RequestInterface;
+use Hyperf\HttpServer\Contract\ResponseInterface;
+use Psr\Container\ContainerInterface;
+
+abstract class AbstractController
+{
+    #[Inject]
+    protected ContainerInterface $container;
+
+    #[Inject]
+    protected RequestInterface $request;
+
+    #[Inject]
+    protected ResponseInterface $response;
+}

+ 57 - 0
app/Controller/Api/Notify/PaymentController.php

@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controller\Api\Notify;
+
+use App\Master\Framework\Library\Easywechat\PayService;
+use App\Utils\LogUtil;
+use Hyperf\HttpMessage\Stream\SwooleStream;
+use Hyperf\HttpServer\Contract\ResponseInterface;
+
+class PaymentController
+{
+    // 日志模块名称
+    const LOG_MODULE = 'Notify/PaymentController';
+
+    /**
+     * 微信支付回调
+     *
+     * @param ResponseInterface $response
+     * @return \Psr\Http\Message\MessageInterface|\Psr\Http\Message\ResponseInterface
+     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
+     * @throws \ReflectionException
+     * @throws \Throwable
+     */
+    public function wechat(ResponseInterface $response)
+    {
+        LogUtil::info("=== 支付回调开始 ===", self::LOG_MODULE, __FUNCTION__);
+
+        $pay = new PayService();
+        $server = $pay->getServer();
+        $server->handlePaid(function ($message){
+            // $message 为微信推送的通知结果,详看微信官方文档
+            LogUtil::info("通知结果", self::LOG_MODULE, 'wechat',$message);
+
+            // 微信支付订单号 $message['transaction_id']
+            // 商户订单号 $message['out_trade_no']
+            // 商户号 $message['mchid']
+            // 具体看微信官方文档...
+            // 进行业务处理,如存数据库等...
+        });
+
+
+        // 处理结果通知
+        try {
+            LogUtil::info("=== 支付回调结束 ===", self::LOG_MODULE, __FUNCTION__);
+            return $server->serve();
+        } catch (\Exception $exception){
+            LogUtil::info("=== 支付回调结束 ===", self::LOG_MODULE, __FUNCTION__,$exception);
+            // 抛出异常
+            return $response->withStatus(500)->withBody(new SwooleStream(json_encode([
+                'code' => 'FAIL',
+                'message' => '失败'
+            ])));
+        }
+    }
+}

+ 337 - 0
app/Controller/Api/v1/CommonController.php

@@ -0,0 +1,337 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controller\Api\v1;
+
+use App\Controller\AbstractController;
+use App\Master\Enum\RedisKeyEnum;
+use App\Master\Framework\Library\Google\Maps;
+use App\Master\Framework\Library\Tencent\TencentIm;
+use App\Model\Arts\AgreementModel;
+use App\Model\Arts\CarSeatModel;
+use App\Model\Arts\MessageModel;
+use App\Model\Arts\MobileAreaCodeModel;
+use App\Model\Arts\QuestionAnswerModel;
+use App\Model\Arts\QuestionReasonModel;
+use App\Model\Arts\RaiseAmountModel;
+use App\Model\Arts\UserMoneyLogModel;
+use App\Model\Arts\VersionAppModel;
+use App\Request\Api\v1\Common\AgreementRequest;
+use App\Request\Api\v1\Common\DirectionsRequest;
+use App\Request\Api\v1\Common\GeocodeRequest;
+use App\Request\Api\v1\Common\MessageRequest;
+use App\Request\Api\v1\Common\PlaceAutoSearchRequest;
+use App\Request\Api\v1\Common\PlaceDetailsRequest;
+use App\Request\Api\v1\Common\PlaceSearchRequest;
+use App\Request\Api\v1\Common\VersionRequest;
+use App\Request\Api\v1\User\MoneyLogRequest;
+use App\Utils\AppResult;
+use App\Utils\Control\AuthUser;
+use App\Utils\LogUtil;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+/**
+ * 公共管理
+ * CommonController
+ */
+class CommonController extends AbstractController
+{
+    // 日志模块名称
+    const LOG_MODULE = 'v1/CommonController';
+
+    public function config()
+    {
+        return AppResult::success('success', [
+            'is_open_tailwind' => (int)site('is_open_tailwind'),
+            'is_open_userapp' => (int)site('is_open_userapp'),
+        ]);
+    }
+
+    /**
+     * 手机号国际区号
+     * @return string
+     */
+    public function mobile_area_code()
+    {
+        $model         = new MobileAreaCodeModel();
+        $model->select = ['id', 'area_code', 'area'];
+        $list          = $model->getList(
+            orderBy: ['weigh' => 'desc', 'id' => 'desc']
+        );
+        return AppResult::success('success', $list);
+    }
+
+    /**
+     * 客服中心
+     * @return string
+     */
+    public function customer_service()
+    {
+        // 问答
+        $model         = new QuestionAnswerModel();
+        $model->select = ['id', 'question', 'answer'];
+        $list          = $model->getList(
+            params : ['type' => 1],
+            orderBy: ['weigh' => 'desc', 'id' => 'desc']
+        );
+        // 客服二维码
+        return AppResult::success('success', [
+            'service_qr_code'   => cdn_url(site('service_qr_code')),
+            'service_telephone' => site('service_telephone'),
+            'question_answer'   => $list,
+        ]);
+    }
+
+    // 生活服务页面
+    public function life_service()
+    {
+        $life_banner = site('life_banner');
+        foreach ($life_banner as &$item){
+            $item = cdn_url($item);
+        }
+        // 客服二维码
+        return AppResult::success('success', [
+            'life_banner'     => $life_banner,
+            'life_wechat'     => cdn_url(site('life_wechat')),
+            'service_qr_code' => cdn_url(site('service_qr_code'))
+        ]);
+    }
+
+    // 下载页面二维码
+    public function down_qr_code()
+    {
+        // 客服二维码
+        return AppResult::success('success', [
+            'down_user_app'   => cdn_url(site('down_user_app')),
+            'down_driver_app' => cdn_url(site('down_driver_app')),
+            'down_bg_img'     => cdn_url(site('down_bg_img'))
+        ]);
+    }
+
+    /**
+     * 人数和行行李数
+     * @return string
+     */
+    public function people_luggage()
+    {
+        // 最大人数和最大行李数
+        $model          = new CarSeatModel();
+        $info           = $model->query()->select([
+            Db::raw('max(people_num) as people_num'),
+            Db::raw('max(luggage_num) as luggage_num'),
+        ])->first();
+        $people_num     = $info['people_num'] ?? 1;// 最大人数
+        $luggage_num    = $info['luggage_num'] ?? 0;// 最大行李数
+        $list['people'] = [];
+        for ($i = 1; $i <= $people_num; $i++) {
+            $list['people'][] = [
+                'label' => "{$i}人",
+                'value' => $i
+            ];
+        }
+        $list['luggage'] = [];
+        for ($i = 0; $i <= $luggage_num; $i++) {
+            $list['luggage'][] = [
+                'label' => "{$i}托运行李",
+                'value' => $i
+            ];
+        }
+        return AppResult::success('success', $list);
+    }
+
+    /**
+     * 加价金额
+     * @return string
+     */
+    public function raise_amount()
+    {
+        // 最大人数和最大行李数
+        $model = new RaiseAmountModel();
+        $model->setSelect(['id', 'price']);
+        $list = $model->getList(orderBy: ['weight' => 'desc', 'id' => 'desc']);
+        return AppResult::success('success', $list);
+    }
+
+    /**
+     * 谷歌地址搜索
+     * @param PlaceSearchRequest $request
+     * @return string
+     */
+    public function place_search(PlaceSearchRequest $request)
+    {
+        $params    = $request->validated();
+        $googleMap = new Maps();
+        if (!$googleMap->place_search($params['input'])) {
+            return AppResult::error($googleMap->getMessage());
+        }
+        return AppResult::success('获取成功', $googleMap->getData());
+    }
+
+    /**
+     * 谷歌地址搜索(自动补全)
+     * @param PlaceAutoSearchRequest $request
+     * @return string
+     */
+    public function place_auto_search(PlaceAutoSearchRequest $request)
+    {
+        $params    = $request->validated();
+        $googleMap = new Maps();
+        if (!$googleMap->place_auto_search($params['input'],$params['latitude'] ?? '',$params['longitude'] ?? '',$params['sessionToken'] ?? '')) {
+            return AppResult::error($googleMap->getMessage());
+        }
+        return AppResult::success('获取成功', $googleMap->getData());
+    }
+
+    /**
+     * 谷歌地址详情
+     * @param PlaceDetailsRequest $request
+     * @return string
+     */
+    public function place_details(PlaceDetailsRequest $request)
+    {
+        $params    = $request->validated();
+        $googleMap = new Maps();
+        if (!$googleMap->place_details($params['place_id'])) {
+            return AppResult::error($googleMap->getMessage());
+        }
+        return AppResult::success('获取成功', $googleMap->getData());
+    }
+
+    /**
+     * 谷歌逆地理编码
+     * @param GeocodeRequest $request
+     * @return string
+     */
+    public function geocode(GeocodeRequest $request)
+    {
+        $params    = $request->validated();
+        $googleMap = new Maps();
+        if (!$googleMap->geocode($params['latlng'])) {
+            return AppResult::error($googleMap->getMessage());
+        }
+        return AppResult::success('获取成功', $googleMap->getData());
+    }
+
+    /**
+     * 谷歌路线规划
+     * @param DirectionsRequest $request
+     * @return string
+     */
+    public function directions(DirectionsRequest $request)
+    {
+        $params    = $request->validated();
+        $googleMap = new Maps();
+        if (!$googleMap->directions($params['destination'], $params['origin'], $params['waypoints'] ?? '')) {
+            return AppResult::error($googleMap->getMessage());
+        }
+        return AppResult::success('获取成功', $googleMap->getData());
+    }
+
+    // 系统消息数量
+    public function message_num()
+    {
+        $user = AuthUser::getInstance()->get();
+        $model   = new MessageModel();
+        $message = $model->getDetail(
+            params : ['user_id' => $user['id']],
+            orderBy: ['create_time' => 'desc']
+        );
+        if ($driver_chat_id = RedisUtil::getInstance(RedisKeyEnum::USER_DRIVER_CHAT_DEL,"user_{$user['id']}")->get()){
+            RedisUtil::getInstance(RedisKeyEnum::USER_DRIVER_CHAT_DEL,"user_{$user['id']}")->del();
+            $tencent = new TencentIm();
+            $tencent->delete_chat("user_{$user['id']}",$driver_chat_id);
+        }
+        LogUtil::warning('系统错误', self::LOG_MODULE, __FUNCTION__, [
+            'message_num' => MessageModel::getNumByUserId($user['id']),
+            'name'       => $message['name'] ?? '暂无新的消息',
+            'create_time' => !empty($message['create_time']) ? unix_time(strtotime($message['create_time'])) : '',
+            'driver_chat_id' => $driver_chat_id ?: '',
+        ]);
+        return AppResult::success(result: [
+            'message_num' => MessageModel::getNumByUserId($user['id']),
+            'name'       => $message['name'] ?? '暂无新的消息',
+            'create_time' => !empty($message['create_time']) ? unix_time(strtotime($message['create_time'])) : '',
+            'driver_chat_id' => $driver_chat_id ?: '',
+        ]);
+    }
+
+    /**
+     * 系统消息列表
+     * @param MessageRequest $request
+     * @return \Psr\Http\Message\MessageInterface|\Psr\Http\Message\ResponseInterface
+     * @throws \Psr\Container\ContainerExceptionInterface
+     * @throws \Psr\Container\NotFoundExceptionInterface
+     */
+    public function message(MessageRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+        $model  = new MessageModel();
+        $list   = $model->setSelect(['id', 'user_id', 'name', 'type', 'content', 'create_time'])->getList(
+            params : array_merge($params, ['user_id' => $user['id']]),
+            orderBy: ['create_time' => 'desc'],
+            with   : ['reads'],
+        );
+
+        foreach ($list as $key => $val) {
+            $list[$key]['is_read'] = 0;
+            if (!empty($val['reads'])) {
+                $list[$key]['is_read'] = 1;
+            }
+            unset($list[$key]['reads']);
+        }
+
+        MessageModel::read($user['id']);
+
+        return AppResult::success(result: $list);
+    }
+
+    // 获取腾讯 userSig
+    public function get_user_sig()
+    {
+        $user = AuthUser::getInstance()->get();
+        return AppResult::success(result: [
+            'userSig' => (new TencentIm())->userSig("user_{$user['id']}")
+        ]);
+    }
+
+    /**
+     * 协议信息
+     * @param AgreementRequest $request
+     * @return \Psr\Http\Message\MessageInterface|\Psr\Http\Message\ResponseInterface
+     * @throws \Psr\Container\ContainerExceptionInterface
+     * @throws \Psr\Container\NotFoundExceptionInterface
+     */
+    public function agreement(AgreementRequest $request)
+    {
+        $params    = $request->validated();
+        $agreement = (new AgreementModel())->getDetail($params);
+        return AppResult::success(result: [
+            'name'    => $agreement['name'] ?? '',
+            'content' => $agreement['content'] ?? '',
+        ]);
+    }
+
+    public function reason()
+    {
+        // 问答
+        $model         = new QuestionReasonModel();
+        $model->select = ['reason'];
+        $list          = $model->getList(
+            orderBy: ['weigh' => 'desc', 'id' => 'desc']
+        );
+        // 客服二维码
+        return AppResult::success('success', $list);
+    }
+
+    // 升级app
+    public function version(VersionRequest $request)
+    {
+        $params = $request->validated();
+        $version = (new VersionAppModel())->getDetail(array_merge(['platform' => 1],$params));
+        $version['is_force'] = (bool)$version['is_force'];
+        return AppResult::success(result: $version);
+    }
+}

+ 174 - 0
app/Controller/Api/v1/DemoController.php

@@ -0,0 +1,174 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controller\Api\v1;
+
+use App\Controller\AbstractController;
+use App\Master\Enum\RedisKeyEnum;
+use App\Master\Framework\Extend\Module;
+use App\Master\Framework\Library\GeTui\Push;
+use App\Master\Framework\Library\Tencent\TencentIm;
+use App\Model\Arts\DemoModel;
+use App\Model\Arts\DriverModel;
+use App\Model\Arts\OrderModel;
+use App\Request\Api\v1\DemoIndexRequest;
+use App\Service\QueueService;
+use App\Utils\AppResult;
+use App\Utils\Control\AuthUser;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+use Hyperf\Di\Annotation\Inject;
+use Psr\Http\Message\ServerRequestInterface;
+use function Hyperf\Coroutine\co;
+
+/**
+ * Demo
+ * 示例
+ */
+class DemoController extends AbstractController
+{
+    // 日志模块名称
+    const LOG_MODULE = 'v1/DemoController';
+
+    #[Inject]
+    protected QueueService $service;
+
+    /**
+     * 示例接口
+     *
+     * @param DemoIndexRequest $request 校验参数注入类
+     * @return string
+     */
+    public function index(DemoIndexRequest $request)
+    {
+        // 【快车】早高峰时间
+//        $taxi_morning_peak_time = explode('-', site('taxi_morning_peak_time'));
+//        return AppResult::response200('Coming Soon!!!', [
+//            $taxi_morning_peak_time,
+//            strtotime($taxi_morning_peak_time[0]),
+//            date('Y-m-d H:i:s', strtotime($taxi_morning_peak_time[0])),
+//            date('Y-m-d H:i:s', strtotime($taxi_morning_peak_time[1])),
+//            strtotime('2024-08-31 12:00:00'),
+//            date('Y-m-d H:i:s', strtotime('2024-08-31 12:00:00')),
+//            date('H:i', strtotime('2024-08-31 12:00:00')),
+//            strtotime(date('H:i', strtotime('2024-08-31 12:00:00'))),
+//            date('Y-m-d H:i:s', strtotime(date('H:i', strtotime('2024-08-31 12:00:00')))),
+//        ]);
+//        return AppResult::response200('Coming Soon!!!');
+//        $tencent = new TencentIm();
+//        $tencent->delete_chat("user_5",'driver_6');
+//        RedisUtil::getInstance(RedisKeyEnum::USER_DRIVER_CHAT_DEL,"user_5")->setex("driver_6",60);
+//        return AppResult::response200('Coming Soon!!!',$tencent->getData());
+//        // 删除司机用户聊天框
+//        RedisUtil::getInstance(RedisKeyEnum::USER_DRIVER_CHAT_DEL,"user_3")->setex("driver_1",10);
+//        return AppResult::response200('Coming Soon!!!');
+        $params = $request->validated();// 获取校验参数结果
+//        $data = [
+//            'push_cid'     => $params['push_cid'],
+//            'push_title'   => '好滴用车来新订单了',
+//            'push_content' => "订单距您5km,立即前往接单!",
+//            'ring_name'    => $params['ring_name'],//'ringing.mp3',
+//            'type'         => 1,
+//            'platform'     => $params['platform'],
+//        ];
+//        $this->service->pushGeTui($data);
+////        $geTui = new Push();
+////        $geTui->push($data['push_cid'], $data['push_title'], $data['push_content'], $data['ring_name'], $data['type'], $data['platform']);
+//        return AppResult::response200('Coming Soon!!!');
+        $model = new OrderModel();
+        $order = $model->getDetail(['order_no' => $params['order_no']]);
+        $list = DriverModel::query()
+            ->select([
+                'id',
+                'push_cid',
+                'platform',
+                'lng',
+                Db::raw("(st_distance(point ({$order['start_lng']}, {$order['start_lat']}),point(lng,lat))*111195) as distance")
+            ])
+            ->with(['car'])
+            ->where('status', 1)
+            ->where('is_online', 1)
+            ->where('push_cid', '!=', '')
+            //->havingRaw('distance < ?', [10000])
+            ->get();
+        $list = json_decode(json_encode($list), true);
+//        $count = 0;
+        if (!empty($list)) {
+            foreach ($list as $item) {
+                if (!empty($item['push_cid'])) {
+                    // 司机距离
+                    $distance = 0;
+                    if (isset($item['distance'])) {
+                        if ($item['distance'] >= 1000) {
+                            $distance = round(($item['distance'] / 1000), 2) . 'km';
+                        } else {
+                            $distance = round($item['distance'], 1) . 'm';
+                        }
+                    }
+                    dd("订单距您{$distance},立即前往接单!");
+//                    $this->service->pushGeTui([
+//                        'push_cid'     => $item['push_cid'],
+//                        'push_title'   => '好滴用车来新订单了',
+//                        'push_content' => "订单距您{$distance},立即前往接单!",
+//                        'ring_name'    => $order['appointment_time'] > 0 ? 'ringing1.mp3': 'ringing.mp3',
+//                        'type'         => 1,
+//                        'platform'     => $item['platform'],
+//                    ]);
+//                    $count += 1;
+                }
+            }
+        }
+        return AppResult::response200('Coming Soon!!!', [
+            $order,$list
+        ]);
+
+        /**
+         * 当处理HTTP请求时,无论是通过路径参数、查询参数还是请求体传递的参数,Hyperf都会将其作为字符串类型返回。
+         * 这是因为HTTP请求中的参数本质上就是字符串,即使它们代表的是其他数据类型(如整数、布尔值等)。
+         * 注意:所有的参数(除 Content-type:application/json 外)类型都是字符串 如有需要 则可强转后使用
+         * 例如,如果期望得到一个整数值,可以使用 intval() 函数将字符串转换为整数。同样,对于布尔值,可以使用 boolval() 函数。
+         *
+         * POST 建议使用 Content-type:application/json
+         */
+        $params = $request->validated();// 获取校验参数结果
+
+        /**
+         * 获取用户信息
+         */
+        $user = AuthUser::getInstance()->get();
+
+        $model = new DemoModel();
+        $list  = $model->getList($params);
+
+        $setup = Module::_SetupModule('siteBase');
+
+        // 测试投递异步队列消息
+        $this->service->demoPush(['name' => 'one'], 10);
+        $this->service->demoTwoPush(['name' => 'two']);
+
+        // 携程 闭包
+        co(function () use ($params){
+            sleep(10);
+        });
+
+        return AppResult::response200('Coming Soon!!!', [
+            'params'       => $params,
+            'list'         => $list,
+            'module_setup' => $setup,
+        ]);
+    }
+
+    /**
+     * 原始示例
+     * @param ServerRequestInterface $request
+     * @return string
+     */
+    public function demo01(ServerRequestInterface $request)
+    {
+        return AppResult::response200('Coming Soon!!!', [
+            // 获取请求参数方式
+            'params' => $request->getQueryParams()
+        ]);
+    }
+}

+ 122 - 0
app/Controller/Api/v1/HomeController.php

@@ -0,0 +1,122 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controller\Api\v1;
+
+use App\Controller\AbstractController;
+use App\Model\Arts\CouponModel;
+use App\Model\Arts\UserAddressModel;
+use App\Model\Arts\UserCouponModel;
+use App\Model\Arts\UserModel;
+use App\Model\Arts\UserMoneyLogModel;
+use App\Model\Arts\UserWalletModel;
+use App\Request\Api\v1\User\AddressAddRequest;
+use App\Request\Api\v1\User\AddressDelRequest;
+use App\Request\Api\v1\User\AddressDetailRequest;
+use App\Request\Api\v1\User\AddressEditRequest;
+use App\Request\Api\v1\User\AddressListRequest;
+use App\Request\Api\v1\User\MoneyLogRequest;
+use App\Utils\AppResult;
+use App\Utils\Common;
+use App\Utils\Control\AuthUser;
+
+/**
+ * 首页
+ * UserController
+ */
+class HomeController extends AbstractController
+{
+    // 日志模块名称
+    const LOG_MODULE = 'v1/HomeController';
+
+    public function index()
+    {
+        $home_notice = site('home_notice');
+        $home_notice = explode("\n", $home_notice);
+        return AppResult::success('success', [
+            'home_notice'   => $home_notice,
+            'home_invite_banner' => cdn_url(site('home_invite_banner'))
+        ]);
+    }
+
+    /**
+     * 邀请页面
+     * @return \Psr\Http\Message\MessageInterface|\Psr\Http\Message\ResponseInterface
+     * @throws \Psr\Container\ContainerExceptionInterface
+     * @throws \Psr\Container\NotFoundExceptionInterface
+     */
+    public function invite()
+    {
+        $user = AuthUser::getInstance()->get();
+        if (empty($user['invite_code'])) {
+            $user['invite_code'] = 'U' . Common::createRandomKeys($user['id']);
+            UserModel::query()->where('id', $user['id'])->update(['invite_code' => $user['invite_code']]);
+        }
+
+        // 优惠券数量
+        $list  = (new UserCouponModel())->getList(
+            params : [
+                'user_id'       => $user['id'],
+                'is_use'        => 0,
+                'valid'         => 0
+            ],
+            orderBy: ['id' => 'desc']
+        );
+        $user_coupon_ids = array_column($list, 'coupon_id');
+        $coupon_num      = count($user_coupon_ids);
+        // 邀请人数
+        $invite_people_num = UserModel::query()->where('parent_id', $user['id'])->where('status', 1)->count();
+        // 待领取优惠券
+        // 未领取的优惠券
+        $new_coupons = (new CouponModel())->query()->whereNotIn('id', $user_coupon_ids)->where(['status' => 1])->get()->toArray();
+        return AppResult::success(result: [
+            'invite_code'       => $user['invite_code'],
+            'coupon_num'        => $coupon_num,
+            'invite_people_num' => $invite_people_num,
+            'coupons'           => $new_coupons,
+        ]);
+    }
+
+    /**
+     * 领取优惠券
+     * @return \Psr\Http\Message\MessageInterface|\Psr\Http\Message\ResponseInterface
+     * @throws \Psr\Container\ContainerExceptionInterface
+     * @throws \Psr\Container\NotFoundExceptionInterface
+     */
+    public function get_coupon()
+    {
+        $user = AuthUser::getInstance()->get();
+        // 已领取的优惠券
+        $user_coupon_ids = UserCouponModel::query()->where(['user_id' => $user['id']])->pluck('coupon_id')->toArray();
+        // 未领取的优惠券
+        $new_coupons = CouponModel::query()->whereNotIn('id', $user_coupon_ids)->where(['status' => 1])->get()->toArray();
+        if (!$new_coupons) {
+            return AppResult::success('暂无新的优惠券');
+        }
+
+        $time   = time();
+        $insert = [];
+        foreach ($new_coupons as $key => $item) {
+            $insert[] = [
+                'user_id'     => $user['id'],
+                'coupon_id'   => $item['id'],
+                'name'        => $item['name'],
+                'type'        => $item['type'],
+                'min_money'   => $item['min_money'],
+                'money'       => $item['money'],
+                'valid_at'    => strtotime(date('Y-m-d 23:59:59', $time + ($item['valid_days'] * 24 * 60 * 60))),// 自领取之日起 计算过期时间
+                'remark'      => $item['remark'],
+                'status'      => 1,
+                'create_time' => $time
+            ];
+        }
+        // 领取优惠券
+        if (count($insert) > 0) {
+            if (!UserCouponModel::query()->insert($insert)) {
+                return AppResult::error('领取失败');
+            }
+        }
+        return AppResult::success('领取成功');
+    }
+}

+ 184 - 0
app/Controller/Api/v1/LiveController.php

@@ -0,0 +1,184 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controller\Api\v1;
+
+use App\Controller\AbstractController;
+use App\Controller\Business\GiftBusiness;
+use App\Master\Framework\Library\Tencent\TencentIm;
+use App\Model\Arts\LiveGiftLogModel;
+use App\Model\Arts\LiveGiftModel;
+use App\Model\Arts\LiveRoomModel;
+use App\Model\Arts\UserModel;
+use App\Model\Arts\UserWalletModel;
+use App\Request\Api\v1\DemoIndexRequest;
+use App\Request\Api\v1\Live\GiveGiftsRequest;
+use App\Request\Api\v1\Live\LikeRequest;
+use App\Service\QueueService;
+use App\Utils\AppResult;
+use App\Utils\Control\AuthUser;
+use App\Utils\LogUtil;
+use Hyperf\DbConnection\Db;
+use Hyperf\Di\Annotation\Inject;
+
+/**
+ * Demo
+ * 示例
+ */
+class LiveController extends AbstractController
+{
+    // 日志模块名称
+    const LOG_MODULE = 'v1/LiveController';
+
+    #[Inject]
+    protected QueueService $service;
+
+    public function index()
+    {
+        return AppResult::response200('success',(new LiveGiftLogModel())->getPkTopList());
+    }
+
+    /**
+     * 直播间观众列表
+     * @param LikeRequest $request
+     * @return string
+     * @throws \Exception
+     */
+    public function audience(LikeRequest $request)
+    {
+        $params = $request->validated();// 获取校验参数结果
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        $room   = $model->getDetailOne((int)$params['room_no']);
+        if (!$room) {
+            return AppResult::error('直播间异常');
+        }
+        $model = new LiveGiftLogModel();
+        $list  = $model->getTopList($room);
+        return AppResult::success('success', $list);
+    }
+
+    /**
+     * 点赞
+     * @param LikeRequest $request
+     * @return string
+     */
+    public function like(LikeRequest $request)
+    {
+        $params = $request->validated();// 获取校验参数结果
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        $room   = $model->getDetailOne((int)$params['room_no']);
+        if (!$room) {
+            return AppResult::error('直播间异常');
+        }
+        // 不可以赠送给自己
+        if ($user['id'] == $room['user_id']) {
+            return AppResult::error('不可以给自己点赞');
+        }
+
+        // 加入点赞队列
+        $this->service->likePush([
+            'room_no' => $room['room_no'],
+            'session' => $room['session']
+        ]);
+
+        return AppResult::success('success');
+    }
+
+    /**
+     * 送礼物
+     * @param GiveGiftsRequest $request
+     * @return string
+     */
+    public function giveGifts(GiveGiftsRequest $request)
+    {
+        $params = $request->validated();// 获取校验参数结果
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        $room   = $model->getDetailOne((int)$params['room_no']);
+        if (!$room) {
+            return AppResult::error('直播间异常');
+        }
+        // 不可以赠送给自己
+        if ($user['id'] == $room['user_id']) {
+            return AppResult::error('不可以给自己送礼物');
+        }
+
+        // 获取礼物信息
+        if (!$giftInfo = (new LiveGiftModel())->getDetail(['id' => $params['gift_id']])) {
+            return AppResult::error('请选择礼物');
+        }
+
+        // 被赠送人信息
+        if (!$toUserInfo = (new UserModel())->getDetail(['id' => $room['user_id']])) {
+            return AppResult::error('不存在的用户');
+        }
+
+        // 礼物价值
+        $giftValue = bcmul((string)$giftInfo['value'], (string)$params['number']);
+
+        // 用户钱包
+        $wallet = new UserWalletModel();
+        if (!$wallet->getOne($user['id'])) {
+            return AppResult::error($wallet->getMessage());
+        }
+        $user_gold = $wallet->getData()['gold'] ?? 0;
+
+        if ($user_gold < $giftValue) {
+            return AppResult::error('您的金币余额不足');
+        }
+
+        Db::beginTransaction();
+        try {
+            // 添加礼物赠送记录表
+            $data = [
+                'user_id'     => $user['id'],
+                'user_to_id'  => $toUserInfo['id'],
+                'room_id'     => $room['id'],
+                'room_no'     => $room['room_no'],
+                'session'     => $room['session'],
+                'gift_id'     => $giftInfo['id'],
+                'gift_name'   => $giftInfo['name'],
+                'number'      => $params['number'],
+                'price'       => $giftValue,
+                'is_pk'       => $room['is_pk'],
+                'pk_id'       => $room['pk_id'],
+                'pk_type'     => $room['pk_type'],
+                'create_time' => time()
+            ];
+            if (!$log_id = (new LiveGiftLogModel())->insertGetId($data)) {
+                Db::rollback();
+                return AppResult::error('赠送失败');
+            }
+
+            if ($giftValue > 0) {
+                // 扣除当前用户余额
+                if (!$wallet->change($user['id'], $toUserInfo['id'], 'gold', -$giftValue, 53, "赠送礼物:{$giftInfo['name']}*{$params['number']}", 'gift_user_typing', $log_id)) {
+                    Db::rollBack();
+                    return AppResult::error($wallet->getMessage());
+                }
+//                $business = new GiftBusiness();
+//                if (!$business->LiveGiftDeal($log_id)){
+//                    LogUtil::info('同步执行成交收益', self::LOG_MODULE, __FUNCTION__,$business->getMessage());
+//                    Db::rollBack();
+//                    return AppResult::error($business->getMessage());
+//                }
+            }
+            Db::commit();
+        } catch (\Throwable $e) {
+            Db::rollBack();
+            return AppResult::error($e->getMessage());
+        }
+
+        if ($giftValue > 0) {
+            // 队列中处理:添加礼物赠送记录;添加pk数据;添加赠送用户余额;增加赠送用户上级余额;增加亲密度
+            $this->service->liveGiftPush([
+                'log_id' => $log_id
+            ]);
+        }
+
+        return AppResult::success('赠送成功');
+    }
+}

+ 660 - 0
app/Controller/Api/v1/OrderController.php

@@ -0,0 +1,660 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controller\Api\v1;
+
+use App\Controller\AbstractController;
+use App\Controller\Business\DistanceBusiness;
+use App\Controller\Business\DriverTypeBusiness;
+use App\Master\Framework\Library\GeTui\Push;
+use App\Model\Arts\CarSeatModel;
+use App\Model\Arts\DriverCarModel;
+use App\Model\Arts\DriverMessageModel;
+use App\Model\Arts\OrderDriverModel;
+use App\Model\Arts\OrderModel;
+use App\Model\Arts\UserCouponModel;
+use App\Model\Arts\UserModel;
+use App\Model\Arts\UserWalletModel;
+use App\Request\Api\v1\Order\AddMoneyRequest;
+use App\Request\Api\v1\Order\CancelRequest;
+use App\Request\Api\v1\Order\DetailRequest;
+use App\Request\Api\v1\Order\ListRequest;
+use App\Request\Api\v1\Order\PageRequest;
+use App\Request\Api\v1\Order\SubmitRequest;
+use App\Request\Api\v1\Order\TailwindDriverInfoRequest;
+use App\Request\Api\v1\Order\TailwindDriverRequest;
+use App\Request\Api\v1\Order\TailwindInviteRequest;
+use App\Service\QueueService;
+use App\Utils\AppResult;
+use App\Utils\Control\AuthUser;
+use Hyperf\DbConnection\Db;
+use Hyperf\Di\Annotation\Inject;
+
+/**
+ * 用户管理
+ * UserController
+ */
+class OrderController extends AbstractController
+{
+    // 日志模块名称
+    const LOG_MODULE = 'v1/OrderController';
+
+    #[Inject]
+    protected QueueService $service;
+
+    /**
+     * 预下单(快车 接送机)
+     * @param PageRequest $request
+     * @return string
+     */
+    public function page(PageRequest $request)
+    {
+        $params = $request->validated();// 获取校验通过的参数
+        $user   = AuthUser::getInstance()->get();
+
+        $point            = $params['point'];
+        $people_num       = !empty($params['people_num']) ? $params['people_num'] : 1;
+        $luggage_num      = !empty($params['luggage_num']) ? $params['luggage_num'] : 0;
+        $is_back          = !empty($params['is_back']) ? $params['is_back'] : 0;
+        $is_exclusive     = !empty($params['is_exclusive']) ? $params['is_exclusive'] : 0;
+        $appointment_time = !empty($params['appointment_time']) ? strtotime($params['appointment_time']) : 0;
+        $appointment_time = $params['type'] == 30 && empty($appointment_time) ? time() : $appointment_time;// 如果是顺风车 且未选择时间,则默认为当前时间
+
+        // 计算公里数
+        $distanceBusiness = new DistanceBusiness();
+        if (!$distanceBusiness->computeRoutes($point, $appointment_time)) {
+            return AppResult::error($distanceBusiness->getMessage() ?? '计算距离错误');
+        }
+        $mapData = $distanceBusiness->getData();
+        // 获取计算距离
+        $distance = $mapData['distance'];// 距离m
+        $duration = $mapData['duration'];// 时长s
+
+        // 计算对应订单类型的价格
+        $driverTypeBusiness = new DriverTypeBusiness();
+        if (!$driverTypeBusiness->calculate((int)$params['type'], $distance, $duration, $point, $appointment_time, $is_back, $is_exclusive)) {
+            return AppResult::error($driverTypeBusiness->getMessage() ?? '计算失败');
+        }
+        $res = $driverTypeBusiness->getData();
+
+        if ($params['type'] == 50) {
+            $total_amount = (string)round((float)$res['total_amount']);// 四舍五入
+            $rmb_total_amount = (string)round((float)bcmul($total_amount, site('dollar_to_rmb'), 2));// 四舍五入
+            // 跑腿订单
+            $data = [
+                'errand'  => [
+                    'total_amount' => $total_amount,
+                    'rmb_total_amount' => $rmb_total_amount
+                ]
+            ];
+        } else {
+            $airport_fee_min_price = site('airport_fee_min_price');// 接送机最低消费
+            $carSeat = new CarSeatModel();
+            $list    = $carSeat->getList(orderBy: ['weight' => 'desc']);
+            foreach ($list as $key => $val) {
+                $basics_amount = bcsub(bcmul((string)$res['basics_amount'], (string)$val['subsidy_multiple'], 2),(string)$res['basics_amount'],2);// 计算车型补贴:公里数价格 * 补贴比例
+                $total_amount = bcadd((string)$res['total_amount'], $basics_amount, 2);
+                // 预约费
+                $platform_subscribe_price = (string)($total_amount >= site('platform_subscribe_min') ? $val['platform_subscribe_price'] : '0');
+                $total_amount = (string)round((float)bcadd($total_amount,$platform_subscribe_price,2));// 美元 四舍五入
+                if ($params['type'] == 20 || ($params['type'] == 10 && $res['is_at_airport'] == 1)){
+                    $total_amount = max($total_amount,$airport_fee_min_price);// 接送机最低消费
+                }
+
+                $rmb_total_amount = (string)round((float)bcmul($total_amount, site('dollar_to_rmb'), 2));// 人民币 四舍五入
+                $list[$key]['image']        = cdn_url($val['image']);
+                $list[$key]['total_amount'] = $total_amount;
+                $list[$key]['rmb_total_amount'] = $rmb_total_amount;
+                $list[$key]['is_show']      = $val['people_num'] >= $people_num && $val['luggage_num'] >= $luggage_num ? 1 : 0;
+            }
+
+            // 往返提示说明
+            $come_explain = '';
+            if ($is_back == 1){
+                $come_explain = $params['type'] == 10 ? site('wf_taxi_explain') : site('wf_airport_explain');
+            }
+
+            $data = [
+                'distance' => bcdiv((string)$distance,'1000'),
+                'come_explain'  => $come_explain,
+                'car_seat_list' => $list,
+                'tailwind'      => [
+                    [
+                        'image'        => cdn_url('/assets/img/shun_che1.png'),
+                        'total_amount' => (string)round((float)$res['total_amount']),
+                        'rmb_total_amount' => (string)round((float)bcmul($res['total_amount'], site('dollar_to_rmb'), 2)),
+                        'name'         => '拼车',
+                        'is_exclusive' => 0,
+                    ],
+                    [
+                        'image'        => cdn_url('/assets/img/shun_che2.png'),
+                        'total_amount' => (string)round((float)bcadd($res['total_amount'], site('tailwind_exclusive'), 2)),
+                        'rmb_total_amount' => (string)round((float)bcmul(bcadd($res['total_amount'], site('tailwind_exclusive'), 2),site('dollar_to_rmb'), 2)),
+                        'name'         => '独享',
+                        'is_exclusive' => 1,
+                    ]
+                ],
+                'substitute'    => [
+                    'total_amount' => (string)round((float)$res['total_amount']),
+                    'rmb_total_amount' => (string)round((float)bcmul($res['total_amount'], site('dollar_to_rmb'), 2)),
+                    'image'        => cdn_url('/assets/img/dai_toup1.png'),
+                ]
+            ];
+        }
+
+        // 汇率
+        $data['dollar_to_rmb'] = site('dollar_to_rmb');
+
+        if ($appointment_time == 0){
+            // 如果有实时订单则提示是否继续接单
+            $model = new OrderModel();
+            if ($model->getDetail(params: ['user_id' => $user['id'],'status_in' => [10,20,30],'appointment_time_zero' => 0])){
+                return AppResult::response_fast(2,"您有一条正在进行订单,确定要继续下单吗?", $data);
+            }
+        }
+
+        return AppResult::success('success', $data);
+    }
+
+    /**
+     * 提交订单(快车 接送机)
+     * @param SubmitRequest $request
+     * @return string
+     */
+    public function submit(SubmitRequest $request)
+    {
+        $params = $request->validated();// 获取校验通过的参数
+        $user   = AuthUser::getInstance()->get();
+
+        // 如果是会员 则可设置一口价
+        if ($user['is_vip'] != 1 && !empty($params['pay_amount'])){
+            return AppResult::error('暂无设置一口价权限');
+        }
+        if ($user['is_vip'] == 1 && !empty($params['pay_amount']) && !in_array($params['type'], [10, 20])){
+            return AppResult::error('仅支持快车或接送机设置一口价');
+        }
+
+        $point            = $params['point'];
+        $people_num       = !empty($params['people_num']) ? $params['people_num'] : 1;// 乘车人数
+        $luggage_num      = !empty($params['luggage_num']) ? $params['luggage_num'] : 0;// 行李数
+        $is_back          = !empty($params['is_back']) ? $params['is_back'] : 0;// 是否往返
+        $is_exclusive     = !empty($params['is_exclusive']) ? $params['is_exclusive'] : 0;// 是否独享
+        $appointment_time = !empty($params['appointment_time']) ? strtotime($params['appointment_time']) : 0;// 出发时间
+        $appointment_time = $params['type'] == 30 && empty($appointment_time) ? time() : $appointment_time;// 如果是顺风车 且未选择时间,则默认为当前时间
+
+        // 计算公里数
+        $distanceBusiness = new DistanceBusiness();
+        if (!$distanceBusiness->computeRoutes($point, $appointment_time)) {
+            return AppResult::error($distanceBusiness->getMessage() ?? '计算距离错误');
+        }
+        $mapData = $distanceBusiness->getData();
+        // 获取计算距离
+        $distance = $mapData['distance'];// 距离m
+        $duration = $mapData['duration'];// 时长s
+
+        // 如果是会员 且是快车 或 接送机 则可设置一口价
+        if ($user['is_vip'] == 1 && !empty($params['pay_amount']) && in_array($params['type'], [10, 20])){
+            $carSeat = new CarSeatModel();
+            $info    = $carSeat->getDetail(params: ['id' => $params['car_seat_id']]);
+            if (!($info['people_num'] >= $people_num && $info['luggage_num'] >= $luggage_num)) {
+                return AppResult::error('当前车型不支持选择的人数行李数');
+            }
+            $pay_amount = $total_amount = $params['pay_amount'];
+            // 预约费 vip预约费已经包含在
+            $platform_subscribe_price = (string)($total_amount >= site('platform_subscribe_min') ? $info['platform_subscribe_price'] : '0');
+        }else{
+            // 计算对应订单类型的价格
+            $driverTypeBusiness = new DriverTypeBusiness();
+            if (!$driverTypeBusiness->calculate((int)$params['type'], $distance, $duration, $point, $appointment_time, $is_back, $is_exclusive)) {
+                return AppResult::error($driverTypeBusiness->getMessage() ?? '计算失败');
+            }
+            $res = $driverTypeBusiness->getData();
+
+            // 如果是接送机 快车 则根据车型计算价格
+            if (in_array($params['type'], [10, 20])) {
+                $carSeat = new CarSeatModel();
+                $info    = $carSeat->getDetail(params: ['id' => $params['car_seat_id']]);
+                if (!($info['people_num'] >= $people_num && $info['luggage_num'] >= $luggage_num)) {
+                    return AppResult::error('当前车型不支持选择的人数行李数');
+                }
+                $basics_amount = bcsub(bcmul((string)$res['basics_amount'], (string)$info['subsidy_multiple'], 2),(string)$res['basics_amount'],2);// 计算车型补贴:公里数价格 * 补贴比例
+                $total_amount = bcadd((string)$res['total_amount'], $basics_amount, 2);
+                // 预约费
+                $platform_subscribe_price = (string)($total_amount >= site('platform_subscribe_min') ? $info['platform_subscribe_price'] : '0');
+                $total_amount = bcadd($total_amount,$platform_subscribe_price,2);
+                if ($params['type'] == 20 || ($params['type'] == 10 && $res['is_at_airport'] == 1)){
+                    $airport_fee_min_price = site('airport_fee_min_price');// 接送机最低消费
+                    $total_amount = max($total_amount,$airport_fee_min_price);
+                }
+            }
+
+            // 如果是 代驾 顺风车 跑腿 则无需单独增加价格
+            if (in_array($params['type'], [30, 40, 50])) {
+                $total_amount = $res['total_amount'];
+            }
+
+            if (!isset($total_amount)) {
+                return AppResult::error('操作异常');
+            }
+
+            $pay_amount = $total_amount = (string)round((float)$total_amount);// 支付金额 四舍五入
+            // 优惠券核销
+            if (!empty($params['coupon_id'])){
+                $order_coupons = (new UserCouponModel())->getOrderCoupon($user['id'],(int)$params['type'],(int)$params['coupon_id'],$total_amount);
+                if ($total_amount >= $order_coupons['min_money']){
+                    $pay_amount = bcsub($total_amount,$order_coupons['money'],2);
+                    $discount_amount = $order_coupons['money'];
+                }
+            }
+        }
+
+        // 在线支付需校验金额
+        if ($params['pay_type'] == 1) {
+            $money = UserWalletModel::getOne($user['id'], 'money');
+            if ($money < $pay_amount) {
+                return AppResult::error('余额不足');
+            }
+        }
+
+        $data = [
+            'type'               => $params['type'],
+            'flight_type'        => $params['flight_type'] ?? 0,
+            'distance'           => (int)$distance,
+            'duration'           => (int)$duration,
+            'total_amount'       => $total_amount,
+            'dollar_to_rmb'       => site('dollar_to_rmb'),
+            'pay_amount'         => $pay_amount,
+            'discount_amount'    => $discount_amount ?? 0,
+            'pay_type'           => $params['pay_type'],
+            'is_pay'             => $params['pay_type'] == 1 ? 1 : 0,
+            'coupon_id'          => $params['coupon_id'] ?? 0,
+            'car_seat_id'        => $params['car_seat_id'] ?? 0,
+            'name'               => $params['name'] ?? $user['nickname'],
+            'phone'              => $params['phone'] ?? "{$user['area_code']} {$user['mobile']}",
+            'people_num'         => $people_num,
+            'luggage_num'        => $luggage_num,
+            'start_lng'          => $point[0]['lng'],
+            'start_lat'          => $point[0]['lat'],
+            'end_lng'            => end($point)['lng'],
+            'end_lat'            => end($point)['lat'],
+            'is_back'            => $is_back,
+            'is_exclusive'       => $is_exclusive,
+            'is_child'           => $params['is_child'] ?? 0,
+            'appointment_time'   => $appointment_time,
+            'flight_no'          => $params['flight_no'] ?? '',
+            'basics_amount'      => $res['basics_amount'] ?? '0.00',
+            'city_subsidy_price' => $res['city_subsidy_price'] ?? '0.00',
+            'peak_price'         => $res['peak_price'] ?? '0.00',
+            'night_price'        => $res['night_price'] ?? '0.00',
+            'exclusive_price'    => $res['exclusive_price'] ?? '0.00',
+            'platform_subscribe_price'    => $platform_subscribe_price ?? '0.00',
+            'remark'             => $params['remark'] ?? '',
+            'freight_name'       => $params['freight_name'] ?? '',
+            'image'              => $params['image'] ?? '',
+        ];
+
+        Db::beginTransaction();
+        // 在线支付需要提前扣费
+        if ($params['pay_type'] == 1) {
+            $type_name = DriverTypeBusiness::type[$params['type']] ?? '';
+            $wallet    = new UserWalletModel();
+            if (!$wallet->change($user['id'], (float)$data['pay_amount'], "{$type_name}订单消费")) {
+                Db::rollBack();
+                return AppResult::error($wallet->getMessage());
+            }
+        }
+        // 使用优惠券
+        if (!empty($params['coupon_id'])) {
+            $coupon = UserCouponModel::query()
+                ->where('id',$params['coupon_id'])
+                ->where('type',$params['type'])
+                ->where('is_use',0)
+                ->where('status',1)
+                ->where('valid_at', '>', time())
+                ->update(['is_use' => 1, 'use_at' => time()]);
+            if (!$coupon) {
+                Db::rollBack();
+                return AppResult::error('优惠券核销失败,请校验优惠券');
+            }
+        }
+        // 增加用户订单数
+        UserModel::query()->where('id', $user['id'])->increment('order_num');
+        // 创建订单
+        $model = new OrderModel();
+        if (!$model->createOrder($user['id'], $data, $point)) {
+            Db::rollBack();
+            return AppResult::error($model->getMessage());
+        }
+        Db::commit();
+
+        // 加入提醒司机接单队列
+        if (!empty($model->getData()['order_no'])){
+            $this->service->pushOrder([
+                'order_no' => $model->getData()['order_no']
+            ]);
+        }
+
+        // 加入超时订单取消队列
+        if (in_array($params['type'],[10,20,30,40,50])){
+            $delay = $appointment_time > 0 ? 600 : 300;// 延迟时间 单位秒
+            if ($params['type'] == 30){
+                $delay = $appointment_time - time();
+                $delay = max($delay, 600);
+            }
+            $this->service->cancelOrder([
+                'order_no' => $model->getData()['order_no']
+            ],$delay);
+        }
+
+        return AppResult::success($model->getMessage(), $model->getData());
+    }
+
+    /**
+     * 订单列表
+     * @param ListRequest $request
+     * @return string
+     */
+    public function list(ListRequest $request)
+    {
+        $params            = $request->validated();
+        $user              = AuthUser::getInstance()->get();
+        $params['user_id'] = $user['id'];
+
+        // 列表筛选
+        switch ($params['status_type']) {
+            case 1:
+                $params['status_in']            = [10, 20];
+                $params['appointment_time_not'] = 1;
+                break;
+            case 2:
+                $params['status_in'] = [10, 20, 30];
+                break;
+            case 3:
+                $params['status_in'] = [40];
+        }
+
+        $model = new OrderModel();
+        $model->setSelect([
+            'id', 'user_id', 'driver_id', 'type', 'order_no', 'driver_order_id', 'total_amount', 'pay_amount', 'appointment_time', 'create_time',
+            'driver_status', 'status'
+        ]);
+        $list = $model->getList(
+            params : $params,
+            orderBy: ['id' => 'desc'],
+            with   : [
+                'points' => function ($query) {
+                    $query->select('id', 'order_id', 'type', 'lng', 'lat', 'address', 'postal_code', 'is_in_city', 'is_at_airport')->where('status', 1)->orderBy('id', 'asc');
+                }
+            ]
+        );
+        foreach ($list as $key => $val) {
+            $list[$key]['start_time'] = date('Y-m-d H:i', !empty($val['appointment_time']) ? $val['appointment_time'] : $val['create_time']);
+            $list[$key]['create_time'] = strtotime($list[$key]['start_time']);
+        }
+        return AppResult::success(result: $list);
+    }
+
+    /**
+     * 订单详情
+     * @param DetailRequest $request
+     * @return string
+     */
+    public function detail(DetailRequest $request)
+    {
+        $params = $request->validated();// 获取校验通过的参数
+        $user   = AuthUser::getInstance()->get();
+        $model = new OrderModel();
+        $model->setSelect([
+            'id', 'user_id', 'driver_id', 'type', 'flight_type', 'order_no', 'driver_order_id', 'total_amount', 'pay_amount', 'is_pay', 'raise_amount',
+            'pay_type', 'car_seat_id', 'name', 'phone', 'people_num', 'luggage_num', 'remark', 'distance', 'duration', 'is_back', 'is_exclusive', 'is_child',
+            'appointment_time', 'flight_no', 'car_no', 'car_color', 'car_brand', 'take_time', 'driver_status', 'status', 'create_time', 'start_driver_time', 'end_driver_time'
+        ]);
+        $info = $model->getDetail(
+            params: ['order_no' => $params['order_no'], 'user_id' => $user['id']],
+            with  : [
+                'driver' => function ($query) {
+                    $query->select('id', 'name', 'area_code', 'mobile', 'avatar');
+                },
+                'points' => function ($query) {
+                    $query->select('id', 'order_id', 'type', 'lng', 'lat', 'address', 'postal_code', 'is_in_city', 'is_at_airport')->where('status', 1)->orderBy('id', 'asc');
+                }
+            ]
+        );
+        if (!$info) {
+            return AppResult::error('订单不存在');
+        }
+        $car_img = DriverCarModel::query()->where('driver_id',$info['driver']['id'] ?? 0)->value('car_img');
+        $info['end_driver_time']   = $info['driver_status'] == 2 ? time_hour(!empty($info['end_driver_time']) ? $info['end_driver_time'] : ($info['end_driver_time'] + $info['duration'])) : 0;
+        $info['driver_name']     = $info['driver']['name'] ?? '';
+        $info['driver_avatar']     = $car_img ?? '';
+        $info['driver_mobile']     = ($info['driver']['area_code'] ?? '') . ' ' . ($info['driver']['mobile'] ?? '');
+        $info['service_telephone'] = site('service_telephone');// 客服电话
+        unset($info['driver']);
+        return AppResult::success('success', $info);
+    }
+
+    /**
+     * 加价
+     * @param AddMoneyRequest $request
+     * @return string
+     */
+    public function add_money(AddMoneyRequest $request)
+    {
+        $params = $request->validated();// 获取校验通过的参数
+        $user   = AuthUser::getInstance()->get();
+        $model  = new OrderModel();
+        $info   = $model->getDetail(
+            params: ['order_no' => $params['order_no'], 'user_id' => $user['id'], 'status' => 10],
+        );
+        if (!$info) {
+            return AppResult::error('订单不存在或已被接单');
+        }
+
+        // 在线支付需校验金额
+        if ($info['pay_type'] == 1) {
+            $money = UserWalletModel::getOne($user['id'], 'money');
+            if ($money < $params['amount']) {
+                return AppResult::error('余额不足');
+            }
+        }
+
+        Db::beginTransaction();
+        // 在线支付需要提前扣费
+        if ($info['pay_type'] == 1) {
+            $type_name = DriverTypeBusiness::type[$info['type']] ?? '';
+            $wallet    = new UserWalletModel();
+            if (!$wallet->change($user['id'], (float)$params['amount'], "{$type_name}订单加价消费")) {
+                Db::rollBack();
+                return AppResult::error($wallet->getMessage());
+            }
+        }
+
+        if (!$model->addMoney($info['id'], (float)$params['amount'])) {
+            Db::rollBack();
+            return AppResult::error($model->getMessage());
+        }
+
+        Db::commit();
+
+        return AppResult::success($model->getMessage());
+    }
+
+    /**
+     * 取消订单
+     * @param DetailRequest $request
+     * @return string
+     */
+    public function cancel(CancelRequest $request)
+    {
+        $params = $request->validated();// 获取校验通过的参数
+        $user   = AuthUser::getInstance()->get();
+        Db::beginTransaction();
+        $model = new OrderModel();
+        if (!$model->cancel(order_no: $params['order_no'], user_id: $user['id'],reason: $params['reason'])) {
+            Db::rollBack();
+            return AppResult::error($model->getMessage());
+        }
+        Db::commit();
+        return AppResult::success($model->getMessage());
+    }
+
+    /**
+     * 司机发布顺风车
+     * @param TailwindDriverRequest $request
+     * @return string
+     */
+    public function tailwind_driver(TailwindDriverRequest $request)
+    {
+        $params = $request->validated();// 获取校验通过的参数
+        $user   = AuthUser::getInstance()->get();
+
+        $start_lng = $params['point'][0]['lng'] ?? '';
+        $start_lat = $params['point'][0]['lat'] ?? '';
+        $end_lng   = $params['point'][1]['lng'] ?? '0.00';
+        $end_lat   = $params['point'][1]['lat'] ?? '0.00';
+        unset($params['point']);
+        $params['status'] = 1;
+        $params['start_time_gt'] = time();
+        $model            = new OrderDriverModel();
+        $model->setSelect([
+            'id', 'driver_id', 'car_no', 'remark', 'start_time', 'people_num', 'people_at_num', 'start_address', 'start_lng', 'start_lat',
+            'end_address', 'end_lng', 'end_lat', 'status', 'create_time',
+            Db::raw("(st_distance(point ({$start_lng}, {$start_lat}),point(start_lng,start_lat))*111195) as start_distance"),
+            Db::raw("(st_distance(point ({$end_lng}, {$end_lat}),point(end_lng,end_lat))*111195) as end_distance"),
+        ]);
+        $list = $model->getList(
+            params : $params,
+            orderBy: ['start_time'=>'asc','start_distance' => 'asc', 'end_distance' => 'asc'],
+            with   : [
+                'order'  => function ($query) use ($user) {
+                    $query->select('id', 'driver_order_id', 'user_id', 'driver_id', 'status')->where('user_id', $user['id'] ?? 0)->whereIn('status', [10, 20, 30, 40]);
+                },
+                'driver' => function ($query) {
+                    $query->select('id', 'name', 'avatar');
+                },
+                'car'    => function ($query) {
+                    $query->select('driver_id', 'car_color', 'car_brand');
+                }
+            ]
+        );
+
+        foreach ($list as $key => $val) {
+            // 起点终点总距离差
+            $total_distance               = bcadd((string)$val['start_distance'], (string)($val['end_distance'] ?? 0), 2);
+            $list[$key]['total_distance'] = $total_distance;
+            // 起点距离
+            $stat_distance = 0;
+            if (isset($val['start_distance'])) {
+                if ($val['start_distance'] >= 1000) {
+                    $stat_distance = round(($val['start_distance'] / 1000), 2) . 'km';
+                } else {
+                    $stat_distance = round($val['start_distance'], 1) . 'm';
+                }
+            }
+            $list[$key]['start_distance'] = $stat_distance;
+
+            // 终点距离
+            $end_distance = 0;
+            if (isset($val['end_distance'])) {
+                if ($val['end_distance'] >= 1000) {
+                    $end_distance = round(($val['end_distance'] / 1000), 2) . 'km';
+                } else {
+                    $end_distance = round($val['end_distance'], 1) . 'm';
+                }
+            }
+            $list[$key]['end_distance'] = $end_distance;
+
+            $list[$key]['start_time']   = time_hour($val['start_time']);
+            $list[$key]['is_subscribe'] = !empty($val['order']) ? 1 : 0;
+
+            $list[$key]['car_color']     = $val['car']['car_color'] ?? '';
+            $list[$key]['car_brand']     = $val['car']['car_brand'] ?? '';
+            $list[$key]['driver_name']   = $val['driver']['name'] ?? '';
+            $list[$key]['driver_avatar'] = !empty($val['driver']['avatar']) ? cdn_url($val['driver']['avatar']) : '';
+            unset($list[$key]['car'], $list[$key]['driver']);
+        }
+
+        return AppResult::success('success', $list);
+    }
+
+    /**
+     * 司机发布顺风车详情
+     * @param TailwindDriverInfoRequest $request
+     * @return string
+     */
+    public function tailwind_driver_info(TailwindDriverInfoRequest $request)
+    {
+        $params = $request->validated();// 获取校验通过的参数
+        $user   = AuthUser::getInstance()->get();
+        $params['status'] = 1;
+        $model            = new OrderDriverModel();
+        $model->setSelect([
+            'id', 'driver_id', 'car_no', 'remark', 'start_time', 'people_num', 'people_at_num', 'start_address', 'start_lng', 'start_lat',
+            'end_address', 'end_lng', 'end_lat', 'status', 'create_time'
+        ]);
+        $info = $model->getDetail(
+            params : $params,
+            with   : [
+                'order'  => function ($query) use ($user) {
+                    $query->select('id', 'driver_order_id', 'user_id', 'driver_id', 'status')->where('user_id', $user['id'] ?? 0)->whereIn('status', [10, 20, 30, 40]);
+                },
+                'driver' => function ($query) {
+                    $query->select('id', 'name', 'avatar');
+                },
+                'car'    => function ($query) {
+                    $query->select('driver_id', 'car_color', 'car_brand');
+                }
+            ]
+        );
+
+        $info['start_time']   = time_hour($info['start_time']);
+        $info['is_subscribe'] = !empty($info['order']) ? 1 : 0;
+        $info['car_color']    = $info['car']['car_color'] ?? '';
+        $info['car_brand']    = $info['car']['car_brand'] ?? '';
+        $info['driver_name']  = $info['driver']['name'] ?? '';
+        $info['driver_avatar'] = !empty($info['driver']['avatar']) ? cdn_url($info['driver']['avatar']) : '';
+        unset($info['car'], $info['driver']);
+
+        return AppResult::success('success', $info);
+    }
+
+    /**
+     * 邀请司机接单
+     * @param TailwindInviteRequest $request
+     * @return string
+     */
+    public function tailwind_invite(TailwindInviteRequest $request)
+    {
+        $params = $request->validated();// 获取校验通过的参数
+        $user   = AuthUser::getInstance()->get();
+
+        $model = new OrderModel();
+        $info  = $model->getDetail(
+            params: ['order_no' => $params['order_no'], 'user_id' => $user['id'], 'status' => 10],
+        );
+        if (!$info) {
+            return AppResult::error('订单不存在或已被接单');
+        }
+
+        $driver_order = OrderDriverModel::query()->where('id', $params['driver_order_id'])->where('status',1)->first();
+        if (!$driver_order){
+            return AppResult::error('当前拼车路线不存在或行程已出发');
+        }
+
+        if (!$model->tailwind_invite($info['id'], $params['driver_order_id'])) {
+            return AppResult::error($model->getMessage());
+        }
+
+        // 发送系统消息
+        DriverMessageModel::add([
+            'driver_id' => $driver_order['driver_id'],
+            'type'      => 1,
+            'name'      => "顺风车有新订单",
+            'content'   => "用户邀请您同行,请前往顺风车行程中查看详情",
+            'value'     => $params['order_no']
+        ]);
+
+        return AppResult::success($model->getMessage());
+    }
+}

+ 41 - 0
app/Controller/Api/v1/SystemController.php

@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controller\Api\v1;
+
+use App\Controller\AbstractController;
+use App\Model\Arts\OrderModel;
+use App\Request\Api\v1\System\CancelRequest;
+use App\Utils\AppResult;
+use Hyperf\DbConnection\Db;
+
+/**
+ * Demo
+ * 管理系统操作
+ */
+class SystemController extends AbstractController
+{
+    // 日志模块名称
+    const LOG_MODULE = 'v1/SystemController';
+
+    /**
+     * 示例接口
+     *
+     * @param CancelRequest $request 校验参数注入类
+     * @return string
+     */
+    public function cancel(CancelRequest $request)
+    {
+        $params = $request->validated();// 获取校验通过的参数
+        Db::beginTransaction();
+        $model = new OrderModel();
+        if (!$model->cancel(order_no: $params['order_no'], reason: $params['reason'], is_system: 1)) {
+            Db::rollBack();
+            return AppResult::error($model->getMessage());
+        }
+        Db::commit();
+
+        return AppResult::success($model->getMessage());
+    }
+}

+ 328 - 0
app/Controller/Api/v1/UserController.php

@@ -0,0 +1,328 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controller\Api\v1;
+
+use App\Controller\AbstractController;
+use App\Model\Arts\CouponModel;
+use App\Model\Arts\DriverMessageModel;
+use App\Model\Arts\DriverModel;
+use App\Model\Arts\DriverWalletModel;
+use App\Model\Arts\MessageModel;
+use App\Model\Arts\UserAddressModel;
+use App\Model\Arts\UserCouponModel;
+use App\Model\Arts\UserModel;
+use App\Model\Arts\UserMoneyLogModel;
+use App\Model\Arts\UserWalletModel;
+use App\Request\Api\v1\User\AddressAddRequest;
+use App\Request\Api\v1\User\AddressDelRequest;
+use App\Request\Api\v1\User\AddressDetailRequest;
+use App\Request\Api\v1\User\AddressEditRequest;
+use App\Request\Api\v1\User\AddressListRequest;
+use App\Request\Api\v1\User\CouponsRequest;
+use App\Request\Api\v1\User\EditRequest;
+use App\Request\Api\v1\User\InviteRequest;
+use App\Request\Api\v1\User\MoneyLogRequest;
+use App\Request\Api\v1\User\OrderPageCouponsRequest;
+use App\Utils\AppResult;
+use App\Utils\Common;
+use App\Utils\Control\AuthUser;
+use Hyperf\DbConnection\Db;
+
+/**
+ * 用户管理
+ * UserController
+ */
+class UserController extends AbstractController
+{
+    // 日志模块名称
+    const LOG_MODULE = 'v1/UserController';
+
+    /**
+     * 用户信息
+     * @return string
+     */
+    public function info()
+    {
+        $user = AuthUser::getInstance()->get();
+        unset($user['password'], $user['salt']);
+        $user['money']  = UserWalletModel::getOne($user['id'], 'money');
+        $model = new UserCouponModel();
+        $list  = $model->getList(
+            params : [
+                'user_id'       => $user['id'],
+                'is_use'        => 0,
+                'valid'         => 0
+            ],
+            orderBy: ['id' => 'desc']
+        );
+        $user['coupon'] = count($list);
+        $user['avatar'] = cdn_url($user['avatar']);
+        $user['credit'] = "{$user['over_order_num']}/{$user['order_num']}";
+
+        return AppResult::success(result: $user);
+    }
+
+    // 个人信息编辑
+    public function edit(EditRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+        if (empty($params['avatar']) && empty($params['email']) && empty($params['nickname'])) {
+            return AppResult::success('修改成功');
+        }
+        $data = [];
+        !empty($params['nickname']) && $data['nickname'] = $params['nickname'];
+        !empty($params['avatar']) && $data['avatar'] = $params['avatar'];
+        !empty($params['email']) && $data['email'] = $params['email'];
+        if (!UserModel::query()->where('id', $user['id'])->update($data)) {
+            return AppResult::error('修改失败');
+        }
+        return AppResult::success('修改成功');
+    }
+
+    /**
+     * 余额变动记录
+     * @param MoneyLogRequest $request
+     * @return string
+     */
+    public function money_log(MoneyLogRequest $request)
+    {
+        $params = $request->validated();// 获取校验通过的参数
+        $user   = AuthUser::getInstance()->get();
+
+        if ($params['type_in'] == 1) {
+            $params['type_in'] = [1, 4];
+        } else {
+            $params['type_in'] = [2, 3];
+        }
+
+        $model = new UserMoneyLogModel();
+        $list  = $model->getList(
+            params: array_merge(['user_id' => $user['id']], $params), orderBy: ['id' => 'desc']
+        );
+        return AppResult::success('success', $list);
+    }
+
+    /**
+     * 常用地址列表
+     * @param AddressListRequest $request
+     * @return string
+     */
+    public function address_list(AddressListRequest $request)
+    {
+        $params = $request->validated();// 获取校验通过的参数
+        $user   = AuthUser::getInstance()->get();
+        $model  = new UserAddressModel();
+        $list   = $model->getList(
+            params: array_merge($params, ['user_id' => $user['id']])
+        );
+
+        return AppResult::success(result: $list);
+    }
+
+    public function address_detail(AddressDetailRequest $request)
+    {
+        $params = $request->validated();// 获取校验通过的参数
+        $user   = AuthUser::getInstance()->get();
+        $model  = new UserAddressModel();
+        $list   = $model->getDetail(
+            params: array_merge($params, ['user_id' => $user['id']])
+        );
+        return AppResult::success(result: $list);
+    }
+
+    /**
+     * 常用地址添加
+     * @param AddressAddRequest $request
+     * @return string
+     */
+    public function address_add(AddressAddRequest $request)
+    {
+        $params = $request->validated();// 获取校验通过的参数
+        $user   = AuthUser::getInstance()->get();
+        $params = array_merge($params, ['user_id' => $user['id']]);
+        if (!UserAddressModel::add($params)) {
+            return AppResult::error('添加失败');
+        }
+        return AppResult::success('创建成功');
+    }
+
+    /**
+     * 常用地址编辑
+     * @param AddressEditRequest $request
+     * @return string
+     */
+    public function address_edit(AddressEditRequest $request)
+    {
+        $params = $request->validated();// 获取校验通过的参数
+        $user   = AuthUser::getInstance()->get();
+        $params = array_merge($params, ['user_id' => $user['id']]);
+        if (!UserAddressModel::edit((int)$params['id'], $params)) {
+            return AppResult::error('修改失败');
+        }
+        return AppResult::success('修改成功');
+    }
+
+    /**
+     * 常用地址编辑
+     * @param AddressDelRequest $request
+     * @return string
+     */
+    public function address_del(AddressDelRequest $request)
+    {
+        $params = $request->validated();// 获取校验通过的参数
+        $user   = AuthUser::getInstance()->get();
+        if (!UserAddressModel::del((int)$params['id'], (int)$user['id'])) {
+            return AppResult::error('删除失败');
+        }
+        return AppResult::success('删除成功');
+    }
+
+    /**
+     * 我的优惠券
+     * @param CouponsRequest $request
+     * @return \Psr\Http\Message\MessageInterface|\Psr\Http\Message\ResponseInterface
+     * @throws \Psr\Container\ContainerExceptionInterface
+     * @throws \Psr\Container\NotFoundExceptionInterface
+     */
+    public function coupons(CouponsRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+
+        $model = new UserCouponModel();
+        $list  = $model->getList(
+            params: array_merge($params, ['user_id' => $user['id']]), orderBy: ['id' => 'desc']
+        );
+        foreach ($list as $key => $val) {
+            $list[$key]['valid_at'] = date('Y-m-d H:i:s', $val['valid_at']);
+        }
+        return AppResult::success(result: $list);
+    }
+
+    /**
+     * 订单使用优惠券
+     * @param OrderPageCouponsRequest $request
+     * @return \Psr\Http\Message\MessageInterface|\Psr\Http\Message\ResponseInterface
+     * @throws \Psr\Container\ContainerExceptionInterface
+     * @throws \Psr\Container\NotFoundExceptionInterface
+     */
+    public function order_page_coupons(OrderPageCouponsRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+
+        $model = new UserCouponModel();
+        $list  = $model->getList(
+            params : [
+                'user_id'       => $user['id'],
+                'type'          => $params['type'],
+                'is_use'        => 0,
+                'is_valid'         => 0,
+                'min_money_min' => $params['total_amount']
+            ],
+            orderBy: ['id' => 'desc']
+        );
+        foreach ($list as $key => $val) {
+            $total_amount = (string)round((float)bcsub($params['total_amount'], $val['money'], 2));// 四舍五入
+            $rmb_total_amount = (string)round((float)bcmul($total_amount, site('dollar_to_rmb'), 2));// 四舍五入
+            $list[$key]['valid_at']   = date('Y-m-d H:i:s', $val['valid_at']);
+            $list[$key]['pay_amount'] = $total_amount;
+            $list[$key]['rmb_pay_amount'] = $rmb_total_amount;
+        }
+        return AppResult::success(result: $list);
+    }
+
+    /**
+     * 邀请用户
+     * @param InviteRequest $request
+     * @return \Psr\Http\Message\MessageInterface|\Psr\Http\Message\ResponseInterface
+     * @throws \Psr\Container\ContainerExceptionInterface
+     * @throws \Psr\Container\NotFoundExceptionInterface
+     */
+    public function invite(InviteRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+        if (!empty($user['parent_id']) || !empty($user['driver_id'])) {
+            return AppResult::error('已经被邀请过了');
+        }
+        if ($params['invite_code'] == $user['invite_code']) {
+            return AppResult::error('不可以邀请自己');
+        }
+        $first = substr($params['invite_code'], 0, 1);
+        if (!in_array($first, ['U', 'D'])) {
+            return AppResult::error('邀请码错误');
+        }
+        if ($first == 'U') {
+            $info            = UserModel::query()->where('invite_code', $params['invite_code'])->where('status', 1)->first();
+            $up['parent_id'] = $info['id'] ?? 0;
+        } else {
+            $info            = DriverModel::query()->where('invite_code', $params['invite_code'])->where('status', 1)->first();
+            $up['driver_id'] = $info['id'] ?? 0;
+        }
+        if (!$info) {
+            return AppResult::error('邀请码异常');
+        }
+        Db::beginTransaction();
+        if (!UserModel::query()->where('id', $user['id'])->update($up)) {
+            Db::rollBack();
+            return AppResult::error('操作失败');
+        }
+
+        $time = time();
+        if ($first == 'U'){
+            // 发放优惠券
+            $coupon = CouponModel::getDetail(['id' => site('invite_user_coupon_id')]);
+            if ($coupon){
+                $insert = [
+                    'user_id'     => $up['parent_id'],
+                    'coupon_id'   => $coupon['id'],
+                    'name'        => $coupon['name'],
+                    'type'        => $coupon['type'],
+                    'min_money'   => $coupon['min_money'],
+                    'money'       => $coupon['money'],
+                    'valid_at'    => strtotime(date('Y-m-d 23:59:59', $time + ($coupon['valid_days'] * 24 * 60 * 60))),// 自领取之日起 计算过期时间
+                    'remark'      => '邀请用户奖励',
+                    'status'      => 1,
+                    'create_time' => $time
+                ];
+                if (!UserCouponModel::query()->insert($insert)) {
+                    Db::rollBack();
+                    return AppResult::error('发放失败');
+                }
+            }
+            // 发送系统消息
+            MessageModel::add([
+                'user_id' => $up['parent_id'],
+                'type'    => 1,
+                'name'    => "邀请用户奖励",
+                'content' => "邀请用户成功,优惠券奖励已发放",
+                'value'   => $coupon['id']
+            ]);
+        } else {
+            // 发放奖金
+            $invite_driver_amount = site('invite_driver_amount');
+            if ($invite_driver_amount){
+                $wallet = new DriverWalletModel();
+                if (!$wallet->change($up['driver_id'], (float)$invite_driver_amount, "邀请用户奖励", 4)) {
+                    Db::rollBack();
+                    return AppResult::error($wallet->getMessage());
+                }
+            }
+            // 发送系统消息
+            DriverMessageModel::add([
+                'driver_id' => $up['driver_id'],
+                'type'      => 1,
+                'name'      => "邀请用户奖励",
+                'content'   => "邀请用户成功,奖励金已发放",
+                'value'     => ''
+            ]);
+        }
+
+        Db::commit();
+        return AppResult::success('操作成功');
+    }
+}

+ 94 - 0
app/Controller/Api/v1/WechatController.php

@@ -0,0 +1,94 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controller\Api\v1;
+
+use App\Controller\AbstractController;
+use App\Master\Framework\Library\Easywechat\MiniApp;
+use App\Master\Framework\Library\Easywechat\PayService;
+use App\Request\Api\v1\WechatMiniAppCode;
+use App\Utils\AppResult;
+
+/**
+ * Wechat
+ * 示例
+ */
+class WechatController extends AbstractController
+{
+    // 日志模块名称
+    const LOG_MODULE = 'v1/WechatController';
+
+    /**
+     * 小程序授权
+     *
+     * @param WechatMiniAppCode $request
+     * @return string
+     * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
+     */
+    public function miniAppCode(WechatMiniAppCode $request): string
+    {
+        $params = $request->validated();
+        $mini = new MiniApp();
+        if (!$mini->jscode2session($params['code'] ?? '')){
+            return AppResult::response201($mini->getMessage(),$mini->get());
+        }
+        /**
+         * ==== 返回值示例 ====
+         * {"session_key": "session_key-xxx","openid": "openid-xxx"}
+         */
+        return AppResult::response200($mini->getMessage(),$mini->get());
+    }
+
+    /**
+     * 小程序手机号授权
+     *
+     * @param WechatMiniAppCode $request
+     * @return string
+     * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
+     */
+    public function miniAppPhone(WechatMiniAppCode $request): string
+    {
+        $params = $request->validated();
+        $mini = new MiniApp();
+        if (!$mini->getUserPhone($params['code'] ?? '')){
+            return AppResult::response201($mini->getMessage(),$mini->get());
+        }
+        /**
+         * ==== 返回值示例 ====
+         * {
+         *     "errcode": 0,
+         *     "errmsg": "ok",
+         *     "phone_info": {
+         *         "phoneNumber": "158xxxxxxxx",
+         *         "purePhoneNumber": "158xxxxxxxx",
+         *         "countryCode": "86",
+         *         "watermark": {
+         *             "timestamp": 1709534957,
+         *             "appid": "wxcced716f40d8b84b"
+         *         }
+         *     }
+         * }
+         */
+        return AppResult::response200($mini->getMessage(),$mini->get());
+    }
+
+    /**
+     * 小程序支付
+     *
+     * @return string
+     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
+     * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
+     * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
+     */
+    public function miniAppPay()
+    {
+        $openid = 'oflOP6qqIN7-mNbpgL38Pp8wXvVs';
+        $order_no = time().rand(10,99);
+        $pay = new PayService();
+        if (!$pay->jsapi($openid,$order_no,1,'测试','http://hyperf.yangertao.com')){
+            return AppResult::response201($pay->getMessage(),$pay->get());
+        }
+        return AppResult::response200($pay->getMessage(),$pay->get());
+    }
+}

+ 58 - 0
app/Exception/Handler/AppExceptionHandler.php

@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+
+namespace App\Exception\Handler;
+
+use App\Kernel\StdoutLogInterface;
+use Hyperf\Config\Annotation\Value;
+use Hyperf\Contract\StdoutLoggerInterface;
+use Hyperf\ExceptionHandler\ExceptionHandler;
+use Hyperf\HttpMessage\Stream\SwooleStream;
+use Psr\Http\Message\ResponseInterface;
+use Throwable;
+
+class AppExceptionHandler extends ExceptionHandler
+{
+    /**
+     * @var bool
+     */
+    #[Value("app_debug")]
+    private $app_debug;
+
+    public function __construct(protected StdoutLoggerInterface $logger, public StdoutLogInterface $stdoutLog)
+    {
+    }
+
+    public function handle(Throwable $throwable, ResponseInterface $response)
+    {
+        $error = sprintf('%s[%s] in %s', $throwable->getMessage(), $throwable->getLine(), $throwable->getFile());
+        $error_string = $throwable->getTraceAsString();
+
+        $this->logger->error($error);
+        $this->logger->error($error_string);
+
+        // 自定义日志通道
+        if ($this->app_debug) {
+            $this->stdoutLog->log->error($error);
+            $this->stdoutLog->log->error($error_string);
+        }
+
+        // 根据debug输出异常
+        $body = $this->app_debug ? "{$error}\n{$error_string}" : 'Internal Server Error.';
+        return $response->withHeader('Server', 'Hyperf')->withStatus(500)->withBody(new SwooleStream($body));
+    }
+
+    public function isValid(Throwable $throwable): bool
+    {
+        return true;
+    }
+}

+ 44 - 0
app/Exception/Handler/ValidationExceptionHandler.php

@@ -0,0 +1,44 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * 表单验证器,异常处理器
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace App\Exception\Handler;
+
+use App\Utils\AppResult;
+use Hyperf\ExceptionHandler\ExceptionHandler;
+use Hyperf\HttpMessage\Stream\SwooleStream;
+use Hyperf\Validation\ValidationException;
+use Psr\Http\Message\ResponseInterface;
+use Throwable;
+
+class ValidationExceptionHandler extends ExceptionHandler
+{
+    public function handle(Throwable $throwable, ResponseInterface $response)
+    {
+        $this->stopPropagation();
+        /** @var \Hyperf\Validation\ValidationException $throwable */
+        $body = json_encode([
+            'code'    => 0,
+            'msg' => $throwable->validator->errors()->first(),
+            'data'  => null
+        ], JSON_UNESCAPED_UNICODE);
+        if (!$response->hasHeader('content-type')) {
+            $response = $response->withAddedHeader('content-type', 'application/json; charset=utf-8');
+        }
+        return $response->withStatus(200)->withBody(new SwooleStream($body));
+    }
+
+    public function isValid(Throwable $throwable): bool
+    {
+        return $throwable instanceof ValidationException;
+    }
+}

+ 45 - 0
app/Job/DemoJob.php

@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Job;
+
+use App\Utils\LogUtil;
+use Hyperf\AsyncQueue\Job;
+
+class DemoJob extends Job
+{
+    //日志板块
+    private const LOG_MODULE = 'DemoJob';
+
+    public $params;
+
+    /**
+     * 任务执行失败后的重试次数,即最大执行次数为 $maxAttempts+1 次
+     */
+    protected int $maxAttempts = 2;
+
+    public function __construct($params)
+    {
+        // 这里最好是普通数据,不要使用携带 IO 的对象,比如 PDO 对象
+        $this->params = $params;
+    }
+
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        //日志统一写入
+        LogUtil::getInstance('Queues/');//设置日志存入通道
+        LogUtil::info('开始处理', self::LOG_MODULE, __FUNCTION__, ['params' => $this->params]);
+        // 根据参数处理具体逻辑
+        // 通过具体参数获取模型等
+        // 这里的逻辑会在 ConsumerProcess 进程中执行
+//        var_dump($this->params);
+        LogUtil::info('处理结果', self::LOG_MODULE, __FUNCTION__);
+        LogUtil::close();
+    }
+}

+ 16 - 0
app/Kernel/Log.php

@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Kernel;
+
+use Hyperf\Context\ApplicationContext;
+use Hyperf\Logger\LoggerFactory;
+
+class Log
+{
+    public static function get(string $name = 'app')
+    {
+        return ApplicationContext::getContainer()->get(LoggerFactory::class)->get($name);
+    }
+}

+ 13 - 0
app/Kernel/StdoutLogInterface.php

@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Kernel;
+
+class StdoutLogInterface
+{
+    public $log;
+    public function __construct(){
+        $this->log = Log::get('sys');
+    }
+}

+ 66 - 0
app/Listener/DbQueryExecutedListener.php

@@ -0,0 +1,66 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+
+namespace App\Listener;
+
+use Hyperf\Collection\Arr;
+use Hyperf\Database\Events\QueryExecuted;
+use Hyperf\Event\Annotation\Listener;
+use Hyperf\Event\Contract\ListenerInterface;
+use Hyperf\Logger\LoggerFactory;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+
+#[Listener]
+class DbQueryExecutedListener implements ListenerInterface
+{
+    /**
+     * @var LoggerInterface
+     */
+    private $logger;
+
+    public function __construct(ContainerInterface $container)
+    {
+        $this->logger = $container->get(LoggerFactory::class)->get('sql');
+    }
+
+    public function listen(): array
+    {
+        return [
+            QueryExecuted::class,
+        ];
+    }
+
+    /**
+     * @param QueryExecuted $event
+     */
+    public function process(object $event): void
+    {
+        if ($event instanceof QueryExecuted) {
+            $sql = $event->sql;
+            if (! Arr::isAssoc($event->bindings)) {
+                $position = 0;
+                foreach ($event->bindings as $value) {
+                    $position = strpos($sql, '?', $position);
+                    if ($position === false) {
+                        break;
+                    }
+                    $value = "'{$value}'";
+                    $sql = substr_replace($sql, $value, $position, 1);
+                    $position += strlen($value);
+                }
+            }
+
+            $this->logger->info(sprintf('[%s] %s', $event->time, $sql));
+        }
+    }
+}

+ 41 - 0
app/Listener/MqttListener.php

@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+
+namespace App\Listener;
+
+use Hyperf\Event\Annotation\Listener;
+use Hyperf\Event\Contract\ListenerInterface;
+use Nashgao\MQTT\Event\OnReceiveEvent;
+
+/**
+ * mqtt 信号处理器
+ */
+#[Listener]
+class MqttListener implements ListenerInterface
+{
+    public function listen(): array
+    {
+        return [
+            OnReceiveEvent::class,
+        ];
+    }
+
+
+    /**
+     * @param OnReceiveEvent $event
+     * @return void
+     */
+    public function process($event): void
+    {
+        dd($event->message);
+    }
+}

+ 35 - 0
app/Listener/ResumeExitCoordinatorListener.php

@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+
+namespace App\Listener;
+
+use Hyperf\Command\Event\AfterExecute;
+use Hyperf\Coordinator\Constants;
+use Hyperf\Coordinator\CoordinatorManager;
+use Hyperf\Event\Annotation\Listener;
+use Hyperf\Event\Contract\ListenerInterface;
+
+#[Listener]
+class ResumeExitCoordinatorListener implements ListenerInterface
+{
+    public function listen(): array
+    {
+        return [
+            AfterExecute::class,
+        ];
+    }
+
+    public function process(object $event): void
+    {
+        CoordinatorManager::until(Constants::WORKER_EXIT)->resume();
+    }
+}

+ 16 - 0
app/Master/Enum/PassportEnum.php

@@ -0,0 +1,16 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Master\Enum;
+
+use Hyperf\Constants\AbstractConstants;
+use Hyperf\Constants\Annotation\Constants;
+
+#[Constants]
+class PassportEnum extends AbstractConstants
+{
+    /**
+     * @Message("用户信息")
+     */
+    const USER_INFO = 'USER_INFO';
+}

+ 27 - 0
app/Master/Enum/RedisKeyEnum.php

@@ -0,0 +1,27 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Master\Enum;
+
+use Hyperf\Constants\AbstractConstants;
+use Hyperf\Constants\Annotation\Constants;
+
+#[Constants]
+class RedisKeyEnum extends AbstractConstants
+{
+    /**
+     * @Message("用户信息")
+     */
+    const ORDER_NO                 = 'ORDER_NO:';                // 订单编号
+    const NO                       = 'NO:';                // 编号
+    const API_REQUEST_TRAFFIC      = 'API_REQUEST_TRAFFIC:';     // 接口流量
+    const WX_MINI_APP_ACCESS_TOKEN = 'WX_MINI_APP_ACCESS_TOKEN:';// 微信 小程序 access_token
+    const TOKEN_ONCE               = 'TOKEN_ONCE:';              // 设置唯一token
+    const TOKEN_TIME               = 'TOKEN_TIME:';              // 设置token时间
+    const SEND_SMS_TIMEOUT_TIMES   = 'SEND_SMS_TIMEOUT_TIMES:';  // 短信发送次数限制
+    const SEND_SMS_MOBILE          = 'SEND_SMS_MOBILE:';         // 手机号短信发送次数
+    const FA_SITE_SETUP            = 'FA_SITE_SETUP:';           // fastadmin site.php
+    const API_DRIVER_SKILL_ORDER   = 'API_DRIVER_SKILL_ORDER:';     // 司机抢单
+    const UNI_PUSH_TOKEN           = 'UNI_PUSH_TOKEN:';     // 个推token
+    const USER_DRIVER_CHAT_DEL     = 'USER_DRIVER_CHAT_DEL:';     // 删除用户司机聊天框
+}

+ 20 - 0
app/Master/Framework/Extend/Module.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace App\Master\Framework\Extend;
+
+use App\Model\Framework\AdminSetupModel;
+
+class Module
+{
+    /**
+     * 单页模型
+     * @param string $tables
+     * @return array|mixed
+     */
+    public static function _SetupModule(string $tables): mixed
+    {
+        $setup = new AdminSetupModel();
+        $info  = $setup->getDetail(params: ['table' => $tables]);
+        return $info['value'] ?? [];
+    }
+}

+ 207 - 0
app/Master/Framework/Helper/common.php

@@ -0,0 +1,207 @@
+<?php
+if (!function_exists('line_feed')) {
+    /**
+     * 多行输入框回车换行
+     * @param string $str
+     * @return string mixed
+     */
+    function line_feed(string $str): string
+    {
+        return str_replace("\n", "<br />", $str ?? '');
+    }
+}
+
+if (!function_exists('str_limit')){
+    /**
+     * 超出字符省略
+     * @param $value
+     * @param int $limit
+     * @param string $end
+     * @return mixed|string
+     */
+    function str_limit($value, int $limit = 100, string $end = '...'): mixed {
+        if (mb_strwidth($value, 'UTF-8') <= $limit) {
+            return $value;
+        }
+        return rtrim(mb_strimwidth($value, 0, $limit, '', 'UTF-8')) . $end;
+    }
+}
+
+if (!function_exists('str_behind')) {
+    /**
+     * 获取指定字符之后的数据
+     * @param string $str
+     * @param string $keyword
+     * @return string
+     */
+    function str_behind(string $str, string $keyword = '')
+    {
+        $str = explode($keyword, $str);
+        if (count($str) < 1) {
+            return $str[0];
+        }
+        $string = '';
+        foreach ($str as $key => $val) {
+            if ($key === 0) continue;
+            $string .= "/{$val}";
+        }
+        return $string;
+    }
+}
+
+/**
+ * fastadmin site config
+ * @param string $key
+ * @return false|mixed|Redis|string
+ * @throws Exception
+ */
+if (!function_exists('site')) {
+    function site(string $key = '')
+    {
+        $config = \App\Utils\RedisUtil::getInstance(\App\Master\Enum\RedisKeyEnum::FA_SITE_SETUP)->get();
+        if (!$config){
+            throw new Exception('fastadmin site config not found');
+        }
+        $config = json_decode($config,true);
+        if (!empty($key)){
+            return $config[$key] ?? '';
+        }
+        return $config;
+    }
+}
+
+/**
+ * 将秒时间转换具体时间:秒转天时分秒
+ * @return bool|string
+ */
+if (!function_exists('time_ext')) {
+    function time_ext(int $seconds,int $type = 0,int $min_m = 0): bool|string
+    {
+        $d = floor($seconds / (3600*24));
+        $h = floor(($seconds % (3600*24)) / 3600);
+        $m = floor((($seconds % (3600*24)) % 3600) / 60);
+        if ($type === 1){
+            if($d>'0'){
+                $time = "{$d}天{$h}小时{$m}分钟";
+            }else{
+                if($h!='0'){
+                    $time = "{$h}小时{$m}分钟";
+                }else{
+                    $m = ($min_m === 1 && $m < 1) ? 1 : $m;
+                    $time = "{$m}分钟";
+                }
+            }
+        } else {
+            if($d>'0'){
+                $time = "{$d}d{$h}h{$m}m";
+            }else{
+                if($h!='0'){
+                    $time = "{$h}h{$m}m";
+                }else{
+                    $m = ($min_m === 1 && $m < 1) ? 1 : $m;
+                    $time = "{$m}m";
+                }
+            }
+        }
+        return $time;
+    }
+}
+
+/**
+ * 将时间戳转换小时时间
+ * @return bool|string
+ */
+if (!function_exists('time_hour')) {
+    function time_hour(int $time): bool|string
+    {
+        $year = date('Y', $time);
+        $month = date('m', $time);
+        $day = date('d', $time);
+
+        if ($day == date('d') && $month == date('m') && $year == date('Y')) {
+            $times = date('H:i', $time);
+        } else {
+            $times = date('m-d H:i', $time);
+        }
+
+        return $times;
+    }
+}
+
+if (!function_exists('unix_time')) {
+    /**
+     * 格式化
+     * @param $time
+     * @return string
+     */
+    function unix_time($time): string
+    {
+        //获取今天凌晨的时间戳
+        $day = strtotime(date('Y-m-d', time()));
+        //获取昨天凌晨的时间戳
+        $pday = strtotime(date('Y-m-d', strtotime('-1 day')));
+        //获取现在的时间戳
+        $nowtime = time();
+        $t       = $nowtime - $time;
+        if ($time < $pday) {
+            $str = date('m-d', $time);
+        } elseif ($time < $day && $time > $pday) {
+            $str = "昨天";
+        } elseif ($t > 60 * 60) {
+            $str = floor($t / (60 * 60)) . "小时前";
+        } elseif ($t > 60) {
+            $str = floor($t / 60) . "分钟前";
+        } else {
+            $str = "刚刚";
+        }
+        return $str;
+    }
+}
+
+/**
+ * 毫秒时间戳
+ * @return int
+ */
+if (!function_exists('ms_time')) {
+    function ms_time(): int
+    {
+        list($ms, $sec) = explode(' ', microtime());
+        return intval((floatval($ms) + floatval($sec)) * 1000);
+    }
+}
+
+/**
+ * 获取上传资源的CDN的地址
+ * @param string $url 资源相对地址
+ * @param bool $ssl
+ * @return string
+ */
+if (!function_exists('cdn_url')) {
+    function cdn_url(string $url,bool $ssl = true)
+    {
+        $regex = "/^((?:[a-z]+:)?\/\/|data:image\/)(.*)/i";
+        $cdn_url = \Hyperf\Config\config('cdn_url');
+
+        if (strrpos($url, 'http') !== false || preg_match($regex, $url)) {
+            $url = $url;
+        } elseif(empty($cdn_url)) {
+            $domain = $_SERVER['HTTP_HOST'] ?? '127.0.0.1';
+            $http = 'http://';
+            $ssl && $http = ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')) ? 'https://' : 'http://';
+            $url = $http.$domain.$url;
+        }else{
+            $url = $cdn_url.$url;
+        }
+
+        return $url;
+    }
+}
+
+if (!function_exists('dd')) {
+    function dd(...$vars)
+    {
+        foreach ($vars as $v) {
+            var_dump($v);
+        }
+    }
+}

+ 90 - 0
app/Master/Framework/Library/AliCloud/AliSms.php

@@ -0,0 +1,90 @@
+<?php
+
+namespace App\Master\Framework\Library\AliCloud;
+
+use AlibabaCloud\SDK\Dysmsapi\V20170525\Dysmsapi;
+use App\Master\Framework\Library\Library;
+use AlibabaCloud\Tea\Exception\TeaError;
+use Darabonba\OpenApi\Models\Config;
+use AlibabaCloud\SDK\Dysmsapi\V20170525\Models\SendSmsRequest;
+use AlibabaCloud\Tea\Utils\Utils\RuntimeOptions;
+
+class AliSms extends Library
+{
+    private string $AccessKeyId;//id
+    private string $AccessSecret;//key
+    private string $SignName;//短信签名
+
+    private array $type = [
+        1 => 'SMS_469005749',// 验证码 code
+        2 => 'SMS_473780055',// 预约提前三小时提醒 string:name,int:time
+        3 => 'SMS_473155011',// 订单被接单提醒 string:time
+        4 => 'SMS_473005012',// 订单取消通知
+    ];
+
+    /**
+     * 实例化
+     */
+    public function __construct()
+    {
+        // 获取配置信息
+        $this->AccessKeyId  = (string)site('ali_access_key_id');
+        $this->AccessSecret = (string)site('ali_access_secret');
+        $this->SignName     = (string)site('ali_sign_name');
+    }
+
+    /**
+     * 使用AK&SK初始化账号Client
+     * @return Dysmsapi Client
+     */
+    public function createClient(){
+        // 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
+        // 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/311677.html。
+        $config = new Config([
+            // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
+            "accessKeyId" => $this->AccessKeyId,
+            // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
+            "accessKeySecret" => $this->AccessSecret
+        ]);
+        // Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi
+        $config->endpoint = "dysmsapi.aliyuncs.com";
+        return new Dysmsapi($config);
+    }
+
+    /**
+     * 发送短信
+     * @param string $mobile
+     * @param array $authData
+     * @return bool
+     */
+    public function send(string $mobile, array $authData,int $type = 1): bool
+    {
+        if (!isset($this->type[$type])){
+            return $this->error('短信模板类型有误');
+        }
+        $client = $this->createClient();
+        $sendSmsRequest = new SendSmsRequest([
+            "phoneNumbers" => $mobile,
+            "signName" => $this->SignName,
+            "templateCode" => $this->type[$type],
+            "templateParam" => !empty($authData) ? json_encode($authData, JSON_UNESCAPED_UNICODE) : ''
+        ]);
+
+        try {
+            // 复制代码运行请自行打印 API 的返回值
+            $res = $client->sendSmsWithOptions($sendSmsRequest, new RuntimeOptions([]));
+        } catch (\Exception $error) {
+            if (!($error instanceof TeaError)) {
+                $error = new TeaError([], $error->getMessage(), $error->getCode(), $error);
+            }
+            // 错误 message $error->message
+            // 诊断地址 $error->data["Recommend"]
+            return $this->error($error->message,$error->data);
+        }
+        if ($res->body->code != 'OK'){
+            return $this->error($res->body->message);
+        }
+
+        return $this->success('发送成功');
+    }
+}

+ 135 - 0
app/Master/Framework/Library/Easywechat/EasyModule.php

@@ -0,0 +1,135 @@
+<?php
+
+namespace App\Master\Framework\Library\Easywechat;
+
+use Pengxuxu\HyperfWechat\EasyWechat;
+
+/**
+ * 微信小程序开发组
+ * class MiniAppService
+ */
+class EasyModule
+{
+    protected string $message = '';
+    protected array  $data    = [];
+    protected int    $ttl     = 1 * 24 * 60 * 60;
+
+    public function __construct() {}
+
+    /**
+     * 小程序
+     * @param array $config
+     * @param string $name
+     * @return \EasyWeChat\MiniApp\Application
+     */
+    protected function miniApp(array $config = [], string $name = 'default')
+    {
+        return EasyWechat::miniApp($name,$config);
+    }
+
+    /**
+     * 公众号
+     * @param array $config
+     * @param string $name
+     * @return \EasyWeChat\OfficialAccount\Application
+     */
+    protected function official(array $config = [], string $name = 'default')
+    {
+        return EasyWechat::officialAccount($name,$config);
+    }
+
+    /**
+     * 支付
+     * @param array $config
+     * @param string $name
+     * @return \EasyWeChat\Pay\Application
+     */
+    protected function pay(array $config = [], string $name = 'default')
+    {
+        return EasyWechat::pay($name,$config);
+    }
+
+    /**
+     * 统一校验 返回
+     * @param $response
+     * @return bool
+     */
+    protected function response($response): bool
+    {
+        if ($response->isFailed()) {
+            return $this->error($response->getContent());
+        }
+
+        return $this->success($response->getContent());
+    }
+
+    /**
+     * 返回成功结果
+     * @param string $response
+     * @return bool
+     */
+    protected function success(string $response): bool
+    {
+        $this->set($response,true);
+        return true;
+    }
+
+    /**
+     * 返回失败结果
+     * @param string $response
+     * @return false
+     */
+    protected function error(string $response): bool
+    {
+        $this->set($response,false);
+        return false;
+    }
+
+    /**
+     * 存入结果
+     * @param string $response
+     * @return bool
+     */
+    protected function set(string $response, bool $status = true): bool
+    {
+        $this->data = $this->json($response);
+
+        if (!empty($this->data['errmsg'])) {
+            $this->message = $this->data['errmsg'];
+        } elseif (!empty($this->data['message'])) {
+            $this->message = $this->data['message'];
+        } else {
+            $this->message = $status ? 'success' : 'EasyModule控件有误,请输出response';
+        }
+
+        return true;
+    }
+
+    /**
+     * 解析数据
+     * @param string $response
+     * @return mixed
+     */
+    protected function json(string $response): mixed
+    {
+        return json_decode($response, true);
+    }
+
+    /**
+     * 获取成功数据
+     * @return array
+     */
+    public function get(): array
+    {
+        return $this->data;
+    }
+
+    /**
+     * 获取消息
+     * @return string
+     */
+    public function getMessage(): string
+    {
+        return $this->message;
+    }
+}

+ 135 - 0
app/Master/Framework/Library/Easywechat/MiniApp.php

@@ -0,0 +1,135 @@
+<?php
+
+namespace App\Master\Framework\Library\Easywechat;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Utils\RedisUtil;
+use EasyWeChat\MiniApp\Application;
+use EasyWeChat\Kernel\Contracts\Config;
+
+/**
+ * 微信小程序开发组
+ * class MiniAppService
+ */
+class MiniApp extends EasyModule
+{
+    protected Application $app;
+    protected Config      $config;
+
+    /**
+     * 实例化
+     *
+     */
+    public function __construct()
+    {
+        parent::__construct();
+
+        $this->app    = $this->miniApp();
+        $this->config = $this->app->getConfig();
+    }
+
+    /**
+     * openid 授权
+     *
+     * @param string $code
+     * @return bool
+     * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
+     */
+    public function jscode2session(string $code): bool
+    {
+        $app = $this->app;
+
+        $api = $app->getClient();
+
+        $response = $api->get('/sns/jscode2session', [
+            'appid'      => $this->config['app_id'],
+            'secret'     => $this->config['secret'],
+            'js_code'    => $code,
+            'grant_type' => 'authorization_code',
+        ]);
+
+        if (!$this->response($response)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * 获取手机号
+     *
+     * @param string $code
+     * @return bool
+     * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
+     */
+    public function getUserPhone(string $code): bool
+    {
+        $app = $this->app;
+
+        if (!$access_token = $this->getAccessToken()) {
+            return false;
+        }
+
+        $api = $app->getClient();
+
+        $response = $api->postJson("/wxa/business/getuserphonenumber?access_token={$access_token}", [
+            'code' => $code
+        ]);
+
+        if (!$this->response($response)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * 获取 access_token
+     * @return false|mixed|\Redis|string
+     * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
+     */
+    private function getAccessToken()
+    {
+        if ($access_token = RedisUtil::getInstance(RedisKeyEnum::WX_MINI_APP_ACCESS_TOKEN)->get()) {
+            return $access_token;
+        }
+
+        if (!$this->stable_token()) {
+            return false;
+        }
+
+        $data = $this->get();
+        if (empty($data['access_token'])) {
+            return false;
+        }
+
+        RedisUtil::getInstance(RedisKeyEnum::WX_MINI_APP_ACCESS_TOKEN)->setex($data['access_token'], (int)($data['expires_in'] ?? 0));
+
+        return $data['access_token'];
+    }
+
+    /**
+     * 获取 access_token
+     * @return bool
+     * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
+     */
+    private function stable_token(): bool
+    {
+        $app = $this->app;
+
+        $api = $app->getClient();
+
+        $response = $api->postJson('/cgi-bin/stable_token', [
+            'grant_type'    => 'client_credential',
+            'appid'         => $this->config['app_id'],
+            'secret'        => $this->config['secret'],
+            'force_refresh' => false,// 默认false:普通模式false;强制刷新模式true;
+        ]);
+
+        if (!$this->response($response)) {
+            return false;
+        }
+
+        return true;
+    }
+}

+ 56 - 0
app/Master/Framework/Library/Easywechat/OfficialService.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace App\Master\Framework\Library\Easywechat;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Master\Framework\Extend\Module;
+use App\Utils\RedisUtil;
+use EasyWeChat\OfficialAccount\Application;
+use Illuminate\Support\Facades\Cache;
+
+/**
+ * 微信小程序开发组
+ * class MiniAppService
+ */
+class OfficialService extends EasyModule
+{
+    /**
+     * @var Application
+     */
+    private Application $app;
+    private array       $config;
+
+    /**
+     * 实例化
+     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
+     */
+    public function __construct()
+    {
+        parent::__construct();
+        // 微信开发配置
+        $_table = "EasywechatConfig";
+        $config = Cache::remember("OFFICIAL_SERVICE_{$_table}", $this->ttl, function () use ($_table) {
+            $module = Module::_SetupModule($_table);
+            return $module['official'] ?? [];
+        });
+
+        $config = [
+            'app_id'  => $config['app_id'] ?? '',
+            'secret'  => $config['app_secret'] ?? '',
+            'token'   => $config['token'] ?? '',
+            'aes_key' => $config['aes_key'] ?? '',
+            /**
+             * 接口请求相关配置,超时时间等,具体可用参数请参考:
+             * https://github.com/symfony/symfony/blob/5.3/src/Symfony/Contracts/HttpClient/HttpClientInterface.php
+             */
+            'http'    => [
+                'throw'   => true, // 状态码非 200、300 时是否抛出异常,默认为开启
+                'timeout' => 5.0,
+                'retry'   => true, // 使用默认重试配置
+            ],
+        ];
+
+        $this->config = $config;
+        $this->app    = new Application($config);
+    }
+}

+ 186 - 0
app/Master/Framework/Library/Easywechat/PayService.php

@@ -0,0 +1,186 @@
+<?php
+
+namespace App\Master\Framework\Library\Easywechat;
+
+use EasyWeChat\Pay\Application;
+use EasyWeChat\Kernel\Contracts\Config;
+
+class PayService extends EasyModule
+{
+    protected Application $app;
+    protected Config      $config;
+
+    /**
+     * 实例化
+     */
+    public function __construct()
+    {
+        parent::__construct();
+
+        $this->app    = $this->pay();
+        $this->config = $this->app->getConfig();
+    }
+
+    /**
+     * 支付 server
+     * @return \EasyWeChat\Kernel\Contracts\Server|\EasyWeChat\Pay\Server
+     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
+     * @throws \ReflectionException
+     * @throws \Throwable
+     */
+    public function getServer()
+    {
+        return $this->app->getServer();
+    }
+
+    /**
+     * 统一下单
+     *
+     * @param string $openid
+     * @param string $out_trade_no
+     * @param int $fee
+     * @param string $description
+     * @param string $notify_url
+     * @return bool
+     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
+     * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
+     * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
+     */
+    public function jsapi(string $openid, string $out_trade_no, int $fee = 0, string $description = '', string $notify_url = '')
+    {
+        $app = $this->app;
+
+        $api = $app->getClient();
+
+        $response = $api->postJson("v3/pay/transactions/jsapi", [
+            'mchid'        => (string)$this->config['mch_id'],         // <---- 请修改为您的【子商户号/二级商户号】由微信支付生成并下发。
+            "out_trade_no" => $out_trade_no,                           // 外部商户订单号
+            "appid"        => (string)$this->config['app_id'],         // <---- 请修改为【子商户号/二级商户号】服务号的 appid
+            "description"  => $description,
+            "notify_url"   => $notify_url,// 回调地址
+            "amount"       => [
+                "total"    => $fee,
+                "currency" => "CNY"
+            ],
+            "payer"        => [
+                "openid" => $openid, // <---- 【用户子标识】 用户在子商户AppID下的唯一标识。若传sub_openid,那sub_appid必填。下单前需获取到用户的OpenID
+            ]
+        ]);
+
+        if (!$this->response($response)) {
+            return false;
+        }
+
+        if (empty($this->data['prepay_id'])) {
+            return false;
+        }
+
+        $appId      = (string)$this->config['app_id'];
+        $signType   = 'RSA'; // 默认RSA,v2要传MD5
+        $utils      = $app->getUtils();
+        $this->data = $utils->buildBridgeConfig($this->data['prepay_id'], $appId, $signType); // 返回数组
+
+        return true;
+    }
+
+    /**
+     * App下单
+     * TODO 有待测试
+     * @param string $out_trade_no
+     * @param int $fee
+     * @param string $description
+     * @param string $notify_url
+     * @return bool
+     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
+     * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
+     */
+    public function appPay(string $out_trade_no, int $fee = 0, string $description = '', string $notify_url = '')
+    {
+        $app = $this->app;
+
+        $api = $app->getClient();
+
+        $response = $api->postJson("v3/pay/transactions/app", [
+            "appid"        => $this->config['app_id'], // <---- 请修改为【子商户号/二级商户号】服务号的 appid
+            'mchid'        => $this->config['mch_id'], // <---- 请修改为您的【子商户号/二级商户号】由微信支付生成并下发。
+            "description"  => $description,
+            "out_trade_no" => $out_trade_no,// 外部商户订单号
+            "notify_url"   => $notify_url,  // 回调地址
+            "amount"       => [
+                "total"    => $fee,
+                "currency" => "CNY"
+            ]
+        ])->throw(false);
+
+        if (!$this->response($response)) {
+            return false;
+        }
+
+        if (empty($this->data['prepay_id'])) {
+            return false;
+        }
+
+        $appId      = $this->config['app_id'];
+        $signType   = 'RSA'; // 默认RSA,v2要传MD5
+        $utils      = $app->getUtils();
+        $this->data = $utils->buildBridgeConfig($this->data['prepay_id'], $appId, $signType); // 返回数组
+
+        return true;
+    }
+
+    /**
+     * H5下单
+     * TODO 有待完善
+     * @param string $out_trade_no
+     * @param int $fee
+     * @param string $description
+     * @param string $notify_url
+     * @return bool
+     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
+     * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
+     */
+    public function h5Pay(string $out_trade_no, int $fee = 0, string $description = '', string $notify_url = '')
+    {
+        $app = $this->app;
+
+        $api = $app->getClient();
+
+        $response = $api->postJson("v3/pay/transactions/h5", [
+            "appid"        => $this->config['app_id'], // <---- 请修改为【子商户号/二级商户号】服务号的 appid
+            'mchid'        => $this->config['mch_id'], // <---- 请修改为您的【子商户号/二级商户号】由微信支付生成并下发。
+            "description"  => $description,
+            "out_trade_no" => $out_trade_no,// 外部商户订单号
+            "notify_url"   => $notify_url,  // 回调地址
+            "amount"       => [
+                "total"    => $fee,
+                "currency" => "CNY"
+            ],
+            "scene_info"   => [
+                "payer_client_ip" => $_SERVER['HTTP_X_REAL_IP'] ?? Request::capture()->getClientIp(),
+                ""
+            ]
+        ])->throw(false);
+
+        if (!$this->response($response)) {
+            return false;
+        }
+
+        if (empty($this->data['prepay_id'])) {
+            return false;
+        }
+
+        $appId      = $this->config['app_id'];
+        $signType   = 'RSA'; // 默认RSA,v2要传MD5
+        $utils      = $app->getUtils();
+        $this->data = $utils->buildBridgeConfig($this->data['prepay_id'], $appId, $signType); // 返回数组
+
+        return true;
+    }
+
+    public function handlePaid(callable $function)
+    {
+        if (!$function()){
+            return $this;
+        }
+    }
+}

+ 27 - 0
app/Master/Framework/Library/Extend/Core.php

@@ -0,0 +1,27 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Master\Framework\Library\Extend;
+
+use Hyperf\HttpServer\Contract\RequestInterface;
+
+class Core{
+
+    /**
+     * @var RequestInterface
+     */
+    protected RequestInterface $request;
+
+    public function __construct(RequestInterface $request)
+    {
+        $this->request = $request;
+    }
+
+    public function getLocalUrl(string $path = '')
+    {
+
+        var_dump($this->request->getRequest()->getUri());
+        // 返回完整的本地链接地址
+        return $this->request->getRequest()->getUri()->getScheme() . '://' . $this->request->getRequest()->getUri()->getHost() . '/' . $path;
+    }
+}

+ 23 - 0
app/Master/Framework/Library/Extend/Module.php

@@ -0,0 +1,23 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Master\Framework\Library\Extend;
+
+use App\Model\Framework\SysModulePageModel;
+use App\Model\Framework\SysSetupModel;
+
+class Module{
+    public static function _SetupModule($tables){
+        $value = SysSetupModel::where('tables', $tables)->value('values');
+        return json_decode(
+            $value ?: '{}',
+            true);
+    }
+
+    public static function _ModulePages($tables){
+        $value =  SysModulePageModel::where('tables', $tables)->value('values');
+        return json_decode(
+            $value ?: '{}',
+            true);
+    }
+}

+ 170 - 0
app/Master/Framework/Library/GeTui/Push.php

@@ -0,0 +1,170 @@
+<?php
+
+namespace App\Master\Framework\Library\GeTui;
+
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Master\Framework\Library\Library;
+use App\Utils\Common;
+use App\Utils\RedisUtil;
+
+class Push extends Library
+{
+    protected string $base_uri = "https://restapi.getui.com/v2/";
+    private string   $appId;
+    private string   $appKey;
+    private string   $appSecret;
+    private string   $masterSecret;
+
+    public function __construct()
+    {
+        $this->appId        = site('gt_app_id');
+        $this->appKey       = site('gt_app_key');
+        $this->appSecret    = site('gt_app_secret');
+        $this->masterSecret = site('gt_master_secret');
+    }
+
+    /**
+     * 消息推送
+     * @param string $cid 设备ID
+     * @param string $title 消息标题
+     * @param string $body 消息内容
+     * @param int $type 1=通知,2=透传
+     * @param string $platform 平台
+     * @return bool
+     */
+    public function push(string $cid, string $title, string $body,string $ring_name, int $type = 1, string $platform = 'android')
+    {
+        if (!$access_token = $this->auth()) {
+            return $this->error('ge tui token error');
+        }
+
+        // 通知
+        $notification = [
+            "notification" => [
+                "title"         => $title,
+                "body"          => $body,
+                "ring_name"     => str_replace('.mp3', '', $ring_name),
+                "channel_level" => 4,
+                "channel_id"    => str_replace('.mp3', '', $ring_name),
+                "channel_name"  => str_replace('.mp3', '', $ring_name),
+                "click_type"    => "startapp",//startapp:打开应用首页,payload:自定义消息内容启动应用,
+                "payload"       => json_encode(['t' => time()])
+            ],
+        ];
+
+        // 透传
+        $transmission = [
+            "transmission" => json_encode([
+                "title" => $title,
+                "body"  => $body,
+                "ring_name" => $ring_name,
+                "t"     => time(),
+            ], JSON_UNESCAPED_UNICODE)
+        ];
+
+        switch ($type) {
+            case 1:
+                $push_message = $notification;
+                break;
+            case 2:
+                $push_message = $transmission;
+                break;
+            default:
+                $push_message = $notification;
+        }
+
+        if ($platform == 'ios') {
+            $push_channel = [
+                "ios" => [
+                    "type"       => "notify",
+                    "payload"    => json_encode([
+                        "title" => $title,
+                        "body"  => $body,
+                    ], JSON_UNESCAPED_UNICODE),
+                    "aps"        => [
+                        "alert"             => [
+                            "title" => $title,
+                            "body"  => $body,
+                        ],
+                        "content-available" => 0,
+                        "sound"             => $ring_name,
+                        "category"          => "ACTIONABLE",
+                    ],
+                    "auto_badge" => "+1",
+                ]
+            ];
+            $push_message = $transmission;
+        }
+
+        return $this->send($cid, $push_message, $access_token, $push_channel ?? []);
+    }
+
+    /**
+     * 发送
+     * @param $cid
+     * @param $push_message
+     * @param $access_token
+     * @return bool
+     */
+    private function send($cid, $push_message, $access_token, $push_channel)
+    {
+        $params = [
+            'request_id'   => Common::createNo('GT', 20),
+            'settings'     => [
+                "ttl" => 7200000
+            ],
+            "audience"     => [
+                "cid" => [
+                    $cid
+                ]
+            ],
+            "push_message" => $push_message
+        ];
+
+        if (!empty($push_channel)) {
+            $params['push_channel'] = $push_channel;
+        }
+
+        $response = $this->post('/push/single/cid', $params, [
+            'token' => $access_token
+        ]);
+        if ($response->getStatusCode() != 200) {
+            return $this->error($response->getReasonPhrase());
+        }
+        $json = $response->getBody()->getContents();
+        $body = json_decode($json, true);
+        return $this->success('操作成功', $body);
+    }
+
+    /**
+     * 获取token
+     * @return false|mixed
+     */
+    public function auth()
+    {
+        if ($access_token = RedisUtil::getInstance(RedisKeyEnum::UNI_PUSH_TOKEN)->get()) {
+            return $access_token;
+        }
+        $timestamp = ms_time();
+        $response  = $this->post('/auth', [
+            'sign'      => hash('sha256', "{$this->appKey}{$timestamp}{$this->masterSecret}"),
+            'timestamp' => $timestamp,
+            'appkey'    => $this->appKey
+        ]);
+        if ($response->getStatusCode() != 200) {
+            return false;
+        }
+        $json = $response->getBody()->getContents();
+        $data = json_decode($json, true);
+
+        RedisUtil::getInstance(RedisKeyEnum::UNI_PUSH_TOKEN)->setex($data['data']['token'], 7000);
+
+        return $data['data']['token'];
+    }
+
+    private function post(string $uri, array $params = [], array $header = [])
+    {
+        return $this->postJson($this->appId.$uri,$params,$header);
+    }
+}

+ 265 - 0
app/Master/Framework/Library/Google/Maps.php

@@ -0,0 +1,265 @@
+<?php
+
+namespace App\Master\Framework\Library\Google;
+
+use _PHPStan_579402b64\Nette\Utils\DateTime;
+use App\Master\Framework\Library\Library;
+
+class Maps extends Library
+{
+    public string  $base_uri = '';// 请求域名地址
+    private string $key;
+
+    /**
+     * 实例化
+     */
+    public function __construct()
+    {
+        // 获取配置信息
+        $this->key = (string)site('google_map_api_key');
+    }
+
+    /**
+     * 计算两地距离,行驶时长等
+     * @param float $start_lng 经度
+     * @param float $start_lat 纬度
+     * @param float $end_lng
+     * @param float $end_lat
+     * @param int $departureTime
+     * @param float $point_lng 途径点 经度
+     * @param float $point_lat 途径点 纬度
+     * @return bool
+     */
+    public function computeRoutes(float $start_lng, float $start_lat, float $end_lng, float $end_lat, int $departureTime = 0, float $point_lng = 0, float $point_lat = 0): bool
+    {
+//        $departureTime = $this->gmt_rfc3339($departureTime == 0 ? time() : $departureTime);
+
+        $data = [
+            'origin'                   => [
+                'location' => [
+                    'latLng' => [
+                        'latitude'  => $start_lat,
+                        'longitude' => $start_lng
+                    ]
+                ]
+            ],
+            'destination'              => [
+                'location' => [
+                    'latLng' => [
+                        'latitude'  => $end_lat,
+                        'longitude' => $end_lng
+                    ]
+                ]
+            ],
+            'travelMode'               => 'DRIVE',// 方式 DRIVE 驾车
+            "routingPreference"        => "TRAFFIC_AWARE",
+            //"departureTime"            => $departureTime,// 出发时间 "2024-07-09T21:01:23Z"
+            "computeAlternativeRoutes" => false,// 备选路线
+            "routeModifiers"           => [
+                "avoidTolls"    => true,
+                "avoidHighways" => false,
+                "avoidFerries"  => false
+            ],
+            "languageCode"             => "en-US",
+            "units"                    => "IMPERIAL"
+        ];
+        if (!empty($point_lng) && !empty($point_lat)) {
+            $data['intermediates'] = [
+                [
+                    'location' => [
+                        'latLng' => [
+                            'latitude'  => $point_lat,
+                            'longitude' => $point_lng
+                        ]
+                    ]
+                ]
+            ];
+        }
+        $header         = [
+            'X-Goog-FieldMask' => 'routes.duration,routes.distanceMeters'
+        ];
+        $this->base_uri = 'https://routes.googleapis.com';
+        $response       = $this->post('/directions/v2:computeRoutes', $data, $header);
+        if ($response->getStatusCode() != 200) {
+            return $this->error($response->getReasonPhrase());
+        }
+
+        $json = $response->getBody()->getContents();
+        $body = json_decode($json, true);
+
+        return $this->success('获取成功', $body);
+    }
+
+    /**
+     * 地址搜索
+     * @param string $input
+     * @return bool
+     */
+    public function place_search(string $input)
+    {
+        $data     = [
+            'fields'    => 'formatted_address,name,geometry,place_id',
+            'inputtype' => 'textquery',
+            'input'     => $input,
+            'sensor'    => false,
+            'key'       => $this->key
+        ];
+        $response = $this->get('https://maps.googleapis.com/maps/api/place/findplacefromtext/json', $data);
+        if ($response->getStatusCode() != 200) {
+            return $this->error($response->getReasonPhrase());
+        }
+
+        $json = $response->getBody()->getContents();
+        $body = json_decode($json, true);
+        if (!isset($body['status']) || $body['status'] != 'OK') {
+            return $this->error($body['error_message'] ?? '获取失败', $body);
+        }
+
+        return $this->success('获取成功', $body);
+    }
+
+    /**
+     * 地址搜索(自动补全)
+     * @param string $input
+     * @param string $latitude
+     * @param string $longitude
+     * @param string $sessionToken
+     * @return bool
+     */
+    public function place_auto_search(string $input, string $latitude, string $longitude, string $sessionToken = '')
+    {
+        $data     = [
+            'key'          => $this->key,
+            'input'        => $input,
+            'components'   => 'country:ca',
+            'location'     => "{$latitude},{$longitude}",
+            'sessiontoken' => $sessionToken,
+            'language' => 'zh'
+        ];
+        $response = $this->get('https://maps.googleapis.com/maps/api/place/autocomplete/json', $data);
+        if ($response->getStatusCode() != 200) {
+            return $this->error($response->getReasonPhrase());
+        }
+
+        $json = $response->getBody()->getContents();
+        $body = json_decode($json, true);
+        if (!isset($body['status']) || $body['status'] != 'OK') {
+            return $this->error($body['error_message'] ?? '获取失败', $body);
+        }
+
+        return $this->success('获取成功', $body);
+    }
+
+    /**
+     * 地址详情接口
+     * @param string $place_id
+     * @return bool
+     */
+    public function place_details(string $place_id)
+    {
+        $data     = [
+            'fields'   => 'address_components,geometry',
+            'place_id' => $place_id,
+            'key'      => $this->key
+        ];
+        $response = $this->get('https://maps.googleapis.com/maps/api/place/details/json', $data);
+        if ($response->getStatusCode() != 200) {
+            return $this->error($response->getReasonPhrase());
+        }
+
+        $json = $response->getBody()->getContents();
+        $body = json_decode($json, true);
+        if (!isset($body['status']) || $body['status'] != 'OK') {
+            return $this->error($body['error_message'] ?? '获取失败', $body);
+        }
+
+        return $this->success('获取成功', $body);
+    }
+
+    /**
+     * 地址搜索
+     * @param string $latlng
+     * @return bool
+     */
+    public function geocode(string $latlng)
+    {
+        $data     = [
+            'latlng' => $latlng,
+            'sensor' => false,
+            'key'    => $this->key
+        ];
+        $response = $this->get('https://maps.google.com/maps/api/geocode/json', $data);
+        if ($response->getStatusCode() != 200) {
+            return $this->error($response->getReasonPhrase());
+        }
+
+        $json = $response->getBody()->getContents();
+        $body = json_decode($json, true);
+        if (!isset($body['status']) || $body['status'] != 'OK') {
+            return $this->error($body['error_message'] ?? '获取失败', $body);
+        }
+
+        return $this->success('获取成功', $body);
+    }
+
+    /**
+     * 路线规划
+     * @param string $destination
+     * @param string $origin
+     * @param string $waypoints
+     * @return bool
+     */
+    public function directions(string $destination, string $origin, string $waypoints = '')
+    {
+        $data     = [
+            'destination' => $destination,
+            'origin'      => $origin,
+            'waypoints'   => $waypoints,
+            'avoid'       => 'tolls',
+            'key'         => $this->key
+        ];
+        $response = $this->get('https://maps.googleapis.com/maps/api/directions/json', $data);
+        if ($response->getStatusCode() != 200) {
+            return $this->error($response->getReasonPhrase());
+        }
+
+        $json = $response->getBody()->getContents();
+        $body = json_decode($json, true);
+        if (!isset($body['status']) || $body['status'] != 'OK') {
+            return $this->error($body['error_message'] ?? '获取失败', $body);
+        }
+
+        return $this->success('获取成功', $body);
+    }
+
+    private function post(string $uri, array $params = [], array $header = [])
+    {
+        $header['X-Goog-Api-Key'] = $this->key;
+        return $this->postJson($uri, $params, $header);
+    }
+
+    private function get(string $uri, array $params = [], array $header = [])
+    {
+        return $this->getJson($uri, $params, $header);
+    }
+
+    private function gmt_iso8601($time)
+    {
+        $dtStr      = date("c", $time);
+        $dateTime   = new \DateTime($dtStr);
+        $expiration = $dateTime->format(\DateTime::ISO8601);
+        $pos        = strpos($expiration, '+');
+        $expiration = substr($expiration, 0, $pos);
+        return $expiration . "Z";
+    }
+
+    private function gmt_rfc3339($time)
+    {
+        $dateTime   = new \DateTime();
+        $dateTime->setTimestamp($time);
+        $expiration = $dateTime->format(\DateTime::ATOM);
+        $pos        = strrpos($expiration, '-');
+        $expiration = substr($expiration, 0, $pos);
+        return $expiration . "Z";
+    }
+}

+ 126 - 0
app/Master/Framework/Library/Library.php

@@ -0,0 +1,126 @@
+<?php
+
+namespace App\Master\Framework\Library;
+
+use GuzzleHttp\Client;
+use GuzzleHttp\RequestOptions;
+use Hyperf\Guzzle\HandlerStackFactory;
+
+/**
+ * 扩展返回工具
+ */
+class Library
+{
+    protected string $base_uri = 'http://127.0.0.1:9501';
+    protected string $message  = 'error';
+    protected mixed  $data     = [];
+
+    /**
+     * post json 请求
+     *
+     * @param string $uri
+     * @param array $params
+     * @param array $header
+     * @return \Psr\Http\Message\ResponseInterface
+     * @throws \GuzzleHttp\Exception\GuzzleException
+     */
+    protected function postJson(string $uri, array $params = [], array $header = [])
+    {
+        $factory = new HandlerStackFactory();
+        $stack   = $factory->create();
+        $client  = new Client([
+            // guzzle http里的配置信息
+            'base_uri' => $this->base_uri,
+            'handler'  => $stack,
+            'timeout'  => 5,
+            // swoole的配置信息,内容会覆盖guzzle http里的配置信息
+            'swoole'   => [
+                'timeout'            => 10,
+                'socket_buffer_size' => 1024 * 1024 * 2,
+            ],
+        ]);
+
+        return $client->post($uri, [
+            RequestOptions::JSON    => $params,
+            RequestOptions::VERIFY  => false,
+            RequestOptions::HEADERS => array_merge(
+                $header,
+                [
+                    'Content-Type' => 'application/json'
+                ]
+            ),
+        ]);
+    }
+
+    protected function getJson(string $uri, array $params = [], array $header = [])
+    {
+        $factory = new HandlerStackFactory();
+        $stack   = $factory->create();
+        $client  = new Client([
+            // guzzle http里的配置信息
+            'base_uri' => $this->base_uri,
+            'handler'  => $stack,
+            'timeout'  => 5,
+            // swoole的配置信息,内容会覆盖guzzle http里的配置信息
+            'swoole'   => [
+                'timeout'            => 10,
+                'socket_buffer_size' => 1024 * 1024 * 2,
+            ],
+        ]);
+
+        return $client->get($uri, [
+            RequestOptions::QUERY   => $params,
+            RequestOptions::VERIFY  => false,
+            RequestOptions::HEADERS => array_merge(
+                $header,
+                [
+                    'Content-Type' => 'application/json'
+                ]
+            ),
+        ]);
+    }
+
+    /**
+     * 返回成功结果
+     * @param string $message
+     * @param mixed $data
+     * @return bool
+     */
+    protected function success(string $message = 'success', mixed $data = []): bool
+    {
+        $this->message = $message;
+        $this->data    = $data;
+        return true;
+    }
+
+    /**
+     * 返回失败结果
+     * @param string $message
+     * @param mixed $data
+     * @return bool
+     */
+    protected function error(string $message = 'error', mixed $data = []): bool
+    {
+        $this->message = $message;
+        $this->data    = $data;
+        return false;
+    }
+
+    /**
+     * 获取成功数据
+     * @return mixed
+     */
+    public function getData(): mixed
+    {
+        return $this->data;
+    }
+
+    /**
+     * 获取消息
+     * @return string
+     */
+    public function getMessage(): string
+    {
+        return $this->message;
+    }
+}

+ 119 - 0
app/Master/Framework/Library/Mqtt/MqttClient.php

@@ -0,0 +1,119 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Master\Framework\Library\Mqtt;
+
+use Hyperf\Config\Annotation\Value;
+use Simps\MQTT\Client;
+use Simps\MQTT\Config\ClientConfig;
+
+class MqttClient
+{
+    #[Value("mqtt.client")]
+    protected $config;
+    protected Client $client;
+
+    /**
+     * @param bool $clean 清理会话,默认为 true
+     * @param array $will 遗嘱消息,当客户端断线后 Broker 会自动发送遗嘱消息给其它客户端
+     */
+    public function __construct(bool $clean = true, array $will = [])
+    {
+        $config = $this->config;
+        $configObj = new ClientConfig($config['pool']);
+        $this->client = new Client($config['host'], $config['port'], $configObj);
+        $this->client->connect($clean,$will);
+    }
+
+    /**
+     * 向某个主题发布一条消息
+     *
+     * @param string $topic 主题
+     * @param mixed $data 内容
+     * @param int $qos QoS 等级,默认 0
+     * @param int $dup 重发标志,默认 0
+     * @param int $retain retain 标记,默认 0
+     * @param array $properties 属性,MQTT5 中需要,可选
+     * @return array|bool
+     */
+    public function publish(string $topic, mixed $data, int $qos = 0, int $dup = 0, int $retain = 0, array $properties = [])
+    {
+        $message = is_array($data) ? json_encode($data) : $data;
+        return $this->client->publish($topic, $message, $qos, $dup, $retain, $properties);
+    }
+
+    /**
+     * 订阅一个主题或者多个主题
+     *
+     * @param array $topic $topics 的 key 是主题,值为 QoS 的数组,例如
+     * @param array $properties 属性,MQTT5 中需要,可选
+     * // MQTT 3.x
+     *   $topics = [
+     *       // 主题 => Qos
+     *       'topic1' => 0,
+     *       'topic2' => 1,
+     *   ];
+     *
+     *   // MQTT 5.0
+     *   $topics = [
+     *       // 主题 => 选项
+     *       'topic1' => [
+     *           'qos' => 1,
+     *           'no_local' => true,
+     *           'retain_as_published' => true,
+     *           'retain_handling' => 2,
+     *       ],
+     *       'topic2' => [
+     *           'qos' => 2,
+     *           'no_local' => false,
+     *           'retain_as_published' => true,
+     *           'retain_handling' => 1,
+     *       ],
+     *   ];
+     * @return array|bool
+     */
+    public function subscribe(array $topic, array $properties = [])
+    {
+        return $this->client->subscribe($topic, $properties);
+    }
+
+    /**
+     * 接收消息
+     *
+     * @return array|true
+     */
+    public function recv()
+    {
+        return $this->client->recv();
+    }
+
+    /**
+     * 发送消息
+     *
+     * @param array $data
+     * @param $response
+     * @return array|true
+     */
+    public function send(array $data, $response = false)
+    {
+        return $this->client->send($data,$response);
+    }
+
+    /**
+     * 发送心跳包
+     *
+     * @return array|bool
+     */
+    public function ping()
+    {
+        return $this->client->ping();
+    }
+
+    /**
+     * @return int
+     */
+    public function getKeepAlive()
+    {
+        return $this->client->getConfig()->getKeepAlive();
+    }
+}

+ 38 - 0
app/Master/Framework/Library/Mqtt/Subscribe.php

@@ -0,0 +1,38 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Master\Framework\Library\Mqtt;
+
+use Hyperf\Coroutine\Coroutine;
+
+class Subscribe{
+    /**
+     * MQTT 订阅
+     * @param array $topic
+     * @param $function
+     * @return mixed
+     */
+    public function endlessLoop(array $topic,$function): mixed
+    {
+        $cline = new MqttClient();
+        // 订阅一个主题或者多个主题
+        $cline->subscribe($topic);
+
+        //时间
+        $timeSincePing = time();
+
+        while (true){
+
+            $function($cline,$topic);
+
+            //心跳
+            if ($timeSincePing <= (time() - $cline->getKeepAlive())){
+                if ($cline->ping()) {
+                    $timeSincePing = time();
+                }
+            }
+
+            Coroutine::sleep(0.01);
+        }
+    }
+}

+ 328 - 0
app/Master/Framework/Library/Tencent/GetUserSig.php

@@ -0,0 +1,328 @@
+<?php
+namespace App\Master\Framework\Library\Tencent;
+
+class GetUserSig {
+
+    private $key = false;
+    private $sdkappid = 0;
+
+    /**
+     *【功能说明】用于签发 TRTC 和 IM 服务中必须要使用的 UserSig 鉴权票据
+     *
+     *【参数说明】
+     * @param string userid - 用户id,限制长度为32字节,只允许包含大小写英文字母(a-zA-Z)、数字(0-9)及下划线和连词符。
+     * @param string expire - UserSig 票据的过期时间,单位是秒,比如 86400 代表生成的 UserSig 票据在一天后就无法再使用了。
+     * @return string 签名字符串
+     * @throws \Exception
+     */
+
+    public function genUserSig( $userid, $expire = 864000 ) {
+        return $this->__genSig( $userid, $expire, '', false );
+    }
+
+    /**
+     *【功能说明】
+     * 用于签发 TRTC 进房参数中可选的 PrivateMapKey 权限票据。
+     * PrivateMapKey 需要跟 UserSig 一起使用,但 PrivateMapKey 比 UserSig 有更强的权限控制能力:
+     *  - UserSig 只能控制某个 UserID 有无使用 TRTC 服务的权限,只要 UserSig 正确,其对应的 UserID 可以进出任意房间。
+     *  - PrivateMapKey 则是将 UserID 的权限控制的更加严格,包括能不能进入某个房间,能不能在该房间里上行音视频等等。
+     * 如果要开启 PrivateMapKey 严格权限位校验,需要在【实时音视频控制台】=>【应用管理】=>【应用信息】中打开“启动权限密钥”开关。
+     *
+     *【参数说明】
+     * @param userid - 用户id,限制长度为32字节,只允许包含大小写英文字母(a-zA-Z)、数字(0-9)及下划线和连词符。
+     * @param expire - PrivateMapKey 票据的过期时间,单位是秒,比如 86400 生成的 PrivateMapKey 票据在一天后就无法再使用了。
+     * @param roomid - 房间号,用于指定该 userid 可以进入的房间号
+     * @param privilegeMap - 权限位,使用了一个字节中的 8 个比特位,分别代表八个具体的功能权限开关:
+     *  - 第 1 位:0000 0001 = 1,创建房间的权限
+     *  - 第 2 位:0000 0010 = 2,加入房间的权限
+     *  - 第 3 位:0000 0100 = 4,发送语音的权限
+     *  - 第 4 位:0000 1000 = 8,接收语音的权限
+     *  - 第 5 位:0001 0000 = 16,发送视频的权限
+     *  - 第 6 位:0010 0000 = 32,接收视频的权限
+     *  - 第 7 位:0100 0000 = 64,发送辅路(也就是屏幕分享)视频的权限
+     *  - 第 8 位:1000 0000 = 200,接收辅路(也就是屏幕分享)视频的权限
+     *  - privilegeMap == 1111 1111 == 255 代表该 userid 在该 roomid 房间内的所有功能权限。
+     *  - privilegeMap == 0010 1010 == 42  代表该 userid 拥有加入房间和接收音视频数据的权限,但不具备其他权限。
+     */
+
+    public function genPrivateMapKey( $userid, $expire, $roomid, $privilegeMap ) {
+        $userbuf = $this->__genUserBuf( $userid, $roomid, $expire, $privilegeMap, 0, '' );
+        return $this->__genSig( $userid, $expire, $userbuf, true );
+    }
+    /**
+     *【功能说明】
+     * 用于签发 TRTC 进房参数中可选的 PrivateMapKey 权限票据。
+     * PrivateMapKey 需要跟 UserSig 一起使用,但 PrivateMapKey 比 UserSig 有更强的权限控制能力:
+     *  - UserSig 只能控制某个 UserID 有无使用 TRTC 服务的权限,只要 UserSig 正确,其对应的 UserID 可以进出任意房间。
+     *  - PrivateMapKey 则是将 UserID 的权限控制的更加严格,包括能不能进入某个房间,能不能在该房间里上行音视频等等。
+     * 如果要开启 PrivateMapKey 严格权限位校验,需要在【实时音视频控制台】=>【应用管理】=>【应用信息】中打开“启动权限密钥”开关。
+     *
+     *【参数说明】
+     * @param userid - 用户id,限制长度为32字节,只允许包含大小写英文字母(a-zA-Z)、数字(0-9)及下划线和连词符。
+     * @param expire - PrivateMapKey 票据的过期时间,单位是秒,比如 86400 生成的 PrivateMapKey 票据在一天后就无法再使用了。
+     * @param roomstr - 房间号,用于指定该 userid 可以进入的房间号
+     * @param privilegeMap - 权限位,使用了一个字节中的 8 个比特位,分别代表八个具体的功能权限开关:
+     *  - 第 1 位:0000 0001 = 1,创建房间的权限
+     *  - 第 2 位:0000 0010 = 2,加入房间的权限
+     *  - 第 3 位:0000 0100 = 4,发送语音的权限
+     *  - 第 4 位:0000 1000 = 8,接收语音的权限
+     *  - 第 5 位:0001 0000 = 16,发送视频的权限
+     *  - 第 6 位:0010 0000 = 32,接收视频的权限
+     *  - 第 7 位:0100 0000 = 64,发送辅路(也就是屏幕分享)视频的权限
+     *  - 第 8 位:1000 0000 = 200,接收辅路(也就是屏幕分享)视频的权限
+     *  - privilegeMap == 1111 1111 == 255 代表该 userid 在该 roomid 房间内的所有功能权限。
+     *  - privilegeMap == 0010 1010 == 42  代表该 userid 拥有加入房间和接收音视频数据的权限,但不具备其他权限。
+     */
+
+    public function genPrivateMapKeyWithStringRoomID( $userid, $expire, $roomstr, $privilegeMap ) {
+        $userbuf = $this->__genUserBuf( $userid, 0, $expire, $privilegeMap, 0, $roomstr );
+        return $this->__genSig( $userid, $expire, $userbuf, true );
+    }
+
+    public function __construct( $sdkappid, $key ) {
+        $this->sdkappid = $sdkappid;
+        $this->key = $key;
+    }
+
+    /**
+     * 用于 url 的 base64 encode
+     * '+' => '*', '/' => '-', '=' => '_'
+     * @param string $string 需要编码的数据
+     * @return string 编码后的base64串,失败返回false
+     * @throws \Exception
+     */
+
+    private function base64_url_encode( $string ) {
+        static $replace = Array( '+' => '*', '/' => '-', '=' => '_' );
+        $base64 = base64_encode( $string );
+        if ( $base64 === false ) {
+            throw new \Exception( 'base64_encode error' );
+        }
+        return str_replace( array_keys( $replace ), array_values( $replace ), $base64 );
+    }
+
+    /**
+     * 用于 url 的 base64 decode
+     * '+' => '*', '/' => '-', '=' => '_'
+     * @param string $base64 需要解码的base64串
+     * @return string 解码后的数据,失败返回false
+     * @throws \Exception
+     */
+
+    private function base64_url_decode( $base64 ) {
+        static $replace = Array( '+' => '*', '/' => '-', '=' => '_' );
+        $string = str_replace( array_values( $replace ), array_keys( $replace ), $base64 );
+        $result = base64_decode( $string );
+        if ( $result == false ) {
+            throw new \Exception( 'base64_url_decode error' );
+        }
+        return $result;
+    }
+    /**
+     * TRTC业务进房权限加密串使用用户定义的userbuf
+     * @brief 生成 userbuf
+     * @param account 用户名
+     * @param dwSdkappid sdkappid
+     * @param dwAuthID  数字房间号
+     * @param dwExpTime 过期时间:该权限加密串的过期时间. 过期时间 = now+dwExpTime
+     * @param dwPrivilegeMap 用户权限,255表示所有权限
+     * @param dwAccountType 用户类型, 默认为0
+     * @param roomStr 字符串房间号
+     * @return userbuf string  返回的userbuf
+     */
+
+    private function __genUserBuf( $account, $dwAuthID, $dwExpTime, $dwPrivilegeMap, $dwAccountType,$roomStr ) {
+
+        //cVer  unsigned char/1 版本号,填0
+        if($roomStr == '')
+            $userbuf = pack( 'C1', '0' );
+        else
+            $userbuf = pack( 'C1', '1' );
+
+        $userbuf .= pack( 'n', strlen( $account ) );
+        //wAccountLen   unsigned short /2   第三方自己的帐号长度
+        $userbuf .= pack( 'a'.strlen( $account ), $account );
+        //buffAccount   wAccountLen 第三方自己的帐号字符
+        $userbuf .= pack( 'N', $this->sdkappid );
+        //dwSdkAppid    unsigned int/4  sdkappid
+        $userbuf .= pack( 'N', $dwAuthID );
+        //dwAuthId  unsigned int/4  群组号码/音视频房间号
+        $expire = $dwExpTime + time();
+        $userbuf .= pack( 'N', $expire );
+        //dwExpTime unsigned int/4  过期时间 (当前时间 + 有效期(单位:秒,建议300秒))
+        $userbuf .= pack( 'N', $dwPrivilegeMap );
+        //dwPrivilegeMap unsigned int/4  权限位
+        $userbuf .= pack( 'N', $dwAccountType );
+        //dwAccountType  unsigned int/4
+        if($roomStr != '')
+        {
+            $userbuf .= pack( 'n', strlen( $roomStr ) );
+            //roomStrLen   unsigned short /2   字符串房间号长度
+            $userbuf .= pack( 'a'.strlen( $roomStr ), $roomStr );
+            //roomStr   roomStrLen 字符串房间号
+        }
+        return $userbuf;
+    }
+    /**
+     * 使用 hmac sha256 生成 sig 字段内容,经过 base64 编码
+     * @param $identifier 用户名,utf-8 编码
+     * @param $curr_time 当前生成 sig 的 unix 时间戳
+     * @param $expire 有效期,单位秒
+     * @param $base64_userbuf base64 编码后的 userbuf
+     * @param $userbuf_enabled 是否开启 userbuf
+     * @return string base64 后的 sig
+     */
+
+    private function hmacsha256( $identifier, $curr_time, $expire, $base64_userbuf, $userbuf_enabled ) {
+        $content_to_be_signed = 'TLS.identifier:' . $identifier . "\n"
+            . 'TLS.sdkappid:' . $this->sdkappid . "\n"
+            . 'TLS.time:' . $curr_time . "\n"
+            . 'TLS.expire:' . $expire . "\n";
+        if ( true == $userbuf_enabled ) {
+            $content_to_be_signed .= 'TLS.userbuf:' . $base64_userbuf . "\n";
+        }
+        return base64_encode( hash_hmac( 'sha256', $content_to_be_signed, $this->key, true ) );
+    }
+
+    /**
+     * 生成签名。
+     *
+     * @param $identifier 用户账号
+     * @param int $expire 过期时间,单位秒,默认 180 天
+     * @param $userbuf base64 编码后的 userbuf
+     * @param $userbuf_enabled 是否开启 userbuf
+     * @return string 签名字符串
+     * @throws \Exception
+     */
+
+    private function __genSig( $identifier, $expire, $userbuf, $userbuf_enabled ) {
+        $curr_time = time();
+        $sig_array = Array(
+            'TLS.ver' => '2.0',
+            'TLS.identifier' => strval( $identifier ),
+            'TLS.sdkappid' => intval( $this->sdkappid ),
+            'TLS.expire' => intval( $expire ),
+            'TLS.time' => intval( $curr_time )
+        );
+
+        $base64_userbuf = '';
+        if ( true == $userbuf_enabled ) {
+            $base64_userbuf = base64_encode( $userbuf );
+            $sig_array['TLS.userbuf'] = strval( $base64_userbuf );
+        }
+
+        $sig_array['TLS.sig'] = $this->hmacsha256( $identifier, $curr_time, $expire, $base64_userbuf, $userbuf_enabled );
+        if ( $sig_array['TLS.sig'] === false ) {
+            throw new \Exception( 'base64_encode error' );
+        }
+        $json_str_sig = json_encode( $sig_array );
+        if ( $json_str_sig === false ) {
+            throw new \Exception( 'json_encode error' );
+        }
+        $compressed = gzcompress( $json_str_sig );
+        if ( $compressed === false ) {
+            throw new \Exception( 'gzcompress error' );
+        }
+        return $this->base64_url_encode( $compressed );
+    }
+
+    /**
+     * 验证签名。
+     *
+     * @param string $sig 签名内容
+     * @param string $identifier 需要验证用户名,utf-8 编码
+     * @param int $init_time 返回的生成时间,unix 时间戳
+     * @param int $expire_time 返回的有效期,单位秒
+     * @param string $userbuf 返回的用户数据
+     * @param string $error_msg 失败时的错误信息
+     * @return boolean 验证是否成功
+     * @throws \Exception
+     */
+
+    private function __verifySig( $sig, $identifier, &$init_time, &$expire_time, &$userbuf, &$error_msg ) {
+        try {
+            $error_msg = '';
+            $compressed_sig = $this->base64_url_decode( $sig );
+            $pre_level = error_reporting( E_ERROR );
+            $uncompressed_sig = gzuncompress( $compressed_sig );
+            error_reporting( $pre_level );
+            if ( $uncompressed_sig === false ) {
+                throw new \Exception( 'gzuncompress error' );
+            }
+            $sig_doc = json_decode( $uncompressed_sig );
+            if ( $sig_doc == false ) {
+                throw new \Exception( 'json_decode error' );
+            }
+            $sig_doc = ( array )$sig_doc;
+            if ( $sig_doc['TLS.identifier'] !== $identifier ) {
+                throw new \Exception( "identifier dosen't match" );
+            }
+            if ( $sig_doc['TLS.sdkappid'] != $this->sdkappid ) {
+                throw new \Exception( "sdkappid dosen't match" );
+            }
+            $sig = $sig_doc['TLS.sig'];
+            if ( $sig == false ) {
+                throw new \Exception( 'sig field is missing' );
+            }
+
+            $init_time = $sig_doc['TLS.time'];
+            $expire_time = $sig_doc['TLS.expire'];
+
+            $curr_time = time();
+            if ( $curr_time > $init_time+$expire_time ) {
+                throw new \Exception( 'sig expired' );
+            }
+
+            $userbuf_enabled = false;
+            $base64_userbuf = '';
+            if ( isset( $sig_doc['TLS.userbuf'] ) ) {
+                $base64_userbuf = $sig_doc['TLS.userbuf'];
+                $userbuf = base64_decode( $base64_userbuf );
+                $userbuf_enabled = true;
+            }
+            $sigCalculated = $this->hmacsha256( $identifier, $init_time, $expire_time, $base64_userbuf, $userbuf_enabled );
+
+            if ( $sig != $sigCalculated ) {
+                throw new \Exception( 'verify failed' );
+            }
+
+            return true;
+        } catch ( \Exception $ex ) {
+            $error_msg = $ex->getMessage();
+            return false;
+        }
+    }
+
+    /**
+     * 带 userbuf 验证签名。
+     *
+     * @param string $sig 签名内容
+     * @param string $identifier 需要验证用户名,utf-8 编码
+     * @param int $init_time 返回的生成时间,unix 时间戳
+     * @param int $expire_time 返回的有效期,单位秒
+     * @param string $error_msg 失败时的错误信息
+     * @return boolean 验证是否成功
+     * @throws \Exception
+     */
+
+    public function verifySig( $sig, $identifier, &$init_time, &$expire_time, &$error_msg ) {
+        $userbuf = '';
+        return $this->__verifySig( $sig, $identifier, $init_time, $expire_time, $userbuf, $error_msg );
+    }
+
+    /**
+     * 验证签名
+     * @param string $sig 签名内容
+     * @param string $identifier 需要验证用户名,utf-8 编码
+     * @param int $init_time 返回的生成时间,unix 时间戳
+     * @param int $expire_time 返回的有效期,单位秒
+     * @param string $userbuf 返回的用户数据
+     * @param string $error_msg 失败时的错误信息
+     * @return boolean 验证是否成功
+     * @throws \Exception
+     */
+
+    public function verifySigWithUserBuf( $sig, $identifier, &$init_time, &$expire_time, &$userbuf, &$error_msg ) {
+        return $this->__verifySig( $sig, $identifier, $init_time, $expire_time, $userbuf, $error_msg );
+    }
+}

+ 170 - 0
app/Master/Framework/Library/Tencent/TencentIm.php

@@ -0,0 +1,170 @@
+<?php
+
+namespace App\Master\Framework\Library\Tencent;
+
+use App\Master\Framework\Library\Library;
+use function Hyperf\Config\config;
+
+class TencentIm extends Library
+{
+    public string $base_uri = "https://console.tim.qq.com";
+    private mixed $config;
+
+    /**
+     * 实例化
+     */
+    public function __construct()
+    {
+        // 获取配置信息
+        $this->config = [
+            'appid'      => site('tencent_im_appid'),
+            'identifier' => site('tencent_im_identifier'),
+            'key'        => site('tencent_im_key'),
+        ];
+    }
+
+    /**
+     * 获取直播群在线人数
+     * @param int $room_no
+     * @param array $counter
+     * @return bool
+     */
+    public function get_online_member_num(int $room_no)
+    {
+        $data     = [
+            'GroupId' => (string)$room_no
+        ];
+        $response = $this->post('/v4/group_open_http_svc/get_online_member_num', $data);
+        if ($response->getStatusCode() != 200) {
+            return $this->error($response->getReasonPhrase());
+        }
+
+        $json = $response->getBody()->getContents();
+        $body = json_decode($json, true);
+        if (empty($body['ActionStatus']) || $body['ActionStatus'] != 'OK') {
+            return $this->error(!empty($body['ErrorInfo']) ? $body['ErrorInfo'] : 'im error', $body ?? []);
+        }
+
+        return $this->success('获取成功', $body);
+    }
+
+    /**
+     * 设置计数器
+     * @param int $room_no
+     * @param array $counter
+     * @return bool
+     */
+    public function update_group_counter(int $room_no, array $counter)
+    {
+        $data     = [
+            'GroupId'      => (string)$room_no,// 群组 ID
+            'GroupCounter' => $counter,
+            'Mode'         => 'Set'
+        ];
+        $response = $this->post('/v4/group_open_http_svc/update_group_counter', $data);
+        if ($response->getStatusCode() != 200) {
+            return $this->error($response->getReasonPhrase());
+        }
+
+        $json = $response->getBody()->getContents();
+        $body = json_decode($json, true);
+        if (empty($body['ActionStatus']) || $body['ActionStatus'] != 'OK') {
+            return $this->error(!empty($body['ErrorInfo']) ? $body['ErrorInfo'] : 'im error', $body ?? []);
+        }
+
+        return $this->success('获取成功', $body);
+    }
+
+    /**
+     * 设置群属性
+     * @param int $room_no
+     * @param array $attr
+     * @return bool
+     */
+    public function modify_group_attr(int $room_no, array $attr)
+    {
+        $data     = [
+            'GroupId'   => (string)$room_no,// 群组 ID
+            'GroupAttr' => $attr
+        ];
+        $response = $this->post('/v4/group_open_http_svc/modify_group_attr', $data);
+        if ($response->getStatusCode() != 200) {
+            return $this->error($response->getReasonPhrase());
+        }
+
+        $json = $response->getBody()->getContents();
+        $body = json_decode($json, true);
+        if (empty($body['ActionStatus']) || $body['ActionStatus'] != 'OK') {
+            return $this->error(!empty($body['ErrorInfo']) ? $body['ErrorInfo'] : 'im error', $body ?? []);
+        }
+
+        return $this->success('获取成功', $body);
+    }
+
+    /**
+     * 获取全部群组
+     * @return bool
+     */
+    public function get_appid_group_list()
+    {
+        $data     = [
+            'Limit' => 1000,// 群主 ID
+            'Next'  => 0
+        ];
+        $response = $this->post('/v4/group_open_http_svc/get_appid_group_list', $data);
+        if ($response->getStatusCode() != 200) {
+            return $this->error($response->getReasonPhrase());
+        }
+
+        $json = $response->getBody()->getContents();
+        $body = json_decode($json, true);
+        if (empty($body['ActionStatus']) || $body['ActionStatus'] != 'OK') {
+            return $this->error(!empty($body['ErrorInfo']) ? $body['ErrorInfo'] : 'im error', $body ?? []);
+        }
+
+        return $this->success('获取成功', $body);
+    }
+
+    /**
+     * 获取全部群组
+     * @return bool
+     */
+    public function delete_chat(string $user_id,string $to_user_id)
+    {
+        $data     = [
+            'From_Account' => $user_id,// 用户 ID
+            'Type'  => 1,
+            'To_Account'  => $to_user_id,
+            'ClearRamble'  => 1,
+        ];
+        $response = $this->post('/v4/recentcontact/delete', $data);
+        if ($response->getStatusCode() != 200) {
+            return $this->error($response->getReasonPhrase());
+        }
+
+        $json = $response->getBody()->getContents();
+        $body = json_decode($json, true);
+        if (empty($body['ActionStatus']) || $body['ActionStatus'] != 'OK') {
+            return $this->error(!empty($body['ErrorInfo']) ? $body['ErrorInfo'] : 'im error', $body ?? []);
+        }
+
+        return $this->success('获取成功', $body);
+    }
+
+    private function post(string $uri, array $params = [])
+    {
+        $random  = rand(10000000, 99999999);
+        $userSig = $this->usersig($this->config['identifier']);
+        return $this->postJson("{$uri}?sdkappid={$this->config['appid']}&identifier={$this->config['identifier']}&usersig={$userSig}&random={$random}&contenttype=json", $params);
+    }
+
+    /**
+     * 获取usersig签名-具体操作
+     */
+    public function userSig($user_id)
+    {
+        // 获取配置信息
+        $userSigObj = new GetUserSig($this->config["appid"], $this->config["key"]);
+        return $userSigObj->genUserSig($user_id);
+    }
+}

+ 52 - 0
app/Master/Framework/Library/Twilio/Sms.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace App\Master\Framework\Library\Twilio;
+
+use _PHPStan_579402b64\Nette\Utils\DateTime;
+use App\Master\Framework\Library\Library;
+use Twilio\Rest\Client;
+
+class Sms extends Library
+{
+    private string $account_sid;
+    private string $auth_token;
+    private string $twilio_number;
+
+    /**
+     * 实例化
+     */
+    public function __construct()
+    {
+        // 获取配置信息
+        $this->account_sid   = (string)site('twilio_account_sid');
+        $this->auth_token    = (string)site('twilio_auth_token');
+        $this->twilio_number = (string)site('twilio_number');
+    }
+
+    /**
+     * 发送短信
+     * @param string $mobile
+     * @param string $body
+     * @return bool
+     * @throws \Twilio\Exceptions\ConfigurationException
+     * @throws \Twilio\Exceptions\TwilioException
+     */
+    public function send(string $mobile, string $body): bool
+    {
+        $client  = new Client($this->account_sid, $this->auth_token);
+        try {
+            $message = $client->messages->create($mobile, [
+                'from' => $this->twilio_number,
+                'body' => $body
+            ]);
+        }catch (\Exception $exception){
+            return $this->error($exception->getMessage());
+        }
+
+        return $this->success('获取成功',[
+            'status' => $message->status,
+            'errorMessage' => $message->errorMessage,
+            'errorCode' => $message->errorCode,
+        ]);
+    }
+}

+ 166 - 0
app/Middleware/ApiAgent.php

@@ -0,0 +1,166 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Middleware;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Model\Arts\UserModel;
+use App\Service\SystemService;
+use App\Utils\AppResult;
+use App\Utils\Control\ActionUtil;
+use App\Utils\Control\AuthUser;
+use App\Utils\Encrypt\Token;
+use App\Utils\Encrypt\TokenUtil;
+use App\Utils\LogUtil;
+use App\Utils\RedisUtil;
+use Hyperf\Coroutine\Coroutine;
+use Hyperf\HttpServer\Contract\RequestInterface;
+use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse;
+use Hyperf\HttpServer\Router\Dispatched;
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+class ApiAgent implements MiddlewareInterface
+{
+    // 日志模块名称
+    const LOG_MODULE = 'ApiAgent-Middleware-Log';
+    const LOG_ACTION = 'verifyToken';
+    const PROJECT    = 'Api';
+
+    /**
+     * @var ContainerInterface
+     */
+    protected $container;
+
+    /**
+     * @var RequestInterface
+     */
+    protected $request;
+
+    /**
+     * @var HttpResponse
+     */
+    protected $response;
+
+    protected $action;
+
+    public function __construct(ContainerInterface $container, HttpResponse $response, RequestInterface $request)
+    {
+        $this->container = $container;
+        $this->response  = $response;
+        $this->request   = $request;
+    }
+
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        //日志统一写入
+        LogUtil::getInstance(self::PROJECT . "/");//设置日志存入通道
+
+        Coroutine::defer(function () {
+            LogUtil::close();// 协程结束后统一写入
+        });
+
+        $this->action = $action = ActionUtil::actions($request);
+        $params = $this->request->all();
+        // 记录用户请求参数
+        LogUtil::info('请求参数', $action['controller'], $action['action'], $params);
+
+        //接口限流,写到中间件中
+        if (!RedisUtil::getInstance(RedisKeyEnum::API_REQUEST_TRAFFIC)->requestLimit("{$action['controller']}/{$action['action']}", 1, 10)) {
+            LogUtil::info('请求次数过多', $action['controller'], $action['action']);
+            return $this->response206();
+        }
+
+        $token = $this->request->header('token');
+        if (empty($token)) $token = $params['token'] ?? '';
+        if (!empty($token) && !in_array("{$action['controller']}/{$action['action']}", self::tokenWhiteList())) {
+            LogUtil::info('token 验证开始', self::LOG_MODULE, self::LOG_ACTION, (string)$token);
+
+            // 校验token
+            if (!$user_id = $this->checkToken($token)){
+                return $this->response401();
+            }
+
+            // 查询并记录用户信息
+            $user = (new UserModel())->authUserInfo($user_id);
+            if (!$user) {
+                LogUtil::warning('账号不存在', self::LOG_MODULE, self::LOG_ACTION, ['user_id', $user_id]);
+                return $this->response401();
+            }
+
+            LogUtil::info('用户编号', $action['controller'], $action['action'], $user_id);
+            AuthUser::getInstance()->set(json_decode(json_encode($user), true));
+        }
+
+        return $handler->handle($request);
+    }
+
+    /**
+     * 校验token
+     *
+     * @param string $token
+     * @return false|int
+     */
+    public function checkToken(string $token): false|int
+    {
+        $checkToken = Token::get($token);
+        if (!$checkToken) {
+            LogUtil::warning('token 验证失败', self::LOG_MODULE, self::LOG_ACTION, $checkToken);
+            return false;
+        }
+
+        //验证参数信息
+        if (empty($checkToken['user_id'])) {
+            LogUtil::warning('token 参数不全', self::LOG_MODULE, self::LOG_ACTION, $checkToken);
+            return false;
+        }
+
+        return (int)$checkToken['user_id'];
+    }
+
+    /**
+     * 令牌失效
+     *
+     * @param string $message
+     * @param $result
+     * @return ResponseInterface
+     */
+    private function response401(string $message = '令牌失效,请稍后重试!', $result = null): ResponseInterface
+    {
+        // 记录令牌校验
+        $action = $this->action;
+        LogUtil::info($message, $action['controller'], $action['action'], $result);
+        return AppResult::response401($message, $result);
+    }
+
+    /**
+     * 请求频繁
+     *
+     * @param string $message
+     * @param $result
+     * @return ResponseInterface
+     */
+    private function response206(string $message = '当前访问人数过多,请稍后再试!', $result = null): ResponseInterface
+    {
+        // 记录令牌校验
+        $action = $this->action;
+        LogUtil::info($message, $action['controller'], $action['action'], $result);
+        return AppResult::response206($message, $result);
+    }
+
+    /**
+     * token校验黑名单
+     * @return string[]
+     */
+    public function tokenWhiteList(): array
+    {
+        return [
+            'v1/Common/PassportController/login',
+            'v1/Common/PassportController/wxLogin',
+        ];
+    }
+}

+ 184 - 0
app/Middleware/ApiAgentBak.php

@@ -0,0 +1,184 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Middleware;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Model\Arts\UserModel;
+use App\Service\SystemService;
+use App\Utils\AppResult;
+use App\Utils\Control\ActionUtil;
+use App\Utils\Control\AuthUser;
+use App\Utils\Encrypt\TokenUtil;
+use App\Utils\LogUtil;
+use App\Utils\RedisUtil;
+use Hyperf\Coroutine\Coroutine;
+use Hyperf\HttpServer\Contract\RequestInterface;
+use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse;
+use Hyperf\HttpServer\Router\Dispatched;
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+class ApiAgentBak implements MiddlewareInterface
+{
+    // 日志模块名称
+    const LOG_MODULE = 'ApiAgent-Middleware-Log';
+    const LOG_ACTION = 'verifyToken';
+    const PROJECT    = 'Api';
+
+    /**
+     * @var ContainerInterface
+     */
+    protected $container;
+
+    /**
+     * @var RequestInterface
+     */
+    protected $request;
+
+    /**
+     * @var HttpResponse
+     */
+    protected $response;
+
+    protected $action;
+
+    public function __construct(ContainerInterface $container, HttpResponse $response, RequestInterface $request)
+    {
+        $this->container = $container;
+        $this->response  = $response;
+        $this->request   = $request;
+    }
+
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        //日志统一写入
+        LogUtil::getInstance(self::PROJECT . "/");//设置日志存入通道
+
+        Coroutine::defer(function () {
+            LogUtil::close();//协程结束后统一写入
+        });
+
+        $this->action = $action = ActionUtil::actions($request);
+        // 记录用户请求参数
+        LogUtil::info('请求参数', $action['controller'], $action['action'], $this->request->all());
+
+        //接口限流,写到中间件中
+        if (!RedisUtil::getInstance(RedisKeyEnum::API_REQUEST_TRAFFIC)->requestLimit("{$action['controller']}/{$action['action']}", 1, 10)) {
+            LogUtil::info('请求次数过多', $action['controller'], $action['action']);
+            return $this->response206();
+        }
+
+        $token = $this->request->header('Access-Token');
+        if (!empty($token) && !in_array("{$action['controller']}/{$action['action']}", self::tokenWhiteList())) {
+            LogUtil::info('token 验证开始', self::LOG_MODULE, self::LOG_ACTION, (string)$token);
+
+            //设置一个假token 用于测试
+            if(strpos($token,'testuid_') !== false){
+                $user_id = intval(substr($token,8));
+            } else {
+                // 校验token
+                if (!$user_id = $this->checkToken($token)){
+                    return $this->response400();
+                }
+            }
+
+            // 查询并记录用户信息
+            $user = (new UserModel())->authUserInfo((int)$user_id);
+            if (!$user) {
+                //防止在缓存后操作得用户登录重试,清除缓存
+                $system = new SystemService();
+                $system->flushCache((int)$user_id);
+
+                LogUtil::warning('账号不存在', self::LOG_MODULE, self::LOG_ACTION, ['user_id', $user_id]);
+                return $this->response400();
+            }
+
+            LogUtil::info('用户编号', $action['controller'], $action['action'], $user_id);
+            AuthUser::getInstance()->set(json_decode(json_encode($user), true));
+        }
+
+        return $handler->handle($request);
+    }
+
+    /**
+     * 校验token
+     *
+     * @param string $token
+     * @return false
+     */
+    public function checkToken(string $token)
+    {
+        $checkToken = TokenUtil::verifyToken($token);
+        if (!$checkToken) {
+            LogUtil::warning('token 验证失败', self::LOG_MODULE, self::LOG_ACTION, $checkToken);
+            return false;
+        }
+
+        //签发时间大于当前服务器时间验证失败
+        if (!isset($checkToken['iat']) || $checkToken['iat'] > time()) {
+            LogUtil::warning('token 未生效', self::LOG_MODULE, self::LOG_ACTION, $checkToken);
+            return false;
+        }
+
+        //过期时间小于当前服务器时间验证失败
+        if (!isset($checkToken['exp']) || $checkToken['exp'] < time()) {
+            LogUtil::warning('token 已失效', self::LOG_MODULE, self::LOG_ACTION, $checkToken);
+            return false;
+        }
+
+        //验证参数信息
+        if (empty($checkToken['data']['user_id'])) {
+            LogUtil::warning('token 参数不全', self::LOG_MODULE, self::LOG_ACTION, $checkToken);
+            return false;
+        }
+
+        return $checkToken['data']['user_id'];
+    }
+
+    /**
+     * 令牌失效
+     *
+     * @param string $message
+     * @param $result
+     * @return ResponseInterface
+     */
+    private function response400(string $message = '令牌失效,请稍后重试!', $result = null): ResponseInterface
+    {
+        // 记录令牌校验
+        $action = $this->action;
+        LogUtil::info($message, $action['controller'], $action['action'], $result);
+        return AppResult::response400($message, $result);
+    }
+
+    /**
+     * 请求频繁
+     *
+     * @param string $message
+     * @param $result
+     * @return ResponseInterface
+     */
+    private function response206(string $message = '当前访问人数过多,请稍后再试!', $result = null): ResponseInterface
+    {
+        // 记录令牌校验
+        $action = $this->action;
+        LogUtil::info($message, $action['controller'], $action['action'], $result);
+        return AppResult::response206($message, $result);
+    }
+
+    /**
+     * token校验黑名单
+     * @return string[]
+     */
+    public function tokenWhiteList(): array
+    {
+        return [
+            'v1/Common/PassportController/login',
+            'v1/Common/PassportController/wxLogin',
+        ];
+    }
+}

+ 63 - 0
app/Middleware/ApiSign.php

@@ -0,0 +1,63 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Middleware;
+
+use App\Utils\AppResult;
+use App\Utils\Control\AuthUser;
+use App\Utils\LogUtil;
+use Hyperf\Coroutine\Coroutine;
+use Hyperf\HttpServer\Contract\RequestInterface;
+use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse;
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+class ApiSign implements MiddlewareInterface
+{
+    /**
+     * @var ContainerInterface
+     */
+    protected $container;
+
+    /**
+     * @var RequestInterface
+     */
+    protected $request;
+
+    /**
+     * @var HttpResponse
+     */
+    protected $response;
+
+    public function __construct(ContainerInterface $container, HttpResponse $response, RequestInterface $request)
+    {
+        $this->container = $container;
+        $this->response  = $response;
+        $this->request   = $request;
+    }
+
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        //验证登录
+        if (!AuthUser::getInstance()->check()) {
+            return $this->response401();
+        }
+
+        // 根据具体业务判断逻辑走向
+        return $handler->handle($request);
+    }
+
+    /**
+     * @param string $message
+     * @param $result
+     * @return ResponseInterface
+     */
+    private function response401(string $message = '未登录,令牌失效,请稍后重试!', $result = null): ResponseInterface
+    {
+        return AppResult::response401($message, $result);
+    }
+}

+ 186 - 0
app/Middleware/DriverAgent.php

@@ -0,0 +1,186 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Middleware;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Model\Arts\DriverModel;
+use App\Model\Arts\UserModel;
+use App\Service\SystemService;
+use App\Utils\AppResult;
+use App\Utils\Control\ActionUtil;
+use App\Utils\Control\AuthUser;
+use App\Utils\Encrypt\Token;
+use App\Utils\Encrypt\TokenUtil;
+use App\Utils\LogUtil;
+use App\Utils\RedisUtil;
+use Hyperf\Coroutine\Coroutine;
+use Hyperf\HttpServer\Contract\RequestInterface;
+use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse;
+use Hyperf\HttpServer\Router\Dispatched;
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use function Hyperf\Config\config;
+
+class DriverAgent implements MiddlewareInterface
+{
+    // 日志模块名称
+    const LOG_MODULE = 'DriverAgent-Middleware-Log';
+    const LOG_ACTION = 'verifyToken';
+    const PROJECT    = 'Driver';
+
+    /**
+     * @var ContainerInterface
+     */
+    protected $container;
+
+    /**
+     * @var RequestInterface
+     */
+    protected $request;
+
+    /**
+     * @var HttpResponse
+     */
+    protected $response;
+
+    protected $action;
+
+    public function __construct(ContainerInterface $container, HttpResponse $response, RequestInterface $request)
+    {
+        $this->container = $container;
+        $this->response  = $response;
+        $this->request   = $request;
+    }
+
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        //日志统一写入
+        LogUtil::getInstance(self::PROJECT . "/");//设置日志存入通道
+
+        Coroutine::defer(function () {
+            LogUtil::close();// 协程结束后统一写入
+        });
+
+        $this->action = $action = ActionUtil::actions($request,self::PROJECT);
+        $params = $this->request->all();
+        // 记录用户请求参数
+        LogUtil::info('请求参数', $action['controller'], $action['action'], $params);
+
+        //接口限流,写到中间件中
+        if (!RedisUtil::getInstance(RedisKeyEnum::API_REQUEST_TRAFFIC)->requestLimit("{$action['controller']}/{$action['action']}", 1, 10)) {
+            LogUtil::info('请求次数过多', $action['controller'], $action['action']);
+            return $this->response206();
+        }
+
+        $token = $this->request->header('token');
+        if (empty($token)) $token = $params['token'] ?? '';
+        if (!empty($token) && !in_array("{$action['controller']}/{$action['action']}", self::tokenWhiteList())) {
+            LogUtil::info('token 验证开始', self::LOG_MODULE, self::LOG_ACTION, (string)$token);
+
+            // 校验token
+            if (!$driver_id = $this->checkToken($token)){
+                return $this->response401();
+            }
+
+            // 查询并记录用户信息
+            $user = (new DriverModel())->authUserInfo($driver_id);
+            if (!$user) {
+                LogUtil::warning('账号不存在', self::LOG_MODULE, self::LOG_ACTION, ['driver_id', $driver_id]);
+                return $this->response401();
+            }
+
+            LogUtil::info('用户编号', $action['controller'], $action['action'], $driver_id);
+            AuthUser::getInstance()->set(json_decode(json_encode($user), true));
+        }
+
+        return $handler->handle($request);
+    }
+
+    /**
+     * 校验token
+     *
+     * @param string $token
+     * @return false|int
+     */
+    public function checkToken(string $token): false|int
+    {
+        //方便测试
+        if (strpos($token, 'testuid_') !== false && config('app_debug',false)) {
+            $uid = substr($token, 8);
+            return intval($uid);
+        }
+        //方便测试
+        $checkToken = TokenUtil::verifyToken($token);
+        if (!$checkToken) {
+            LogUtil::warning('token 验证失败', self::LOG_MODULE, self::LOG_ACTION, $checkToken);
+            return false;
+        }
+
+        //签发时间大于当前服务器时间验证失败
+        if (!isset($checkToken['iat']) || $checkToken['iat'] > time()) {
+            LogUtil::warning('token 未生效', self::LOG_MODULE, self::LOG_ACTION, $checkToken);
+            return false;
+        }
+
+        //过期时间小于当前服务器时间验证失败
+        if (!isset($checkToken['exp']) || $checkToken['exp'] < time()) {
+            LogUtil::warning('token 已失效', self::LOG_MODULE, self::LOG_ACTION, $checkToken);
+            return false;
+        }
+
+        //验证参数信息
+        if (empty($checkToken['data']['driver_id'])) {
+            LogUtil::warning('token 参数不全', self::LOG_MODULE, self::LOG_ACTION, $checkToken);
+            return false;
+        }
+
+        return $checkToken['data']['driver_id'];
+    }
+
+    /**
+     * 令牌失效
+     *
+     * @param string $message
+     * @param $result
+     * @return ResponseInterface
+     */
+    private function response401(string $message = '令牌失效,请稍后重试!', $result = null): ResponseInterface
+    {
+        // 记录令牌校验
+        $action = $this->action;
+        LogUtil::info($message, $action['controller'], $action['action'], $result);
+        return AppResult::response401($message, $result);
+    }
+
+    /**
+     * 请求频繁
+     *
+     * @param string $message
+     * @param $result
+     * @return ResponseInterface
+     */
+    private function response206(string $message = '当前访问人数过多,请稍后再试!', $result = null): ResponseInterface
+    {
+        // 记录令牌校验
+        $action = $this->action;
+        LogUtil::info($message, $action['controller'], $action['action'], $result);
+        return AppResult::response206($message, $result);
+    }
+
+    /**
+     * token校验黑名单
+     * @return string[]
+     */
+    public function tokenWhiteList(): array
+    {
+        return [
+            'v1/Common/PassportController/login',
+            'v1/Common/PassportController/wxLogin',
+        ];
+    }
+}

+ 63 - 0
app/Middleware/DriverSign.php

@@ -0,0 +1,63 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Middleware;
+
+use App\Utils\AppResult;
+use App\Utils\Control\AuthUser;
+use App\Utils\LogUtil;
+use Hyperf\Coroutine\Coroutine;
+use Hyperf\HttpServer\Contract\RequestInterface;
+use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse;
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+class DriverSign implements MiddlewareInterface
+{
+    /**
+     * @var ContainerInterface
+     */
+    protected $container;
+
+    /**
+     * @var RequestInterface
+     */
+    protected $request;
+
+    /**
+     * @var HttpResponse
+     */
+    protected $response;
+
+    public function __construct(ContainerInterface $container, HttpResponse $response, RequestInterface $request)
+    {
+        $this->container = $container;
+        $this->response  = $response;
+        $this->request   = $request;
+    }
+
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        //验证登录
+        if (!AuthUser::getInstance()->check()) {
+            return $this->response401();
+        }
+
+        // 根据具体业务判断逻辑走向
+        return $handler->handle($request);
+    }
+
+    /**
+     * @param string $message
+     * @param $result
+     * @return ResponseInterface
+     */
+    private function response401(string $message = '未登录,令牌失效,请稍后重试!', $result = null): ResponseInterface
+    {
+        return AppResult::response401($message, $result);
+    }
+}

+ 44 - 0
app/Model/Arts/AgreementModel.php

@@ -0,0 +1,44 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+class AgreementModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'agreement';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    protected int $is_status_search = 0;// 是否使用 1=是 0=否 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 是否使用 1=是 0=否 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public function searchKeyAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('key', $value);
+    }
+
+}

+ 33 - 0
app/Model/Arts/CarSeatModel.php

@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Model\Model;
+use Hyperf\DbConnection\Db;
+use function Hyperf\Config\config;
+
+class CarSeatModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'car_seat';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+    protected int $is_status_search = 1;// 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+}

+ 34 - 0
app/Model/Arts/CityModel.php

@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Model\Model;
+use Hyperf\DbConnection\Db;
+use function Hyperf\Config\config;
+
+class CityModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'city';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+    protected int $is_status_search = 0;// 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+}

+ 51 - 0
app/Model/Arts/CouponModel.php

@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+class CouponModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'coupon';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    protected int $is_status_search = 1;// 是否使用 1=是 0=否 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 是否使用 1=是 0=否 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public function searchTypeAttribute($query, $value, array $params): mixed
+    {
+        if (!isset($value)) {
+            return $query;
+        }
+        return $query->where('type', $value);
+    }
+
+    public function dataCreateTimeAttribute($value,$params)
+    {
+        if (empty($value)){
+            return '---';
+        }
+        return date('Y-m-d H:i:s',$value);
+    }
+}

+ 28 - 0
app/Model/Arts/DemoModel.php

@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Model\Model;
+
+class DemoModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'admin_role';
+
+    protected ?string $dateFormat = 'U';
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+}

+ 59 - 0
app/Model/Arts/DriverCarModel.php

@@ -0,0 +1,59 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Model\Model;
+use Hyperf\DbConnection\Db;
+use function Hyperf\Config\config;
+
+class DriverCarModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'driver_car';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+    protected int $is_status_search = 1;// 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public function searchDriverIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('driver_id', $value);
+    }
+
+    public static function addEdit(int $driver_id, array $params = [])
+    {
+        if (!empty($driver_id)){
+            $info = self::query()->where('driver_id', $driver_id)->first();
+        }
+        if (isset($info)){
+            $params['update_time'] = time();
+            if (!self::query()->where('id',$info['id'])->update($params)){
+                return 0;
+            }
+            return $info['id'];
+        }else{
+            $params['driver_id']   = $driver_id;
+            $params['create_time'] = time();
+            return self::query()->insertGetId($params);
+        }
+    }
+}

+ 56 - 0
app/Model/Arts/DriverLicenseModel.php

@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Model\Model;
+use Hyperf\DbConnection\Db;
+use function Hyperf\Config\config;
+
+class DriverLicenseModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'driver_license';
+
+    protected ?string $dateFormat = 'U';
+    public bool $timestamps = false;
+    protected int $is_status_search = 1;// 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    /**
+     * @param int $driver_id
+     * @param array $params
+     * @return int
+     */
+    public static function addEdit(int $driver_id, array $params = [])
+    {
+        if (!empty($driver_id)){
+            $info = self::query()->where('driver_id', $driver_id)->first();
+        }
+        if (isset($info)){
+            $params['update_time'] = time();
+            if (!self::query()->where('id',$info['id'])->update($params)){
+                return 0;
+            }
+            return $info['id'];
+        }else{
+            $params['driver_id']   = $driver_id;
+            $params['create_time'] = time();
+            return self::query()->insertGetId($params);
+        }
+    }
+}

+ 144 - 0
app/Model/Arts/DriverMessageModel.php

@@ -0,0 +1,144 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Model\Model;
+
+class DriverMessageModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'driver_message';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    protected int $is_status_search = 1;// 是否使用 1=是 0=否 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 是否使用 1=是 0=否 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public function searchDriverIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('driver_id', $value);
+    }
+
+    public function searchIsReadAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('is_read', $value);
+    }
+
+    public function searchIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('id', $value);
+    }
+
+    public static function add(array $params)
+    {
+        $insert = array_merge($params,[
+            'status' => 1,
+            'create_time' => time()
+        ]);
+        return self::query()->insertGetId($insert);
+    }
+
+    public static function edit(int $id, array $params)
+    {
+        unset($params['id']);
+        $insert = array_merge($params,[
+            'update_time' => time()
+        ]);
+        $query = self::query()->where('id',$id);
+
+        return $query->update($insert);
+    }
+
+    /**
+     * 获取未读消息数量
+     * @param int $driver_id
+     * @return int
+     */
+    public static function getNumByDriverId(int $driver_id)
+    {
+        return self::query()
+            ->leftJoin('driver_message_read',function ($join) use ($driver_id){
+                $join->on('driver_message.id','=','driver_message_read.message_id')
+                    ->where('driver_message_read.driver_id',$driver_id);
+            })
+            ->whereIn('driver_message.driver_id',[0,$driver_id])
+            ->whereNull('driver_message_read.id')->count();
+    }
+
+
+    public static function read(int $driver_id)
+    {
+        $read = self::query()
+            ->leftJoin('driver_message_read',function ($join) use ($driver_id){
+                $join->on('driver_message.id','=','driver_message_read.message_id')
+                    ->where('driver_message_read.driver_id',$driver_id);
+            })
+            ->whereIn('driver_message.driver_id',[0,$driver_id])
+            ->whereNull('driver_message_read.id')
+            ->pluck('driver_message.id');
+
+        $time = time();
+        foreach ($read as $message_id) {
+            $data[] = [
+                'driver_id' => $driver_id,
+                'message_id' => $message_id,
+                'create_time' => $time
+            ];
+        }
+        !empty($data) && DriverMessageReadModel::query()->insert($data);
+    }
+
+    /**
+     * 数据处理器
+     * @param $value
+     * @param $params
+     * @return string
+     */
+    public function dataCreateTimeAttribute($value,$params)
+    {
+        if (empty($value)){
+            return '---';
+        }
+
+        if (date('Y-m-d',$value) == date('Y-m-d')){
+            return date('H:i:s',$value);
+        }
+
+        return date('Y-m-d H:i:s',$value);
+    }
+
+    public function driver()
+    {
+        return $this->hasOne(DriverModel::class,'id','driver_id');
+    }
+
+    public function reads()
+    {
+        return $this->hasOne(DriverMessageReadModel::class,'message_id','id');
+    }
+}

+ 79 - 0
app/Model/Arts/DriverMessageReadModel.php

@@ -0,0 +1,79 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+class DriverMessageReadModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'driver_message_read';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    protected int $is_status_search = 0;// 是否使用 1=是 0=否 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 是否使用 1=是 0=否 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public function searchDriverIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('driver_id', $value);
+    }
+
+    public function searchIsReadAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('is_read', $value);
+    }
+
+    public function searchIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('id', $value);
+    }
+
+    public static function add(array $params)
+    {
+        $insert = array_merge($params,[
+            'status' => 1,
+            'create_time' => time()
+        ]);
+        return self::query()->insertGetId($insert);
+    }
+
+    public static function edit(int $id, array $params)
+    {
+        unset($params['id']);
+        $insert = array_merge($params,[
+            'update_time' => time()
+        ]);
+        $query = self::query()->where('id',$id);
+
+        return $query->update($insert);
+    }
+}

+ 177 - 0
app/Model/Arts/DriverModel.php

@@ -0,0 +1,177 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Model\Model;
+
+class DriverModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'driver';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    protected int $is_status_search = 1;// 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    /**
+     * 中间件中获取用户信息
+     * @param int $user_id
+     * @return array
+     */
+    public function authUserInfo(int $user_id)
+    {
+        return $this->getDetail(params: ['id' => $user_id]);
+    }
+
+    /**
+     * 校验账户
+     * @param string $mobile
+     * @return array
+     */
+    public static function checkExists(string $mobile = '', string $password = '', string $area_code = ''): array
+    {
+        $query = self::query();
+
+        $query->where('mobile', $mobile);
+        !empty($area_code) && $query->where('area_code', $area_code);
+
+        $query->whereIn('status', [0, 1]);
+
+        $exists = $query->exists();
+        $driver = $query->first();
+
+        $driver_id    = $driver['id'] ?? 0;
+        $old_password = $driver['password'] ?? '';
+        if (!empty($password) && !password_verify(md5($password), $old_password)) {
+            $exists = false;
+        }
+
+        return [$exists, $driver_id];
+    }
+
+    /**
+     * 创建账号
+     * @param array $params
+     * @return bool|int
+     */
+    public static function createAccount(array $params): bool|int
+    {
+        if (empty($params['mobile'])) {
+            return false;
+        }
+
+        // 车辆信息
+        $car_id = $params['car_id'] ?? 0;
+        $car_params = [
+            'car_img'        => $params['car_img'] ?? '',
+            'car_color'        => $params['car_color'] ?? '',
+        ];
+
+        // 驾照信息
+        $license_id = $params['license_id'] ?? 0;
+        $license_params = [
+            'license_img' => $params['driver_license_img'] ?? ''
+        ];
+
+        unset(
+            $params['captcha'],
+            $params['car_id'],
+            $params['license_id'],
+            $params['car_img'],
+            $params['car_color'],
+            $params['driver_license_img']
+        );
+
+        if (!empty($params['password'])) {
+            $params['password'] = password_hash(md5($params['password']), PASSWORD_DEFAULT);
+        }
+
+        $params['status']      = 1;
+        $params['create_time'] = time();
+
+        // 创建账户
+        $driver_id = self::query()->insertGetId($params);
+        if (!$driver_id) {
+            return false;
+        }
+
+        // 创建钱包
+        if (!DriverWalletModel::add($driver_id)) {
+            return false;
+        }
+
+        if ($params['type'] == 2){
+            // 创建车辆信息
+            if (!DriverCarModel::addEdit($driver_id, $car_params)) {
+                return false;
+            }
+
+            // 创建驾照
+            if (!DriverLicenseModel::addEdit($driver_id, $license_params)) {
+                return false;
+            }
+        } else {
+            $car = DriverCarModel::query()->where('id',$car_id)->where('driver_id',0)->update(['driver_id' => $driver_id,'update_time'=>time()]);
+            $license = DriverLicenseModel::query()->where('id',$license_id)->where('driver_id',0)->update(['driver_id' => $driver_id,'update_time'=>time()]);
+            if (!$car || !$license) {
+                return false;
+            }
+        }
+
+        return $driver_id;
+    }
+
+    /**
+     * 更新登录时间
+     * @param int $driver_id
+     * @param string $openid
+     * @param string $unionid
+     * @param array $extend
+     * @return bool
+     */
+    public static function loginTime(int $driver_id, string $openid = '', string $unionid = '', array $extend = []): bool
+    {
+        !empty($unionid) && $data['unionid'] = $unionid;
+        !empty($openid) && $data['openid'] = $openid;
+        $data['last_login_time'] = time();
+        $data['push_cid'] = '';
+
+        $update = self::query()->where('id', $driver_id)->update($data);
+
+        return (bool)$update;
+    }
+
+    public function edit(int $driver_id, array $extend = [])
+    {
+        if (!empty($extend['password'])) {
+            $extend['password'] = password_hash(md5($extend['password']), PASSWORD_DEFAULT);
+        }
+        $extend['update_time'] = time();
+        if (!$this->query()->where('id', $driver_id)->update($extend)){
+            return $this->error('更新失败');
+        }
+        return $this->success('更新成功');
+    }
+
+    public function car()
+    {
+        return $this->hasOne(DriverCarModel::class, 'driver_id', 'id');
+    }
+}

+ 67 - 0
app/Model/Arts/DriverMoneyLogModel.php

@@ -0,0 +1,67 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+class DriverMoneyLogModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'driver_money_log';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    protected int $is_status_search = 0;// 是否使用 1=是 0=否 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 是否使用 1=是 0=否 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public function searchDriverIdAttribute($query, $value, array $params): mixed
+    {
+        if (!isset($value)) {
+            return $query;
+        }
+        return $query->where('driver_id', $value);
+    }
+
+    public function searchTypeAttribute($query, $value, array $params): mixed
+    {
+        if (!isset($value)) {
+            return $query;
+        }
+        return $query->where('type', $value);
+    }
+
+    public function searchTypeInAttribute($query, $value, array $params): mixed
+    {
+        if (!isset($value)) {
+            return $query;
+        }
+        return $query->whereIn('type', $value);
+    }
+
+    public function dataCreateTimeAttribute($value,$params)
+    {
+        if (empty($value)){
+            return '---';
+        }
+        return date('Y-m-d H:i:s',$value);
+    }
+}

+ 175 - 0
app/Model/Arts/DriverOnlineLogModel.php

@@ -0,0 +1,175 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Model\Model;
+use Hyperf\DbConnection\Db;
+
+class DriverOnlineLogModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'driver_online_log';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    protected int $is_status_search = 1;// 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public function searchDriverIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('driver_id', $value);
+    }
+
+    public function searchIsOverAttribute($query, $value, array $params): mixed
+    {
+        if (!isset($value)) {
+            return $query;
+        }
+        return $query->where('is_over', $value == 1 ? 1 : 0);
+    }
+
+    /**
+     * 校验用户资质
+     * @param $user
+     * @return bool
+     */
+    public function checkCert($user)
+    {
+        $car = DriverCarModel::query()->where('driver_id', $user['id'])->first();
+        $license = DriverLicenseModel::query()->where('driver_id', $user['id'])->first();
+        if (!isset($license['status']) || $license['status'] != 1) {
+            return $this->error('请提交驾照信息,若已提交请耐心等待审核');
+        }
+
+        if (!isset($car['status']) || $car['status'] != 1) {
+            return $this->error('请提交车辆信息,若已提交请耐心等待审核');
+        }
+
+        if (strtotime($car['insurance_date']) <= time()) {
+            return $this->error('车辆保险过期了');
+        }
+
+        if (strtotime($license['end_date']) <= time()) {
+            return $this->error('驾照过期了');
+        }
+
+        // 判断余额
+        $wallet = DriverWalletModel::getOne($user['id'],'money');
+        if ($wallet <= 0){
+            return $this->error('余额不足,请充值后再行接单');
+        }
+
+        return $this->success();
+    }
+
+    public static function add(array $params)
+    {
+        $insert = array_merge($params, [
+            'status'     => 1,
+            'create_time' => time()
+        ]);
+        return self::query()->insertGetId($insert);
+    }
+
+    public static function edit(int $id, array $params)
+    {
+        unset($params['id']);
+        $insert = array_merge($params, [
+            'update_time' => time()
+        ]);
+        $query  = self::query()->where('id', $id);
+
+        return $query->update($insert);
+    }
+
+    /**
+     * 上线
+     * @param int $id
+     * @return bool
+     */
+    public function online(int $id)
+    {
+        $user = (new DriverModel())->authUserInfo($id);
+        if ($user['is_online'] == 1) {
+            return $this->success('已经上线');
+        }
+
+        // 用户资质校验
+        if (!$this->checkCert($user)) {
+            return $this->error($this->getMessage());
+        }
+
+        $model = (new static());
+        if ($model->getDetail(params: ['driver_id' => $id, 'is_over' => 0])) {
+            return $this->success('已经上线');
+        }
+
+        Db::beginTransaction();
+        $log    = $this->add([
+            'driver_id' => $id,
+            'date'      => date('Y-m-d'),
+            'start_at'  => time()
+        ]);
+        $online = (new DriverModel())->edit($id, ['is_online' => 1]);
+        if (!$log || !$online) {
+            DB::rollBack();
+            return $this->error('上线失败了');
+        }
+        DB::commit();
+        return $this->success('上线成功');
+    }
+
+    /**
+     * 离线
+     * @param $id
+     * @return bool
+     */
+    public function offline($id)
+    {
+        $user = (new DriverModel())->authUserInfo($id);
+        if ($user['is_online'] == 0) {
+            return $this->success('已经下线');
+        }
+        $model = (new static());
+        $info  = $model->getDetail(params: ['driver_id' => $id, 'is_over' => 0]);
+        if (!$info) {
+            return $this->success('已经下线');
+        }
+        DB::beginTransaction();
+        $time = time();
+        $log  = $this->edit($info['id'], [
+            'end_at'   => $time,
+            'is_over'  => 1,
+            'duration' => intval($time - $info['start_at']),
+        ]);
+
+        $offline = (new DriverModel())->edit($id, ['is_online' => 0]);
+
+        if (!$log || !$offline) {
+            DB::rollBack();
+            return $this->error('下线失败');
+        }
+        DB::commit();
+        return $this->success('已经下线');
+    }
+
+}

+ 91 - 0
app/Model/Arts/DriverWalletModel.php

@@ -0,0 +1,91 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Model\Model;
+use Hyperf\DbConnection\Db;
+use function Hyperf\Config\config;
+
+class DriverWalletModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'driver_wallet';
+
+    protected ?string $dateFormat       = 'U';
+    public bool       $timestamps       = false;
+    protected int     $is_status_search = 0;// 默认使用 status = 1 筛选
+    protected int     $is_delete_search = 0;// 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public static function getOne(int $driver_id, string $field = '')
+    {
+        //所有钱包余额
+        $wallet = self::query()->where(['driver_id' => $driver_id])->first();
+        if (!empty($field)) {
+            return $wallet[$field] ?? '';
+        }
+        return $wallet;
+    }
+
+    public static function add(int $driver_id, array $params = []): bool
+    {
+        $params['driver_id'] = $driver_id;
+        $insert              = self::query()->insertGetId($params);
+        return (bool)$insert;
+    }
+
+    /**
+     * 钱包操作
+     * @param int $driver_id
+     * @param float $money
+     * @param string $remark
+     * @param int $type
+     * @return bool
+     */
+    public function change(int $driver_id, float $money, string $remark = '', int $type = 3, string $about_value = '')
+    {
+        if (!in_array($type, [1, 2, 3, 4, 5, 6])) {
+            return $this->error('余额类型错误');
+        }
+        $wallet = (new static())->getOne($driver_id);
+        if (in_array($type, [2, 3, 5, 6])) {
+            // 扣除
+            $data = [
+                'money'          => bcadd($wallet['money'], (string)(-$money), 2),
+                'disburse_money' => bcadd($wallet['disburse_money'], (string)$money, 2)
+            ];
+//            if ($data['money'] < 0) {
+//                return $this->error('余额不足');
+//            }
+            $log_money = -$money;
+        } else {
+            // 充值
+            $data      = [
+                'money'     => bcadd($wallet['money'], (string)$money, 2),
+                'get_money' => bcadd($wallet['get_money'], (string)(-$money), 2)
+            ];
+            $log_money = $money;
+        }
+        if (!$this->query()->where(['driver_id' => $driver_id, 'money' => $wallet['money']])->update($data)) {
+            return $this->error('操作失败');
+        }
+        if (!DriverMoneyLogModel::query()->insert(['driver_id' => $driver_id, 'type' => $type, 'money' => $log_money, 'before' => $wallet['money'], 'after' => $data['money'], 'remark' => $remark, 'about_value' => $about_value, 'create_time' => time()])) {
+            return $this->error('记录操作失败');
+        }
+        return $this->success('操作成功');
+    }
+}

+ 37 - 0
app/Model/Arts/LiveGiftModel.php

@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+class LiveGiftModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'live_gift';
+
+    protected ?string $dateFormat = 'U';
+    public bool $timestamps = false;
+
+    protected int $is_status_search = 1;// 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+
+}

+ 86 - 0
app/Model/Arts/LiveRoomPkModel.php

@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Model\Model;
+use Hyperf\DbConnection\Db;
+
+class LiveRoomPkModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'live_room_pk';
+
+    protected ?string $dateFormat = 'U';
+
+    public bool $timestamps = false;
+
+    protected int $is_status_search = 1;// 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public function searchRoomNoAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('room_no', $value);
+    }
+
+    public function searchSessionAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('session', $value);
+    }
+
+    /**
+     * 结束PK
+     * @param int $pk_id
+     * @return bool
+     */
+    public function endPk(int $pk_id): bool
+    {
+        $pk_info = $this->query()->where('id',$pk_id)->first();
+        if (!$pk_info){
+            return $this->error('pk不存在');
+        }
+        if ($pk_info['status'] == 0) {
+            return $this->success('已结束');
+        }
+        Db::beginTransaction();
+        try {
+            $time = time();
+            if (!$this->query()->where('id', $pk_info['id'])->update(['status' => 0, 'end_time' => $time, 'duration' => Db::raw("{$time} - start_time")])) {
+                Db::rollback();
+                return $this->error('结束失败');
+            }
+
+            $redRooms  = LiveRoomModel::query()->where('id', $pk_info['red_room_id'])->where('pk_id', $pk_info['id'])->update(['pk_id' => 0, 'is_pk' => 0, 'pk_type' => 0]);
+            $blueRooms = LiveRoomModel::query()->where('id', $pk_info['blue_room_id'])->where('pk_id', $pk_info['id'])->update(['pk_id' => 0, 'is_pk' => 0, 'pk_type' => 0]);
+            if (!$redRooms || !$blueRooms) {
+                Db::rollback();
+                return $this->error('操作失败');
+            }
+            Db::commit();
+            return $this->success('已结束');
+        } catch (\Throwable $e) {
+            Db::rollback();
+            return $this->error($e->getMessage());
+        }
+    }
+}

+ 144 - 0
app/Model/Arts/MessageModel.php

@@ -0,0 +1,144 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Model\Model;
+
+class MessageModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'message';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    protected int $is_status_search = 1;// 是否使用 1=是 0=否 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 是否使用 1=是 0=否 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public function searchUserIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('user_id', $value);
+    }
+
+    public function searchIsReadAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('is_read', $value);
+    }
+
+    public function searchIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('id', $value);
+    }
+
+    public static function add(array $params)
+    {
+        $insert = array_merge($params,[
+            'status' => 1,
+            'create_time' => time()
+        ]);
+        return self::query()->insertGetId($insert);
+    }
+
+    public static function edit(int $id, array $params)
+    {
+        unset($params['id']);
+        $insert = array_merge($params,[
+            'update_time' => time()
+        ]);
+        $query = self::query()->where('id',$id);
+
+        return $query->update($insert);
+    }
+
+    /**
+     * 获取未读消息数量
+     * @param int $user_id
+     * @return int
+     */
+    public static function getNumByUserId(int $user_id)
+    {
+        return self::query()
+            ->leftJoin('message_read',function ($join) use ($user_id){
+                $join->on('message.id','=','message_read.message_id')
+                    ->where('message_read.user_id',$user_id);
+            })
+            ->whereIn('message.user_id',[0,$user_id])
+            ->whereNull('message_read.id')->count();
+    }
+
+
+    public static function read(int $user_id)
+    {
+        $read = self::query()
+            ->leftJoin('message_read',function ($join) use ($user_id){
+                $join->on('message.id','=','message_read.message_id')
+                    ->where('message_read.user_id',$user_id);
+            })
+            ->whereIn('message.user_id',[0,$user_id])
+            ->whereNull('message_read.id')
+            ->pluck('message.id');
+
+        $time = time();
+        foreach ($read as $message_id) {
+            $data[] = [
+                'user_id' => $user_id,
+                'message_id' => $message_id,
+                'create_time' => $time
+            ];
+        }
+        !empty($data) && MessageReadModel::query()->insert($data);
+    }
+
+    /**
+     * 数据处理器
+     * @param $value
+     * @param $params
+     * @return string
+     */
+    public function dataCreateTimeAttribute($value,$params)
+    {
+        if (empty($value)){
+            return '---';
+        }
+
+        if (date('Y-m-d',$value) == date('Y-m-d')){
+            return date('H:i:s',$value);
+        }
+
+        return date('Y-m-d H:i:s',$value);
+    }
+
+    public function user()
+    {
+        return $this->hasOne(UserModel::class,'id','user_id');
+    }
+
+    public function reads()
+    {
+        return $this->hasOne(MessageReadModel::class,'message_id','id');
+    }
+}

+ 79 - 0
app/Model/Arts/MessageReadModel.php

@@ -0,0 +1,79 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+class MessageReadModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'message_read';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    protected int $is_status_search = 0;// 是否使用 1=是 0=否 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 是否使用 1=是 0=否 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public function searchDriverIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('driver_id', $value);
+    }
+
+    public function searchIsReadAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('is_read', $value);
+    }
+
+    public function searchIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('id', $value);
+    }
+
+    public static function add(array $params)
+    {
+        $insert = array_merge($params,[
+            'status' => 1,
+            'create_time' => time()
+        ]);
+        return self::query()->insertGetId($insert);
+    }
+
+    public static function edit(int $id, array $params)
+    {
+        unset($params['id']);
+        $insert = array_merge($params,[
+            'update_time' => time()
+        ]);
+        $query = self::query()->where('id',$id);
+
+        return $query->update($insert);
+    }
+}

+ 36 - 0
app/Model/Arts/MobileAreaCodeModel.php

@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+class MobileAreaCodeModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'mobile_area_code';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    protected int $is_status_search = 1;// 是否使用 1=是 0=否 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 是否使用 1=是 0=否 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+}

+ 177 - 0
app/Model/Arts/OrderDriverModel.php

@@ -0,0 +1,177 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Model\Model;
+use App\Utils\Common;
+use Hyperf\DbConnection\Db;
+use function Hyperf\Config\config;
+
+class OrderDriverModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'order_driver';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+    protected int $is_status_search = 0;// 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public function searchDriverIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('driver_id', $value);
+    }
+
+    public function searchStatusAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('status', $value);
+    }
+
+    public function searchStartTimeGtAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('start_time','>=', $value);
+    }
+
+    /**
+     * 创建顺风车行程
+     * @param int $driver_id
+     * @param array $params
+     * @return bool
+     */
+    public function createTailwind(int $driver_id,array $params)
+    {
+        $params['driver_id'] = $driver_id;
+        $params['create_time'] = time();
+        if (!$this->query()->insert($params)) {
+            return $this->error('创建失败');
+        }
+        return $this->success('创建成功');
+    }
+
+    /**
+     * 接单
+     * @param int $driver_id
+     * @param array $order 订单 detail
+     * @param int $driver_order_id
+     * @return bool
+     */
+    public function takeOrder(int $driver_id,string $driver_name, array $order, int $driver_order_id = 0,array $params = [])
+    {
+        $time = time();
+        // 获取代驾车辆
+        $car = DriverCarModel::query()->where('driver_id',$driver_id)->where('status',1)->first();
+        if (!$car){
+            return $this->error('当前车辆信息错误');
+        }
+
+        $orderUp = OrderModel::query()->where('id', $order['id'])->update([
+            'driver_id' => $driver_id,
+            'driver_order_id' => $driver_order_id,
+            'driver_name' => $driver_name,
+            'car_no' => $car['car_no'],
+            'car_color' => $car['car_color'],
+            'car_brand' => $car['car_brand'],
+            'driver_lng' => $params['driver_lng'] ?? '',
+            'driver_lat' => $params['driver_lat'] ?? '',
+            'take_time' => $time,
+            'status' => 20,
+            'update_time' => $time
+        ]);
+        if (!$orderUp) {
+            return $this->error('接单失败');
+        }
+
+        $orderPointUp = OrderPointModel::query()->where('order_id', $order['id'])->update([
+            'driver_id' => $driver_id,
+            'driver_order_id' => $driver_order_id,
+            'update_time' => $time
+        ]);
+        if (!$orderPointUp) {
+            return $this->error('接单失败');
+        }
+
+        // 顺风车接单需要更改 顺风车路线同行信息
+        if ($order['type'] == 30){
+            $driverOrderUp = OrderDriverModel::query()
+                ->where('id',$driver_order_id)
+                ->update(['people_at_num' => Db::raw('people_at_num + '.$order['people_num'])]);
+            if (!$driverOrderUp){
+                return $this->error('接单失败');
+            }
+        }
+
+        return $this->success('操作成功');
+    }
+
+    /**
+     * 操作运送状态
+     * @param int $order_id
+     * @param int $driver_status
+     * @return bool
+     */
+    public function driver_status(int $order_id, int $driver_status = 1,int $is_pay = 0)
+    {
+        $data = [
+            'driver_status' => $driver_status,
+            'status' => 30,
+            'update_time' => time()
+        ];
+        if ($driver_status == 2){
+            $data['start_driver_time'] = time();
+        }
+        if ($driver_status == 3){
+            $is_pay === 1 && $data['is_pay'] = $is_pay;
+            $is_pay === 1 && $data['status'] = 40;
+            $data['end_driver_time'] = time();
+        }
+        if (!OrderModel::query()->where('id', $order_id)->update($data)) {
+            return $this->error('操作失败');
+        }
+
+        return $this->success('操作成功');
+    }
+
+    public function driver()
+    {
+        return $this->hasOne(DriverModel::class, 'id', 'driver_id');
+    }
+
+    public function car()
+    {
+        return $this->hasOne(DriverCarModel::class, 'driver_id', 'driver_id');
+    }
+
+    public function order()
+    {
+        return $this->hasOne(OrderModel::class, 'driver_order_id', 'id');
+    }
+
+    public function orders()
+    {
+        return $this->hasMany(OrderModel::class, 'driver_order_id', 'id');
+    }
+}

+ 437 - 0
app/Model/Arts/OrderModel.php

@@ -0,0 +1,437 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Controller\Business\DriverTypeBusiness;
+use App\Master\Enum\RedisKeyEnum;
+use App\Master\Framework\Library\AliCloud\AliSms;
+use App\Master\Framework\Library\Tencent\TencentIm;
+use App\Master\Framework\Library\Twilio\Sms;
+use App\Model\Model;
+use App\Utils\Common;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+use function Hyperf\Config\config;
+
+class OrderModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'order';
+
+    protected ?string $dateFormat       = 'U';
+    public bool       $timestamps       = false;
+    protected int     $is_status_search = 0;// 默认使用 status = 1 筛选
+    protected int     $is_delete_search = 0;// 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    const status = [
+        10 => '平台已接单',
+        20 => '司机已结单',
+        30 => '正在服务中',
+        40 => '已完成',
+        50 => '已取消'
+    ];
+
+    public function searchOrderNoAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('order_no', $value);
+    }
+
+    public function searchUserIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('user_id', $value);
+    }
+
+    public function searchDriverIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('driver_id', $value);
+    }
+
+    public function searchStatusAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('status', $value);
+    }
+
+    public function searchStatusInAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->whereIn('status', $value);
+    }
+
+    public function searchTypeInAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->whereIn('type', $value);
+    }
+
+    public function searchDriverStatusAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->whereIn('driver_status', $value);
+    }
+
+    public function searchAppointmentTimeNotAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('appointment_time', '>', 0);
+    }
+
+    public function searchAppointmentTimeZeroAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('appointment_time', 0);
+    }
+
+    public function searchAppointmentTimeBetweenAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->whereBetween('appointment_time', $value);
+    }
+
+    public function searchAppointmentTimeGtAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('appointment_time','>=', $value);
+    }
+
+    public function searchPushStatusAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('push_status', $value);
+    }
+
+    public function searchPushStatusInAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->whereIn('push_status', $value);
+    }
+
+    public function searchStatDistanceAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->havingRaw('(stat_distance < ? and appointment_time = 0) or appointment_time != 0', [$value]);
+    }
+
+    public function searchStatusOrderByAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        $order = implode(',',$value);
+        return $query->orderByRaw("FIELD(status,{$order})");
+    }
+
+    public function searchEndDriverDateBetweenAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->whereBetween('end_driver_time', $value);
+    }
+
+    // 筛选,如果是快车和接送机 则需要判断司机车型是否大于 订单所需车型
+    public function searchDriverCarSeatIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where(function ($where) use ($value){
+            $where->whereIn('type',[10,20])->where('car_seat_id','<=', $value);
+        });
+    }
+
+    public function createOrder(int $user_id, array $params = [], array $point = [])
+    {
+        $params['user_id']     = $user_id;
+        $params['order_no']    = Common::createOrderNo('C');
+        $params['status']      = 10;
+        $params['create_time'] = time();
+        if (!$order_id = self::query()->insertGetId($params)) {
+            return $this->error('订单创建失败');
+        }
+
+        $pointNum = count($point);
+
+        // 机场坐标
+        $common_airport_coord = array_values(site('common_airport_coord'));
+        // 接送机范围
+        $common_airport_range = site('common_airport_range');
+        // 市区邮编
+        $common_city_postal_code = array_values(site('common_city_postal_code'));
+
+        // 创建途径点
+        foreach ($point as $key => $item) {
+            if ($key == 0) {
+                $type = 1;
+            } elseif ($key == ($pointNum - 1)) {
+                $type = 2;
+            } else {
+                $type = 3;
+            }
+            $is_in_city    = 0;// 是否在市区:1=是,0=否
+            $is_at_airport = 0;// 是否在机场:1=是,0=否
+            // 判断 起始点 途径点 是否 在市区
+            if (in_array($item['postal_code'], $common_city_postal_code)) {
+                $is_in_city = 1;
+            }
+            // 循环判断 起始点 途径点 是否 在机场附近
+            foreach ($common_airport_coord as $coord) {
+                [$lng, $lat] = explode(',', $coord);
+                [$cha_km,$unit] = Common::distance((float)$lng, (float)$lat, (float)$item['lng'], (float)$item['lat'], 1);
+                if ($cha_km <= $common_airport_range) {
+                    $is_at_airport = 1;
+                }
+            }
+            $order_point[] = [
+                'user_id'       => $user_id,
+                'order_id'      => $order_id,
+                'order_no'      => $params['order_no'],
+                'type'          => $type,
+                'name'          => $item['name'] ?? '',
+                'phone'         => $item['phone'] ?? '',
+                'lng'           => $item['lng'],
+                'lat'           => $item['lat'],
+                'address'       => $item['address'],
+                'postal_code'   => $item['postal_code'],
+                'is_in_city'    => $is_in_city,
+                'is_at_airport' => $is_at_airport,
+                'status'        => 1,
+                'create_time'   => time()
+            ];
+        }
+        if (isset($order_point)) {
+            if (!OrderPointModel::query()->insert($order_point)) {
+                return $this->error('创建途径点失败');
+            }
+        }
+        return $this->success('提交订单成功', [
+            'order_no' => $params['order_no']
+        ]);
+    }
+
+    /**
+     * 取消订单
+     * @param string $order_no
+     * @return bool
+     */
+    public function cancel(string $order_no,int $user_id = 0,int $driver_id = 0,string $reason = '',int $is_system = 0)
+    {
+        $params['order_no'] = $order_no;
+        if (!empty($user_id)){
+            $params['user_id'] = $user_id;
+        }
+        if (!empty($driver_id)){
+            $params['driver_id'] = $driver_id;
+        }
+        $order = $this->getDetail(params: $params, with: ['user']);
+        if ((!empty($params['user_id']) && !in_array($order['status'],[10,20,30])) || (!empty($params['driver_id']) && $order['status'] != 20)){
+            $status_name = self::status[$order['status']];
+            return $this->error("当前订单{$status_name},无法取消");
+        }
+
+        if (!empty($params['user_id']) && $order['status'] == 30 && $order['driver_status'] != 1){
+            return $this->error("订单只能在司机到达出发点前才可取消");
+        }
+
+        // 如果用户编号和司机编号都没传 则只有司机未结单的状态才可取消
+        if (empty($params['user_id']) && empty($params['driver_id']) && $is_system === 0 && !in_array($order['status'],[10])){
+            $status_name = self::status[$order['status']];
+            return $this->error("当前订单{$status_name},无法取消");
+        }
+
+        // 订单取消原因
+        if (!empty($driver_id)){
+            $reason = "司机已取消订单:{$params['order_no']}";
+            // 发送系统消息
+            MessageModel::add([
+                'user_id' => $order['user_id'],
+                'type'      => 1,
+                'name'      => "订单已取消",
+                'content'   => $reason,
+                'value'     => $order_no
+            ]);
+            if ($order['user']['area_code'] == '+86') {
+                $sms = new AliSms();
+                if (!$sms->send($order['user']['mobile'], [],4)) {
+                    return $this->error($sms->getMessage() ?? '发送失败');
+                }
+            } else {
+                // 国外短信
+                $text = "【好滴用车】您的好滴用车订单已被司机取消,请前往好滴用车用户端重新下单";
+                $sms  = new Sms();
+                if (!$sms->send("{$order['user']['area_code']}{$order['user']['mobile']}", $text)) {
+                    return $this->error($sms->getMessage() ?? '发送失败');
+                }
+            }
+        }else{
+            if (!empty($order['driver_id'])){
+                $reason = "用户已取消订单:{$params['order_no']},{$reason}";
+                // 发送系统消息
+                DriverMessageModel::add([
+                    'driver_id' => $order['driver_id'],
+                    'type'      => 1,
+                    'name'      => "订单已取消",
+                    'content'   => $reason,
+                    'value'     => $order_no
+                ]);
+            }elseif ($is_system === 1){
+                $reason = "系统操作:{$params['order_no']},{$reason}";
+                // 发送系统消息
+                MessageModel::add([
+                    'user_id'   => $order['user_id'],
+                    'type'      => 1,
+                    'name'      => "订单已取消",
+                    'content'   => $reason,
+                    'value'     => $order_no
+                ]);
+            }else{
+                $reason = "订单超时暂无司机接单:{$params['order_no']},{$reason}";
+                // 发送系统消息
+                MessageModel::add([
+                    'user_id'   => $order['user_id'],
+                    'type'      => 1,
+                    'name'      => "订单已取消",
+                    'content'   => $reason,
+                    'value'     => $order_no
+                ]);
+            }
+        }
+
+        $orderUp = OrderModel::query()->where('id', $order['id'])->whereIn('status',[10, 20, 30])->update([
+            'status'        => 50,
+            'cancel_reason' => $reason,
+            'update_time'   => time()
+        ]);
+        if (!$orderUp) {
+            return $this->error('操作失败');
+        }
+
+        // 在线支付需要退还支付金额
+        if ($order['pay_type'] == 1){
+            $type_name = DriverTypeBusiness::type[$order['type']] ?? '';
+            $wallet = new UserWalletModel();
+            if (!$wallet->change($order['user_id'],(float)$order['pay_amount'],"{$type_name}订单取消退还支付金额",4)){
+                return $this->error($wallet->getMessage());
+            }
+        }
+
+        // 退还优惠券
+        if (!empty($order['coupon_id'])){
+            if (!UserCouponModel::query()->where('id',$order['coupon_id'])->update(['is_use'=>0,'update_time'=>time()])){
+                return $this->error('优惠券退还失败');
+            }
+        }
+
+        // 删除司机用户聊天框
+        RedisUtil::getInstance(RedisKeyEnum::USER_DRIVER_CHAT_DEL,"user_{$order['user_id']}")->setex("driver_{$order['driver_id']}",500);
+        $tencent = new TencentIm();
+        $tencent->delete_chat("user_{$order['user_id']}","driver_{$order['driver_id']}");
+
+        return $this->success('操作成功');
+    }
+
+    /**
+     * 邀请司机接单
+     * @param int $order_id
+     * @return bool
+     */
+    public function tailwind_invite(int $order_id, int $driver_order_id)
+    {
+        $orderUp = OrderModel::query()->where('id', $order_id)->where('status', 10)->update([
+            'driver_order_id' => $driver_order_id,
+            'update_time'     => time()
+        ]);
+        if (!$orderUp) {
+            return $this->error('操作失败');
+        }
+
+        return $this->success('操作成功');
+    }
+
+    /**
+     * 加价
+     * @param int $order_id
+     * @param float $amount
+     * @return bool
+     */
+    public function addMoney(int $order_id, float $amount)
+    {
+        $orderUp = OrderModel::query()->where('id', $order_id)->update([
+            'total_amount' => Db::raw('total_amount + ' . $amount),
+            'pay_amount'   => Db::raw('pay_amount + ' . $amount),
+            'raise_amount' => Db::raw('raise_amount + ' . $amount),
+            'update_time'  => time()
+        ]);
+        if (!$orderUp) {
+            return $this->error('操作失败');
+        }
+
+        return $this->success('操作成功');
+    }
+
+    public function points()
+    {
+        return $this->hasMany(OrderPointModel::class, 'order_id', 'id');
+    }
+
+    public function user()
+    {
+        return $this->hasOne(UserModel::class, 'id', 'user_id');
+    }
+
+    public function driver()
+    {
+        return $this->hasOne(DriverModel::class, 'id', 'driver_id');
+    }
+
+    public function car_seat()
+    {
+        return $this->hasOne(CarSeatModel::class, 'id', 'car_seat_id');
+    }
+}

+ 31 - 0
app/Model/Arts/OrderPointModel.php

@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Model\Model;
+use Hyperf\DbConnection\Db;
+use function Hyperf\Config\config;
+
+class OrderPointModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'order_point';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+}

+ 43 - 0
app/Model/Arts/QuestionAnswerModel.php

@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+class QuestionAnswerModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'question_answer';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    protected int $is_status_search = 1;// 是否使用 1=是 0=否 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 是否使用 1=是 0=否 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public function searchTypeAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('type', $value);
+    }
+}

+ 43 - 0
app/Model/Arts/QuestionReasonModel.php

@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+class QuestionReasonModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'question_reason';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    protected int $is_status_search = 1;// 是否使用 1=是 0=否 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 是否使用 1=是 0=否 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public function searchTypeAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('type', $value);
+    }
+}

+ 31 - 0
app/Model/Arts/RaiseAmountModel.php

@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Model\Model;
+
+class RaiseAmountModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'raise_amount';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+    protected int $is_status_search = 1;// 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+}

+ 138 - 0
app/Model/Arts/SmsCodeModel.php

@@ -0,0 +1,138 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Master\Framework\Library\AliCloud\AliSms;
+use App\Master\Framework\Library\Twilio\Sms;
+use App\Model\Model;
+use App\Utils\Common;
+use App\Utils\RedisUtil;
+
+class SmsCodeModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'sms_code';
+
+    protected ?string $dateFormat = 'U';
+    public bool $timestamps = false;
+
+    protected int $is_status_search = 1;// 默认使用 status = 1 筛选
+    protected int $is_delete_search = 1;// 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    /**
+     * 验证码
+     * @param $phone
+     * @param $code
+     * @return array
+     */
+    public static function AuthCode($phone, $code): array
+    {
+        if ($code == 1212){
+            return ['code' => 200, 'msg' => 'success'];
+        }
+        $code = self::query()->where('phone', $phone)->where('code', $code)->where('status', 1)->where('is_delete', 0)->first();
+        if (!$code) return ['code' => 201, 'msg' => '验证码错误'];
+
+        if ($code->created_at < time() - 600) return ['code' => 201, 'msg' => '验证码已过期'];
+
+        $code->status     = 2;
+        $code->updated_at = time();
+        $code->save();
+
+        return ['code' => 200, 'msg' => 'success'];
+    }
+
+    /**
+     * 记录验证码
+     * @param string $phone
+     * @param int $code
+     * @param int $currentLimit 限制次数
+     * @param int $timeOut 限制时间
+     * @return array
+     */
+    public static function CreateCode(string $phone, int $code, int $currentLimit = 5, int $timeOut = 300): array
+    {
+        $time  = time();
+        $times = self::query()->where('phone', $phone)->whereBetween('created_at', [$time - $timeOut, $time])->count();
+
+        //验证缓存,如存在则继续限制发送,时间设置 300s
+        if (RedisUtil::getInstance(RedisKeyEnum::SEND_SMS_MOBILE, $phone)->get()) {
+            return ['code' => 201, 'msg' => '您的发送过于频繁!'];
+        }
+
+        //5分钟之内 次数超过5次,限制发送,并记录缓存
+        if ($times > $currentLimit) {
+            //每次锁定 递增 锁定时间,达到最大锁定次数则将当日无法发送
+            $end_time = Common::todayTimeRemain();//设置次日凌晨过期
+            if (!$times = RedisUtil::getInstance(RedisKeyEnum::SEND_SMS_TIMEOUT_TIMES, $phone)->tryTimes($end_time, $currentLimit)) {
+                $timeOut = $end_time;//如果达到最大锁定次数,则设置当日过期
+            } else {
+                $timeOut = $times * $timeOut;//如果未达到最大次数,则依次递增锁定时间
+
+                // 如果最大次数过期时间 超过 当日时间,则直接设置当日过期
+                if ($timeOut > $end_time) {
+                    $timeOut = $end_time;
+                }
+            }
+
+            RedisUtil::getInstance(RedisKeyEnum::SEND_SMS_MOBILE, $phone)->setex('1',$timeOut);
+            $minutes = $timeOut / 60;
+            return ['code' => 201, 'msg' => "发送过于频繁,请{$minutes}分钟后再试"];
+        }
+
+        $code = self::query()->insert([
+            'phone'      => $phone,
+            'code'       => $code,
+            'status'     => 1,
+            'created_at' => time(),
+            'updated_at' => time(),
+        ]);
+
+        if (!$code) return ['code' => 201, 'msg' => '发送失败'];
+
+        return ['code' => 200, 'msg' => 'success'];
+    }
+
+    /**
+     * 发送短信
+     * @param string $phone
+     * @param int $code
+     * @return bool
+     * @throws \Twilio\Exceptions\ConfigurationException
+     * @throws \Twilio\Exceptions\TwilioException
+     */
+    public function send(string $phone, int $code, string $area_code = '+86')
+    {
+        if ($area_code == '+86'){
+            $sms = new AliSms();
+            if (!$sms->send($phone,['code' => $code])){
+                return $this->error($sms->getMessage() ?? '发送失败',$sms->getData());
+            }
+        }else{
+            // 国外短信
+            $text = "【好滴用车】验证码:{$code},您正在使用好滴用车,此验证码10分钟内有效。";
+            $sms = new Sms();
+            if (!$sms->send("{$area_code}{$phone}",$text)){
+                return $this->error($sms->getMessage() ?? '发送失败');
+            }
+        }
+
+        return $this->success('发送成功',$sms->getData());
+    }
+}

+ 90 - 0
app/Model/Arts/UserAddressModel.php

@@ -0,0 +1,90 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+class UserAddressModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'user_address';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    protected int $is_status_search = 1;// 是否使用 1=是 0=否 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 是否使用 1=是 0=否 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public function searchUserIdAttribute($query, $value, array $params): mixed
+    {
+        if (!isset($value)) {
+            return $query;
+        }
+        return $query->where('user_id', $value);
+    }
+
+    public function dataCreateTimeAttribute($value,$params)
+    {
+        if (empty($value)){
+            return '---';
+        }
+        return date('Y-m-d H:i:s',$value);
+    }
+
+    public static function add(array $params)
+    {
+        $insert = array_merge($params,[
+            'status' => 1,
+            'create_time' => time()
+        ]);
+        return self::query()->insertGetId($insert);
+    }
+
+    public static function edit(int $id, array $params)
+    {
+        unset($params['id']);
+        $insert = array_merge($params,[
+            'update_time' => time()
+        ]);
+        $query = self::query()->where('id',$id);
+
+        if (!empty($params['user_id'])){
+            $query->where('user_id',$params['user_id']);
+        }
+
+        return $query->update($insert);
+    }
+
+    public static function del(int $id, int $user_id = 0)
+    {
+        $insert = [
+            'status' => 0,
+            'update_time' => time(),
+        ];
+        $query = self::query()->where('id',$id);
+
+        if (!empty($user_id)){
+            $query->where('user_id',$user_id);
+        }
+
+        return $query->update($insert);
+    }
+}

+ 118 - 0
app/Model/Arts/UserCouponModel.php

@@ -0,0 +1,118 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+class UserCouponModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'user_coupon';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    protected int $is_status_search = 1;// 是否使用 1=是 0=否 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 是否使用 1=是 0=否 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public function searchUserIdAttribute($query, $value, array $params): mixed
+    {
+        if (!isset($value)) {
+            return $query;
+        }
+        return $query->where('user_id', $value);
+    }
+
+    public function searchTypeAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('type', $value);
+    }
+
+    public function searchIsValidAttribute($query, $value, array $params): mixed
+    {
+        if ($value == 1) {
+            return $query->where(function ($where) {
+                $where->orWhere('valid_at', '<', time())->orWhere('is_use', '=', 1);
+            });
+        } else {
+            return $query->where('is_use', 0)->where('valid_at', '>=', time());
+        }
+    }
+
+    public function searchMinMoneyMinAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('min_money', '<=', $value);
+    }
+
+    public function searchIsUseAttribute($query, $value, array $params): mixed
+    {
+        return $query->where('is_use', $value);
+    }
+
+    public function dataCreateTimeAttribute($value, $params)
+    {
+        if (empty($value)) {
+            return '---';
+        }
+        return date('Y-m-d H:i:s', $value);
+    }
+
+    /**
+     * 获取订单优惠券
+     * @param int $user_id
+     * @param int $type
+     * @param int $coupon_id
+     * @return array
+     */
+    public function getOrderCoupon(int $user_id, int $type, int $coupon_id = 0, $total_amount = 0)
+    {
+        // 计算优惠券
+        $coupons   = (new UserCouponModel())->getList(
+            params: [
+                'user_id'       => $user_id,
+                'type'          => $type,
+                'min_money_min' => $total_amount,
+                'is_valid'      => 0,
+                'is_use'        => 0
+            ]);
+        $min_money = '0.00';
+        $money     = '0.00';
+        if (!empty($coupon_id)) {
+            foreach ($coupons as $key => $val) {
+                if ($val['id'] == $coupon_id) {
+                    $min_money = $val['min_money'];
+                    $money     = $val['money'];
+                }
+            }
+        }
+        return [
+            'min_money' => $min_money,
+            'money'     => $money,
+            'coupons'   => $coupons,
+        ];
+    }
+}

+ 51 - 0
app/Model/Arts/UserModel.php

@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\PassportEnum;
+use App\Model\Model;
+use Hyperf\Cache\Annotation\Cacheable;
+
+class UserModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'user';
+
+    protected ?string $dateFormat = 'U';
+    public bool $timestamps = false;
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    protected int $is_status_search = 1;// 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 默认使用 is_delete = 0 筛选
+
+    /**
+     * 中间件中获取用户信息
+     * @param int $user_id
+     * @return array
+     */
+    public function searchUserIdAttribute($query, $value, array $params): mixed
+    {
+        if (!isset($value)) {
+            return $query;
+        }
+        return $query->where('user_id', $value);
+    }
+    public function authUserInfo(int $user_id)
+    {
+        return (new UserModel())->getDetail(params: ['id' => $user_id]);
+    }
+}

+ 67 - 0
app/Model/Arts/UserMoneyLogModel.php

@@ -0,0 +1,67 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+class UserMoneyLogModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'user_money_log';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    protected int $is_status_search = 0;// 是否使用 1=是 0=否 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 是否使用 1=是 0=否 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public function searchUserIdAttribute($query, $value, array $params): mixed
+    {
+        if (!isset($value)) {
+            return $query;
+        }
+        return $query->where('user_id', $value);
+    }
+
+    public function searchTypeAttribute($query, $value, array $params): mixed
+    {
+        if (!isset($value)) {
+            return $query;
+        }
+        return $query->where('type', $value);
+    }
+
+    public function searchTypeInAttribute($query, $value, array $params): mixed
+    {
+        if (!isset($value)) {
+            return $query;
+        }
+        return $query->whereIn('type', $value);
+    }
+
+    public function dataCreateTimeAttribute($value,$params)
+    {
+        if (empty($value)){
+            return '---';
+        }
+        return date('Y-m-d H:i:s',$value);
+    }
+}

+ 88 - 0
app/Model/Arts/UserWalletModel.php

@@ -0,0 +1,88 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Model\Model;
+use Hyperf\DbConnection\Db;
+use function Hyperf\Config\config;
+
+class UserWalletModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'user_wallet';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    /**
+     * 获取钱包信息
+     * @param int $user_id
+     * @param string $field
+     * @return \Hyperf\Database\Model\Builder|\Hyperf\Database\Model\Model|mixed|object|string|null
+     */
+    public static function getOne(int $user_id, string $field = '')
+    {
+        //所有钱包余额
+        $wallet = self::query()->where(['user_id' => $user_id])->first();
+        if (!empty($field)) {
+            return $wallet[$field] ?? '';
+        }
+        return $wallet;
+    }
+
+    /**
+     * 钱包操作
+     * @param int $user_id
+     * @param float $money
+     * @param string $remark
+     * @param int $type
+     * @return bool
+     */
+    public function change(int $user_id, float $money, string $remark = '', int $type = 3)
+    {
+        if (!in_array($type, [3, 4])) {
+            return $this->error('余额类型错误');
+        }
+        $wallet = (new static())->getOne($user_id);
+        if ($type == 3) {
+            // 消费
+            $data      = [
+                'money'          => bcadd($wallet['money'], (string)(-$money), 2),
+                'disburse_money' => bcadd($wallet['disburse_money'], (string)$money, 2)
+            ];
+            if ($data['money'] < 0) {
+                return $this->error('余额不足');
+            }
+            $log_money = -$money;
+        } else {
+            // 退还
+            $data      = [
+                'money'          => bcadd($wallet['money'], (string)$money, 2),
+                'disburse_money' => bcadd($wallet['disburse_money'], (string)(-$money), 2)
+            ];
+            $log_money = $money;
+        }
+        if (!$this->query()->where(['user_id' => $user_id, 'money' => $wallet['money']])->update($data)) {
+            $this->error('操作失败');
+        }
+        if (!UserMoneyLogModel::query()->insert(['user_id' => $user_id, 'type' => $type, 'money' => $log_money, 'before' => $wallet['money'], 'after' => $data['money'], 'remark' => $remark, 'create_time' => time()])) {
+            $this->error('记录操作失败');
+        }
+        return $this->success('操作成功');
+    }
+}

+ 51 - 0
app/Model/Arts/VersionAppModel.php

@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+class VersionAppModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'version_app';
+
+    protected ?string $dateFormat = 'U';
+    public bool       $timestamps = false;
+
+    protected int $is_status_search = 0;// 是否使用 1=是 0=否 默认使用 status = 1 筛选
+    protected int $is_delete_search = 0;// 是否使用 1=是 0=否 默认使用 is_delete = 0 筛选
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+
+    public function searchPlatformAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('platform', $value);
+    }
+    public function searchLangAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('lang', $value);
+    }
+
+}

+ 49 - 0
app/Model/Framework/AdminSetupModel.php

@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Framework;
+
+use App\Model\Model;
+
+class AdminSetupModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'admin_setup';
+
+    protected ?string $dateFormat = 'U';
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        'id', 'name', 'table', 'value'
+    ];
+
+    public function __construct(array $attributes = [])
+    {
+        parent::__construct($attributes);
+        $this->is_delete_search = 0;
+        $this->is_status_search = 0;
+    }
+
+    public function searchTableAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('table', $value);
+    }
+
+    public function dataValueAttribute($value,$params)
+    {
+        $value = !empty($value) ? $value : '{}';
+        return json_decode($value,true);
+    }
+}

+ 293 - 0
app/Model/Model.php

@@ -0,0 +1,293 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+
+namespace App\Model;
+
+use Hyperf\DbConnection\Model\Model as BaseModel;
+use Hyperf\Stringable\Str;
+
+abstract class Model extends BaseModel
+{
+    protected $query;
+    protected array $select = [];
+    protected int $is_status_search = 1;// 默认使用 status = 1 筛选
+    protected int $is_delete_search = 1;// 默认使用 is_delete = 0 筛选
+    protected string $message = '';
+    protected array  $data    = [];
+
+    /**
+     * 筛选条件
+     *
+     * @param $query
+     * @param $value
+     * @param array $params
+     * @return mixed
+     */
+    public function searchIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('id', $value);
+    }
+
+    /**
+     * 设置 select
+     * @param array $select
+     * @return BaseModel
+     */
+    public function setSelect(array $select = ['*'])
+    {
+        $this->select = $select;
+        return $this;
+    }
+
+    /**
+     * 设置 默认使用 status = 1 筛选
+     * @param int $is_status_search
+     * @return $this
+     */
+    public function setIsStatusSearchValue(int $is_status_search = 1)
+    {
+        $this->is_status_search = $is_status_search;
+        return $this;
+    }
+
+    /**
+     * 设置 默认使用 is_delete = 0 筛选
+     * @param int $is_delete_search
+     * @return $this
+     */
+    public function setIsDeleteSearchValue(int $is_delete_search = 1)
+    {
+        $this->is_delete_search = $is_delete_search;
+        return $this;
+    }
+
+    /**
+     * 列表查询
+     *
+     * @param array $params
+     * @param array $orderBy
+     * @param array $select
+     * @param array $with
+     * @return array
+     */
+    public function getList(array $params = [], array $orderBy = [], array $select = [], array $with = [])
+    {
+        $query = $this->catchSearch($params)
+            ->catchPages($params)
+            ->sortTool($orderBy)
+            ->getQueryObj()
+            ->select(array_merge($this->select, $select));
+
+        // 模型关联
+        count($with) > 0 && $query->with($with);
+
+        // 规避 禁用 和 删除 数据
+        $this->is_status_search === 1 && $query->where('status', 1);
+        $this->is_delete_search === 1 && $query->where('is_delete', 0);
+
+        return $this->catchData($query->get()->toArray());
+    }
+
+    /**
+     * 获取总数
+     * @param array $params
+     * @return mixed
+     */
+    public function getTotal(array $params = [])
+    {
+        $query = $this->catchSearch($params)->getQueryObj();
+
+        // 规避 禁用 和 删除 数据
+        $this->is_status_search === 1 && $query->where('status', 1);
+        $this->is_delete_search === 1 && $query->where('is_delete', 0);
+
+        return $query->count();
+    }
+
+    /**
+     * 单条数据查询
+     * @param array $params
+     * @param array $orderBy
+     * @param array $select
+     * @param array $with
+     * @return array
+     */
+    public function getDetail(array $params = [], array $orderBy = [], array $select = [], array $with = [])
+    {
+        $query = $this->catchSearch($params)
+            ->catchPages($params)
+            ->sortTool($orderBy)
+            ->getQueryObj()
+            ->select(array_merge($this->select, $select));
+
+        // 模型关联
+        count($with) > 0 && $query->with($with);
+
+        // 规避 禁用 和 删除 数据
+        $this->is_status_search === 1 && $query->where('status', 1);
+        $this->is_delete_search === 1 && $query->where('is_delete', 0);
+
+        $detail = $query->first();
+        if ($detail){
+            $detail = $detail->toArray();
+        }else{
+            $detail = [];
+        }
+
+        return $this->catchData($detail);
+    }
+
+    /**
+     * 查询器
+     *
+     * @param array $params
+     * @return $this
+     */
+    protected function catchSearch(array $params = [])
+    {
+        $this->query = !empty($this->query) ? $this->query : $this;
+        if (empty($params)) {
+            return $this;
+        }
+
+        foreach ($params as $field => $value) {
+            $method = 'search' . Str::studly($field) . 'Attribute';
+            if ($value !== null && $value !== '' && method_exists($this, $method)) {
+                $this->query = $this->$method($this->query,$value,$params);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * 分页器
+     *
+     * @param $params
+     * @return $this
+     */
+    protected function catchPages($params)
+    {
+        $this->query = !empty($this->query) ? $this->query : $this;
+        if (empty($params['page']) && empty($params['size'])){
+            return $this;
+        }
+        $page = intval($params['page'] ?? 1);
+        $size = intval($params['size'] ?? 15);
+        $this->query = $this->query->offset(($page - 1) * $size)->limit($size);
+        return $this;
+    }
+
+    /**
+     * 排序工具
+     *
+     * @param $orderBy
+     * @return $this
+     */
+    protected function sortTool($orderBy)
+    {
+        $this->query = !empty($this->query) ? $this->query : $this;
+        if (empty($orderBy)){
+            return $this;
+        }
+        foreach ($orderBy as $sort=>$order){
+            if (empty($sort) || empty($order)){
+                continue;
+            }
+            $this->query = $this->query->orderBy($sort,$order);
+        }
+        return $this;
+    }
+
+    /**
+     * 数据集处理器
+     * @param array $data
+     * @return array
+     */
+    protected function catchData(array $data): array
+    {
+        if (isset($data[0])){
+            foreach ($data as $key=>$val){
+                foreach ($val as $k=>$v){
+                    $method = 'data' . Str::studly($k) . 'Attribute';
+                    if (method_exists($this, $method)) {
+                        $data[$key][$k] = $this->$method($v,$data);
+                    }
+                }
+            }
+        }else{
+            foreach ($data as $key=>$val){
+                $method = 'data' . Str::studly($key) . 'Attribute';
+                if (method_exists($this, $method)) {
+                    $data[$key] = $this->$method($val,$data);
+                }
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * @return mixed
+     */
+    protected function getQueryObj()
+    {
+        return $this->query;
+    }
+
+    /**
+     * 返回成功结果
+     * @param string $message
+     * @param array $data
+     * @return bool
+     */
+    protected function success(string $message = 'success',array $data = []): bool
+    {
+        $this->message = $message;
+        $this->data = $data;
+        return true;
+    }
+
+    /**
+     * 返回失败结果
+     * @param string $message
+     * @param array $data
+     * @return bool
+     */
+    protected function error(string $message = 'error',array $data = []): bool
+    {
+        $this->message = $message;
+        $this->data = $data;
+        return false;
+    }
+
+    /**
+     * 获取成功数据
+     * @return array
+     */
+    public function getData(): array
+    {
+        return $this->data;
+    }
+
+    /**
+     * 获取消息
+     * @return string
+     */
+    public function getMessage(): string
+    {
+        return $this->message;
+    }
+}

+ 60 - 0
app/Process/MqttProcess.php

@@ -0,0 +1,60 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Process;
+
+use App\Utils\LogUtil;
+use Hyperf\Process\AbstractProcess;
+use Hyperf\Process\Annotation\Process;
+
+#[Process(name: "mqtt_process", redirectStdinStdout: false, pipeType: 2, enableCoroutine: true)]
+class MqttProcess extends AbstractProcess
+{
+    // 日志模块名称
+    const LOG_MODULE   = 'MqttProcess';
+    const LOG_FUNCTION = 'handle';
+
+    /**
+     * 进程数量
+     */
+    public int $nums = 1;
+
+    /**
+     * 进程名称
+     */
+    public string $name = 'mqtt_process';
+
+    /**
+     * 重定向自定义进程的标准输入和输出
+     */
+    public bool $redirectStdinStdout = false;
+
+    /**
+     * 管道类型
+     */
+    public int $pipeType = 2;
+
+    /**
+     * 是否启用协程
+     */
+    public bool $enableCoroutine = true;
+
+    /**
+     * 监听订阅
+     *
+     * @return void
+     */
+    public function handle(): void
+    {
+        // 日志统一写入
+        LogUtil::getInstance("Mqtt/");//设置日志存入通道
+
+        // 业务代码
+    }
+
+    public function isEnable($server): bool
+    {
+        // 跟随服务启动一同启动
+        return false;
+    }
+}

+ 28 - 0
app/Request/Api/v1/Common/AgreementRequest.php

@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Common;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class AgreementRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     */
+    public function rules(): array
+    {
+        return [
+            'key'    => 'required|string'
+        ];
+    }
+}

+ 30 - 0
app/Request/Api/v1/Common/DirectionsRequest.php

@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Common;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class DirectionsRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     */
+    public function rules(): array
+    {
+        return [
+            'destination'    => 'required|string',
+            'origin'    => 'required|string',
+            'waypoints'    => 'nullable|string',
+        ];
+    }
+}

+ 28 - 0
app/Request/Api/v1/Common/GeocodeRequest.php

@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Common;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class GeocodeRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     */
+    public function rules(): array
+    {
+        return [
+            'latlng'    => 'required|string',
+        ];
+    }
+}

+ 71 - 0
app/Request/Api/v1/Common/MessageRequest.php

@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Common;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class MessageRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     */
+    public function rules(): array
+    {
+        return [
+            'page'    => 'required|integer',
+            'size'    => 'required|integer',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'page.required' => '页码不能为空',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'page' => '页码',
+        ];
+    }
+
+    /**
+     * 表单请求后钩子
+     * @param $validator
+     */
+    public function withValidator($validator)
+    {
+        $validator->after(function ($validator) {
+            //获取参数
+            $params = $this->validationData();
+
+            if (isset($params['page']) && $params['page'] <= 0) {
+                return $validator->errors()->add('page', 'page 数据有误');
+            }
+
+            if (isset($params['size']) && $params['page'] <= 0) {
+                return $validator->errors()->add('size', 'size 数据有误');
+            }
+
+        });
+    }
+}

+ 31 - 0
app/Request/Api/v1/Common/PlaceAutoSearchRequest.php

@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Common;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class PlaceAutoSearchRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     */
+    public function rules(): array
+    {
+        return [
+            'input'        => 'required|string',
+            'latitude'     => 'nullable|string',
+            'longitude'    => 'nullable|string',
+            'sessionToken' => 'nullable|string',
+        ];
+    }
+}

+ 28 - 0
app/Request/Api/v1/Common/PlaceDetailsRequest.php

@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Common;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class PlaceDetailsRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     */
+    public function rules(): array
+    {
+        return [
+            'place_id'    => 'required|string',
+        ];
+    }
+}

+ 28 - 0
app/Request/Api/v1/Common/PlaceSearchRequest.php

@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Common;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class PlaceSearchRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     */
+    public function rules(): array
+    {
+        return [
+            'input'    => 'required|string',
+        ];
+    }
+}

+ 28 - 0
app/Request/Api/v1/Common/VersionRequest.php

@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Common;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class VersionRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     */
+    public function rules(): array
+    {
+        return [
+            'lang'    => 'required|string'
+        ];
+    }
+}

+ 75 - 0
app/Request/Api/v1/DemoIndexRequest.php

@@ -0,0 +1,75 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class DemoIndexRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     */
+    public function rules(): array
+    {
+        return [
+            'page'    => 'nullable|integer',
+            'size'    => 'nullable|integer',
+            'push_cid' => 'nullable|string',
+            'platform' => 'nullable|string',
+            'ring_name' => 'nullable|string',
+            'order_no' => 'nullable|string',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'page.required' => '页码不能为空',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'page' => '页码',
+        ];
+    }
+
+    /**
+     * 表单请求后钩子
+     * @param $validator
+     */
+    public function withValidator($validator)
+    {
+        $validator->after(function ($validator) {
+            //获取参数
+            $params = $this->validationData();
+
+            if (isset($params['page']) && $params['page'] <= 0) {
+                return $validator->errors()->add('page', 'page 数据有误');
+            }
+
+            if (isset($params['size']) && $params['page'] <= 0) {
+                return $validator->errors()->add('size', 'size 数据有误');
+            }
+
+        });
+    }
+}

+ 29 - 0
app/Request/Api/v1/Order/AddMoneyRequest.php

@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Order;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class AddMoneyRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     */
+    public function rules(): array
+    {
+        return [
+            'order_no' => 'required|string',
+            'amount' => 'required|decimal:2',
+        ];
+    }
+}

+ 29 - 0
app/Request/Api/v1/Order/CancelRequest.php

@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Order;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class CancelRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     */
+    public function rules(): array
+    {
+        return [
+            'order_no' => 'required|string',
+            'reason' => 'required|string',
+        ];
+    }
+}

+ 28 - 0
app/Request/Api/v1/Order/DetailRequest.php

@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Order;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class DetailRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     */
+    public function rules(): array
+    {
+        return [
+            'order_no' => 'required|string',
+        ];
+    }
+}

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است