浏览代码

第一次提交,来自xiaoshan-live

lizhen_gitee 3 月之前
当前提交
99969b1058
共有 100 个文件被更改,包括 8804 次插入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. 121 0
      app/Controller/Api/Notify/TencentImController.php
  16. 53 0
      app/Controller/Api/v1/CommonController.php
  17. 84 0
      app/Controller/Api/v1/DemoController.php
  18. 804 0
      app/Controller/Api/v1/LiveController.php
  19. 62 0
      app/Controller/Api/v1/PassportController.php
  20. 32 0
      app/Controller/Api/v1/SmsController.php
  21. 170 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. 46 0
      app/Exception/Handler/ValidationExceptionHandler.php
  25. 36 0
      app/Job/DemoJob.php
  26. 87 0
      app/Job/LiveRoomDataJob.php
  27. 85 0
      app/Job/LiveRoomSendJob.php
  28. 16 0
      app/Kernel/Log.php
  29. 13 0
      app/Kernel/StdoutLogInterface.php
  30. 66 0
      app/Listener/DbQueryExecutedListener.php
  31. 41 0
      app/Listener/MqttListener.php
  32. 35 0
      app/Listener/ResumeExitCoordinatorListener.php
  33. 16 0
      app/Master/Enum/PassportEnum.php
  34. 31 0
      app/Master/Enum/RedisKeyEnum.php
  35. 113 0
      app/Master/Framework/Extend/BaseJob.php
  36. 93 0
      app/Master/Framework/Extend/BaseTask.php
  37. 335 0
      app/Master/Framework/Helper/common.php
  38. 92 0
      app/Master/Framework/Library/AliCloud/AliSms.php
  39. 137 0
      app/Master/Framework/Library/Easywechat/EasyModule.php
  40. 137 0
      app/Master/Framework/Library/Easywechat/MiniApp.php
  41. 55 0
      app/Master/Framework/Library/Easywechat/OfficialService.php
  42. 188 0
      app/Master/Framework/Library/Easywechat/PayService.php
  43. 28 0
      app/Master/Framework/Library/Extend/Core.php
  44. 22 0
      app/Master/Framework/Library/Extend/Module.php
  45. 172 0
      app/Master/Framework/Library/GeTui/Push.php
  46. 267 0
      app/Master/Framework/Library/Google/Maps.php
  47. 100 0
      app/Master/Framework/Library/Library.php
  48. 120 0
      app/Master/Framework/Library/Mqtt/MqttClient.php
  49. 43 0
      app/Master/Framework/Library/Mqtt/Subscribe.php
  50. 331 0
      app/Master/Framework/Library/Tencent/GetUserSig.php
  51. 411 0
      app/Master/Framework/Library/Tencent/TencentIm.php
  52. 53 0
      app/Master/Framework/Library/Twilio/Sms.php
  53. 159 0
      app/Middleware/ApiAgent.php
  54. 51 0
      app/Middleware/ApiSign.php
  55. 34 0
      app/Model/Arts/CityModel.php
  56. 29 0
      app/Model/Arts/DemoModel.php
  57. 43 0
      app/Model/Arts/LiveReportModel.php
  58. 142 0
      app/Model/Arts/LiveRoomAdminModel.php
  59. 66 0
      app/Model/Arts/LiveRoomBlackModel.php
  60. 127 0
      app/Model/Arts/LiveRoomFollowModel.php
  61. 49 0
      app/Model/Arts/LiveRoomGoodsModel.php
  62. 95 0
      app/Model/Arts/LiveRoomKeywordModel.php
  63. 98 0
      app/Model/Arts/LiveRoomLogLikeModel.php
  64. 32 0
      app/Model/Arts/LiveRoomLogModel.php
  65. 322 0
      app/Model/Arts/LiveRoomModel.php
  66. 43 0
      app/Model/Arts/LiveSuggestModel.php
  67. 160 0
      app/Model/Arts/SmsCodeModel.php
  68. 90 0
      app/Model/Arts/UserAddressModel.php
  69. 118 0
      app/Model/Arts/UserCouponModel.php
  70. 105 0
      app/Model/Arts/UserModel.php
  71. 88 0
      app/Model/Arts/UserWalletModel.php
  72. 51 0
      app/Model/Arts/VersionAppModel.php
  73. 49 0
      app/Model/Framework/AdminSetupModel.php
  74. 293 0
      app/Model/Model.php
  75. 73 0
      app/Process/MqttProcess.php
  76. 28 0
      app/Request/Api/v1/Common/AgreementRequest.php
  77. 71 0
      app/Request/Api/v1/Common/MessageRequest.php
  78. 28 0
      app/Request/Api/v1/Common/VersionRequest.php
  79. 71 0
      app/Request/Api/v1/DemoIndexRequest.php
  80. 49 0
      app/Request/Api/v1/Live/AdminListRequest.php
  81. 52 0
      app/Request/Api/v1/Live/AdminSetRequest.php
  82. 55 0
      app/Request/Api/v1/Live/AudienceRequest.php
  83. 54 0
      app/Request/Api/v1/Live/BlackAddRequest.php
  84. 52 0
      app/Request/Api/v1/Live/BlackRemoveRequest.php
  85. 50 0
      app/Request/Api/v1/Live/FollowRequest.php
  86. 50 0
      app/Request/Api/v1/Live/KeywordFilterAddRequest.php
  87. 50 0
      app/Request/Api/v1/Live/KeywordFilterDelRequest.php
  88. 51 0
      app/Request/Api/v1/Live/KeywordFilterListRequest.php
  89. 49 0
      app/Request/Api/v1/Live/LikeRequest.php
  90. 52 0
      app/Request/Api/v1/Live/ReportRequest.php
  91. 51 0
      app/Request/Api/v1/Live/RoomAddRequest.php
  92. 49 0
      app/Request/Api/v1/Live/RoomCloseRequest.php
  93. 49 0
      app/Request/Api/v1/Live/RoomDetailRequest.php
  94. 49 0
      app/Request/Api/v1/Live/RoomJoinRequest.php
  95. 71 0
      app/Request/Api/v1/Live/RoomListRequest.php
  96. 49 0
      app/Request/Api/v1/Live/ShutUpListRequest.php
  97. 54 0
      app/Request/Api/v1/Live/ShutUpRequest.php
  98. 52 0
      app/Request/Api/v1/Live/SuggestRequest.php
  99. 50 0
      app/Request/Api/v1/Live/TalkSetRequest.php
  100. 52 0
      app/Request/Api/v1/Live/UserInfoRequest.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' => '失败'
+            ])));
+        }
+    }
+}

+ 121 - 0
app/Controller/Api/Notify/TencentImController.php

@@ -0,0 +1,121 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controller\Api\Notify;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Master\Framework\Library\Tencent\TencentIm;
+use App\Model\Arts\LiveRoomLogModel;
+use App\Model\Arts\LiveRoomModel;
+use App\Service\QueueService;
+use App\Utils\AppResult;
+use App\Utils\LogUtil;
+use App\Utils\RedisUtil;
+use Hyperf\Di\Annotation\Inject;
+use Hyperf\HttpServer\Contract\RequestInterface;
+use Hyperf\HttpServer\Contract\ResponseInterface;
+
+class TencentImController
+{
+    // 日志模块名称
+    const LOG_MODULE = 'Notify/TencentImController';
+
+    #[Inject]
+    protected QueueService $service;
+
+    /**
+     * im 回调通知
+     * @return \Psr\Http\Message\MessageInterface|\Psr\Http\Message\ResponseInterface
+     */
+    public function callback(RequestInterface $request,ResponseInterface $response)
+    {
+        $params = $request->all();
+        $CallbackCommand = $params["CallbackCommand"] ?? '';
+        switch ($CallbackCommand) {
+            case 'Group.CallbackAfterGroupDestroyed':// 群组解散之后回调
+                LogUtil::info('群组解散之后回调',self::LOG_MODULE,__FUNCTION__);
+                $room_no = $params["GroupId"];
+                $model = new LiveRoomModel();
+                $room = $model->getDetail(params: ['room_no'=>$room_no]);
+                if (!$room){
+                    return AppResult::im_success('直播间不存在或已下播');
+                }
+                $model = new LiveRoomModel();
+                if (!$model->closeRoom($room['id'])){
+                    return AppResult::im_error($model->getMessage() ?? '操作失败');
+                }
+                break;
+            case 'Group.CallbackAfterMemberExit': // 成员离开
+                LogUtil::info('群成员离开之后回调',self::LOG_MODULE,__FUNCTION__);
+                $room_no = $params["GroupId"];
+                $members = $params["ExitMemberList"];
+                $model = new LiveRoomModel();
+                $model->setIsStatusSearchValue(0);
+                $room = $model->getDetail(params: ['room_no'=>$room_no]);
+                if (!$room){
+                    LogUtil::info('直播间不存在或已下播',self::LOG_MODULE,__FUNCTION__);
+                }
+                if($members) {
+                    foreach($members as $k => $v) {
+                        $gz_user_id = im_un_prefix($v["Member_Account"]);
+                        // 从原房间内移除此用户
+                        RedisUtil::getInstance(RedisKeyEnum::ROOM_USER_LIST,$room_no)->zRem($gz_user_id);
+                        // 扣除在线用户在房间情况
+                        RedisUtil::getInstance(RedisKeyEnum::ROOM_USER_IN)->hDel((string)$gz_user_id);
+                        if ($room){
+                            $this->service->liveRoomDataPush(['session' => $room['session'], 'ccu' => -1]);
+                            if ($room['user_id'] == $gz_user_id){
+                                // 如果是主播,则关闭直播间
+                                $model = new LiveRoomModel();
+                                $model->closeRoom($room['id']);
+
+                                // 腾讯直播解散群组
+                                $im = new TencentIm();
+                                $im->destroy_group($room_no);
+                            }
+                        }
+                    }
+                }
+                break;
+            case 'Group.CallbackOnMemberStateChange':
+                LogUtil::info('直播群成员在线状态回调',self::LOG_MODULE,__FUNCTION__);
+                $type = $params['EventType'];
+                $room_no = $params["GroupId"];
+                $members = $params["MemberList"];
+                $model = new LiveRoomModel();
+                $room = $model->where('room_no',$room_no)->whereIn('status',[1,2])->find();
+                if (!$room){
+                    LogUtil::info('直播间不存在或已下播',self::LOG_MODULE,__FUNCTION__);
+                }
+                if ($type == 'Offline'){
+                    LogUtil::info('直播群成员离线',self::LOG_MODULE,__FUNCTION__);
+                    if($members) {
+                        foreach($members as $k => $v) {
+                            $gz_user_id = im_un_prefix($v["Member_Account"]);
+                            // 从原房间内移除此用户
+                            RedisUtil::getInstance(RedisKeyEnum::ROOM_USER_LIST,$room_no)->zrem($gz_user_id);
+                            // 扣除在线用户在房间情况
+                            RedisUtil::getInstance(RedisKeyEnum::ROOM_USER_IN)->hDel($gz_user_id);
+                            if ($room){
+                                $this->service->liveRoomDataPush(['session' => $room['session'], 'ccu' => -1]);
+                                if ($room['user_id'] == $gz_user_id){
+                                    // 如果是主播,则关闭直播间
+                                    $model = new LiveRoomModel();
+                                    $model->closeRoom($room['id']);
+
+                                    // 腾讯直播解散群组
+                                    $im = new TencentIm();
+                                    $im->destroy_group($room_no);
+                                }
+                            }
+                        }
+                    }
+                }
+                break;
+        }
+
+        // 处理结果通知
+        return AppResult::im_success('处理成功');
+    }
+}

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

@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controller\Api\v1;
+
+use App\Controller\AbstractController;
+use App\Master\Framework\Library\Tencent\TencentIm;
+use App\Utils\AppResult;
+use App\Utils\Control\AuthUser;
+
+/**
+ * 公共管理
+ * CommonController
+ */
+class CommonController extends AbstractController
+{
+    // 日志模块名称
+    const LOG_MODULE = 'v1/CommonController';
+
+    public function config()
+    {
+        return AppResult::success('success',[
+            'keyword_filter' => explode('|',site('keyword_filter')),
+            'bug'=>[
+                'time' => ['刚刚','5分钟前','15分钟前','30分钟前'],
+                'question' => [
+                    [
+                        'label' => '音频异常',
+                        'child' => ['声音有回声','没有声音','电流声']
+                    ],
+                    [
+                        'label' => '卡顿问题',
+                        'child' => ['画面卡顿','音画不同步','声音卡顿']
+                    ],
+                    [
+                        'label' => '功能异常',
+                        'child' => ['发不了评论']
+                    ]
+                ]
+            ]
+        ]);
+    }
+
+    // 获取腾讯 userSig
+    public function get_user_sig()
+    {
+        $user = AuthUser::getInstance()->get();
+        return AppResult::success(result: [
+            'userSig' => (new TencentIm())->userSig("user_{$user['id']}")
+        ]);
+    }
+}

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

@@ -0,0 +1,84 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controller\Api\v1;
+
+use App\Controller\AbstractController;
+use App\Model\Arts\DemoModel;
+use App\Request\Api\v1\DemoIndexRequest;
+use App\Service\QueueService;
+use App\Utils\AppResult;
+use App\Utils\Control\AuthUser;
+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)
+    {
+        /**
+         * 当处理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 = site('logo');
+
+        // 测试投递异步队列消息
+        $this->service->demoPush(['name' => 'one'], 10);
+
+        // 携程 闭包
+        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()
+        ]);
+    }
+}

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

@@ -0,0 +1,804 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controller\Api\v1;
+
+use App\Controller\AbstractController;
+use App\Master\Enum\RedisKeyEnum;
+use App\Master\Framework\Library\Tencent\TencentIm;
+use App\Model\Arts\LiveReportModel;
+use App\Model\Arts\LiveRoomAdminModel;
+use App\Model\Arts\LiveRoomBlackModel;
+use App\Model\Arts\LiveRoomFollowModel;
+use App\Model\Arts\LiveRoomGoodsModel;
+use App\Model\Arts\LiveRoomKeywordModel;
+use App\Model\Arts\LiveRoomLogLikeModel;
+use App\Model\Arts\LiveRoomModel;
+use App\Model\Arts\LiveSuggestModel;
+use App\Model\Arts\UserModel;
+use App\Request\Api\v1\Live\AdminListRequest;
+use App\Request\Api\v1\Live\AdminSetRequest;
+use App\Request\Api\v1\Live\AudienceRequest;
+use App\Request\Api\v1\Live\BlackAddRequest;
+use App\Request\Api\v1\Live\BlackRemoveRequest;
+use App\Request\Api\v1\Live\FollowRequest;
+use App\Request\Api\v1\Live\KeywordFilterAddRequest;
+use App\Request\Api\v1\Live\KeywordFilterDelRequest;
+use App\Request\Api\v1\Live\KeywordFilterListRequest;
+use App\Request\Api\v1\Live\LikeRequest;
+use App\Request\Api\v1\Live\ReportRequest;
+use App\Request\Api\v1\Live\RoomAddRequest;
+use App\Request\Api\v1\Live\RoomCloseRequest;
+use App\Request\Api\v1\Live\RoomDetailRequest;
+use App\Request\Api\v1\Live\RoomJoinRequest;
+use App\Request\Api\v1\Live\RoomListRequest;
+use App\Request\Api\v1\Live\ShutUpListRequest;
+use App\Request\Api\v1\Live\ShutUpRequest;
+use App\Request\Api\v1\Live\SuggestRequest;
+use App\Request\Api\v1\Live\TalkSetRequest;
+use App\Request\Api\v1\Live\UserInfoRequest;
+use App\Request\Api\v1\Live\UserRemoveRequest;
+use App\Service\QueueService;
+use App\Utils\AppResult;
+use App\Utils\Control\AuthUser;
+use App\Utils\LogUtil;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+use Hyperf\Di\Annotation\Inject;
+use function Hyperf\Stringable\str;
+
+/**
+ * Demo
+ * 示例
+ */
+class LiveController extends AbstractController
+{
+    // 日志模块名称
+    const LOG_MODULE = 'v1/LiveController';
+
+    #[Inject]
+    protected QueueService $service;
+
+    // 直播间列表
+    public function room_list(RoomListRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        $list   = $model->getList(
+            params : $params,
+            orderBy: ['weigh' => 'desc'],
+            select : ['id', 'user_id', 'room_no', 'name', 'logo', 'image', 'session', 'status', 'create_time'],
+            with   : [
+                'log' => function ($query) {
+                    $query->select(['id', 'session', 'like', 'ccu', 'goods_sales', 'share_num', 'follow_num', 'duration', 'open_time', 'close_time']);
+                }
+            ]
+        );
+
+        $im = new TencentIm();
+        foreach ($list as $key => $val) {
+            $list[$key]['push_url'] = $im->getLivePushUrl($val['room_no']);
+            $list[$key]['play_url'] = $im->getLivePlayUrl($val['room_no']);
+        }
+        return AppResult::success('success', $list);
+    }
+
+    // 房间详情
+    public function room_detail(RoomDetailRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        $info   = $model->getDetail(
+            params: $params,
+            select: ['id', 'user_id', 'room_no', 'name', 'logo', 'image', 'session', 'follow_num', 'status', 'create_time'],
+            with  : [
+                'log'  => function ($query) {
+                    $query->select(['id', 'session', 'like', 'ccu', 'goods_sales', 'share_num', 'follow_num', 'duration', 'open_time', 'close_time']);
+                },
+                'user' => function ($query) {
+                    $query->select(['id', 'nickname', 'avatar', 'gender']);
+                }
+            ]
+        );
+
+        $info['user']['chat_id'] = im_prefix($info['user']['id']);
+        $info['user']['avatar']  = cdn_url($info['user']['avatar']);
+
+        // 判断用户身份
+        $info['role'] = 3;// 观众
+        if ($user['id'] == $info['user_id']) {
+            $info['role'] = 1;// 房主
+        } else {
+            $model = new LiveRoomAdminModel();
+            $admin = $model->getAdmin($user['id'],$params['room_no']);
+            if (!empty($admin)) {
+                $info['role'] = 2;// 场控
+            }
+        }
+
+        // 如果不是自己的直播间,需要判断关注按钮
+        $info['is_follow'] = 2;//状态:0=未关注,1=关注,2=主播自己
+        if ($info['user_id'] != $user['id']) {
+            $info['is_follow'] = 0;
+            // 查询是否关注对方
+            if (LiveRoomFollowModel::query()->where(['room_id' => $info['id'], 'user_id' => $user['id'], 'status' => 1])->exists()) {
+                $info['is_follow'] = 1; //状态:0=关注,1=互为关注
+            }
+        }
+
+        // 配置直播推流连接
+        $im               = new TencentIm();
+        $info['push_url'] = $im->getLivePushUrl($info['room_no']);
+        $info['play_url'] = $im->getLivePlayUrl($info['room_no']);
+        return AppResult::success('获取成功', $info);
+    }
+
+    // 创建直播间
+    public function room_add(RoomAddRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+        // 校验开播资格
+        if ($user['group_id'] !== 2) {
+            return AppResult::error('抱歉,您没有权限');
+        }
+
+        Db::beginTransaction();
+        try {
+            // 创建直播房间
+            $model = new LiveRoomModel();
+            if (!$model->createRoom($user, (string)$params['name'], (string)($params['image'] ?? ''))) {
+                Db::rollBack();
+                return AppResult::error($model->getMessage() ?? '操作失败');
+            }
+            $data        = $model->getData();
+            $room_status = $data['status'] ?? 0;// 直播房间状态
+            RedisUtil::getInstance(RedisKeyEnum::ROOM_USER_LIST, $data['room_no'])->del();
+            // 如果没开播
+            if ($room_status == 0) {
+                // IM直播创建房间 && 创建群组
+                $im = new TencentIm();
+                $im->create_group(im_prefix($user['id']), $data['room_no'], (string)$data['room_name'], notification: site('live_notice') ?? '');
+            }
+            Db::commit();
+        } catch (\Exception $e) {
+            Db::rollBack();
+            return AppResult::error($e->getMessage() ?? '操作失败');
+        }
+        // 配置直播推流连接
+        $im = new TencentIm();
+
+        // 设置群属性
+        $counter[] = [
+            'key'   => 'talk_status',
+            'value' => (string)$data['talk_status']
+        ];
+        $counter[] = [
+            'key'   => 'top_avatar',
+            'value' => ''
+        ];
+        $counter[] = [
+            'key'   => 'like',
+            'value' => '0'
+        ];
+        $counter[] = [
+            'key'   => 'online_member_num',
+            'value' => '0'
+        ];
+        $im->modify_group_attr($data['room_no'], $counter);
+
+        return AppResult::success('创建成功', [
+            'room_no'   => $data['room_no'],
+            'room_name' => $data['room_name'],
+            'push_url'  => $im->getLivePushUrl($data['room_no']),
+            'play_url'  => $im->getLivePlayUrl($data['room_no']),
+        ]);
+    }
+
+    // 关闭直播间
+    public function room_close(RoomCloseRequest $request)
+    {
+        $params            = $request->validated();
+        $user              = AuthUser::getInstance()->get();
+        $params['user_id'] = $user['id'];
+        $model             = new LiveRoomModel();
+        $room              = $model->getDetail(params: $params,with: ['log']);
+        if (!$room) {
+            return AppResult::error('直播间不存在或已关闭');
+        }
+        Db::beginTransaction();
+        try {
+            // 创建直播房间
+            $model = new LiveRoomModel();
+            if (!$model->closeRoom($room['id'])) {
+                Db::rollBack();
+                return AppResult::error($model->getMessage() ?? '操作失败');
+            }
+
+            LiveRoomGoodsModel::query()->where('room_no',$room['room_no'])->where('is_top',1)->update(['is_top' => 0]);
+
+            // 腾讯直播创建房间 && 创建群组
+            $tanIm = new TencentIm();
+            $tanIm->destroy_group($room['room_no']);
+
+            Db::commit();
+        } catch (\Exception $e) {
+            Db::rollBack();
+            return AppResult::error($e->getMessage() ?? '操作失败');
+        }
+        $room['log']['close_time'] = time();
+        $room['log']['duration'] = time() - $room['log']['open_time'];
+        return AppResult::success('关闭成功',$room['log']);
+    }
+
+    // 进入房间
+    public function room_join(RoomJoinRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        $info   = $model->getDetail(params: $params);
+        if (!$info) {
+            return AppResult::error('直播间已关闭');
+        }
+
+        if (RedisUtil::getInstance(RedisKeyEnum::ROOM_BLACK,$info['room_no'],im_prefix($user['id']))->get()){
+            return AppResult::error('您被改直播间拉入黑名单,暂时无法进入');
+        }
+
+        // 处理直播间在线人数
+        // 判断当前用户是否存在在直播间通过切换过来的
+        $ROOM_USER_IN = RedisUtil::getInstance(RedisKeyEnum::ROOM_USER_IN)->hGet((string)$user['id']);
+        if (!$ROOM_USER_IN || $ROOM_USER_IN != $info['id']) {
+            if ($ROOM_USER_IN != $info['id']) {
+                // 从原房间内移除此用户
+                RedisUtil::getInstance(RedisKeyEnum::ROOM_USER_LIST, $info['room_no'])->zRem($user['id']);
+                // 扣除在线用户在房间情况
+                RedisUtil::getInstance(RedisKeyEnum::ROOM_USER_IN)->hDel((string)$user['id']);
+            }
+            // 记录在线用户在房间情况
+            RedisUtil::getInstance(RedisKeyEnum::ROOM_USER_IN)->hSet((string)$user['id'], $info['room_no']);
+            $value = LiveRoomLogLikeModel::query()->where('user_id', $user['id'])->where('room_no', $info['room_no'])->value('like');
+            RedisUtil::getInstance(RedisKeyEnum::ROOM_USER_LIST, $info['room_no'])->zAdd((int)($value ?: 0), $user['id']);
+        }
+
+        // 直播间观看人数加一
+        $this->service->liveRoomDataPush(['session' => $info['session'], 'ccu' => 1]);
+        return AppResult::success('进入成功');
+    }
+
+    // 离开房间
+    public function room_leave(RoomJoinRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        $info   = $model->getDetail(params: $params);
+        if (!$info) {
+            return AppResult::error('直播间已关闭');
+        }
+
+        // 从原房间内移除此用户
+        RedisUtil::getInstance(RedisKeyEnum::ROOM_USER_LIST, $info['room_no'])->zRem($user['id']);
+        // 扣除在线用户在房间情况
+        RedisUtil::getInstance(RedisKeyEnum::ROOM_USER_IN)->hDel((string)$user['id']);
+        if ($info) {
+            $this->service->liveRoomDataPush(['session' => $info['session'], 'ccu' => -1]);
+            if ($info['user_id'] == $user['id']) {
+                // 如果是主播,则关闭直播间
+                $model = new LiveRoomModel();
+                $model->closeRoom($info['id']);
+
+                // 腾讯直播解散群组
+                $im = new TencentIm();
+                $im->destroy_group($info['room_no']);
+            }
+        }
+        return AppResult::success('已离开直播间');
+    }
+
+    // 房管列表
+    public function admin_list(AdminListRequest $request)
+    {
+        $params = $request->validated();
+        $model  = new LiveRoomAdminModel();
+        $model->setIsStatusSearchValue();
+        $list = $model->getList(
+            params: $params,
+            with  : [
+                'user' => function ($query) {
+                    $query->select(['id', 'nickname', 'avatar']);
+                }
+            ]
+        );
+        foreach ($list as $key => $val) {
+            $list[$key]['user']['avatar'] = cdn_url($val['user']['avatar']);
+            $list[$key]['user']['chat_id'] = im_prefix($val['user']['id']);
+        }
+        return AppResult::success('获取成功', $list);
+    }
+
+    // 设置超管
+    public function admin_add(AdminSetRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomAdminModel();
+        if (!$model->add($user['id'], (string)$params['room_no'], im_un_prefix($params['chat_id']))) {
+            return AppResult::error($model->getMessage() ?? '操作失败');
+        }
+        return AppResult::success('设置成功');
+    }
+
+    // 删除超管
+    public function admin_del(AdminSetRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomAdminModel();
+        if (!$model->del($user['id'], (string)$params['room_no'], im_un_prefix($params['chat_id']))) {
+            return AppResult::error($model->getMessage() ?? '操作失败');
+        }
+        return AppResult::success('删除成功');
+    }
+
+    // 房间观众信息
+    public function user_info(UserInfoRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+        $model = new LiveRoomModel();
+        if (!$room = $model->getDetail(params: ['room_no' => $params['room_no']])) {
+            return AppResult::error('直播间已关闭');
+        }
+
+        $userInfo = UserModel::query()->where('id',im_un_prefix($params['chat_id']))->where('status',1)->first();
+        if (!$userInfo){
+            return AppResult::error('用户不存在');
+        }
+
+        // 判断当前用户身份
+        $user_role = 3;// 观众
+        if ($user['id'] == $room['user_id']) {
+            $user_role = 1;// 房主
+        } else {
+            $model = new LiveRoomAdminModel();
+            $admin = $model->getAdmin($user['id'],$params['room_no']);
+            if (!empty($admin)) {
+                $user_role = 2;// 场控
+            }
+        }
+
+        // 判断查询的用户身份
+        $info_role = 3;// 观众
+        if ($userInfo->id == $room['user_id']) {
+            $info_role = 1;// 房主
+        } else {
+            $model = new LiveRoomAdminModel();
+            $admin = $model->getAdmin($userInfo->id,$params['room_no']);
+            if (!empty($admin)) {
+                $info_role = 2;// 场控
+            }
+        }
+
+        return AppResult::success('操作成功',[
+            'info' => [
+                'id' => $userInfo->id,
+                'chat_id' => $params['chat_id'],
+                'nickname' => $userInfo->nickname,
+                'avatar' => cdn_url($userInfo->avatar),
+                'role' => $info_role,
+                'is_shut_up' => RedisUtil::getInstance(RedisKeyEnum::ROOM_SHUT_UP,$params['room_no'],$params['chat_id'])->get() ? 1 : 0,
+                'is_black' => RedisUtil::getInstance(RedisKeyEnum::ROOM_BLACK,$params['room_no'],$params['chat_id'])->get() ? 1 : 0,
+            ],
+            'me' => [
+                'role' => $user_role
+            ]
+        ]);
+    }
+
+    // 移除房间观众
+    public function user_remove(UserRemoveRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+        if ($user['id'] == im_un_prefix($params['chat_id'])) {
+            return AppResult::error('无法对自己操作');
+        }
+        $model = new LiveRoomModel();
+        if (!$room = $model->getDetail(params: ['room_no' => $params['room_no']])) {
+            return AppResult::error('直播间已关闭');
+        }
+        if ($room['user_id'] != $user['id']) {
+            $model = new LiveRoomAdminModel();
+            if (!$model->getAdmin($user['id'], $params['room_no'])) {
+                return AppResult::error('未拥有此权限');
+            }
+        }
+        // 腾讯直播创建房间 && 创建群组
+        $im = new TencentIm();
+        if (!$im->delete_group_member($params['room_no'], im_prefix($params['chat_id']))) {
+            return AppResult::error($im->getMessage() ?? '操作失败');
+        }
+
+        // 移除观众列表
+        RedisUtil::getInstance(RedisKeyEnum::ROOM_USER_LIST, $room['room_no'])->zRem(im_un_prefix($params['chat_id']));
+
+        // 直播间人数减一
+        $this->service->liveRoomDataPush(['session' => $room['session'], 'ccu' => -1]);
+        return AppResult::success('操作成功');
+    }
+
+    // 禁言列表
+    public function shut_up_list(ShutUpListRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        if (!$room = $model->getDetail(params: ['room_no' => $params['room_no']])) {
+            return AppResult::error('直播间已关闭');
+        }
+        if ($room['user_id'] != $user['id']) {
+            $model = new LiveRoomAdminModel();
+            if (!$model->getAdmin($user['id'], $params['room_no'])) {
+                return AppResult::error('未拥有此权限');
+            }
+        }
+        $im = new TencentIm();
+        if (!$im->get_group_muted_account($params['room_no'])) {
+            return AppResult::error($im->getMessage() ?? '操作失败');
+        }
+        $data = $im->getData();
+        $ids  = [];
+        foreach ($data['MutedAccountList'] as $item) {
+            $ids[] = im_un_prefix($item['Member_Account']);
+        }
+        $model = new UserModel();
+        $list  = $model->getList(params: ['ids' => $ids], select: ['id', 'nickname', 'avatar']);
+
+        foreach ($list as $key => $val) {
+            $list[$key]['avatar']  = cdn_url($val['avatar']);
+            $list[$key]['chat_id'] = im_prefix($val['id']);
+        }
+
+        return AppResult::success('操作成功', $list);
+    }
+
+    // 禁言
+    public function shut_up(ShutUpRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+        if ($user['id'] == im_un_prefix($params['chat_id'])) {
+            return AppResult::error('无法对自己操作');
+        }
+        $model = new LiveRoomModel();
+        if (!$model->shut_up($user['id'], (string)$params['room_no'], im_un_prefix($params['chat_id']), (int)($params['time'] ?? 86400))) {
+            return AppResult::error($model->getMessage() ?? '操作失败');
+        }
+        return AppResult::success('操作成功');
+    }
+
+    // 黑名单列表
+    public function black_list(ShutUpListRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        if (!$room = $model->getDetail(params: ['room_no' => $params['room_no']])) {
+            return AppResult::error('直播间已关闭');
+        }
+        if ($room['user_id'] != $user['id']) {
+            $model = new LiveRoomAdminModel();
+            if (!$model->getAdmin($user['id'], $params['room_no'])) {
+                return AppResult::error('未拥有此权限');
+            }
+        }
+//        $im = new TencentIm();
+//        if (!$im->get_group_ban_member($params['room_no'])) {
+//            return AppResult::error($im->getMessage() ?? '操作失败');
+//        }
+//        $data = $im->getData();
+//        $ids  = [];
+//        foreach ($data['BannedAccountList'] as $item) {
+//            $ids[] = im_un_prefix($item['Member_Account']);
+//        }
+        $ids = LiveRoomBlackModel::query()->where('room_no',$params['room_no'])->where('status',1)->where('end_time','>',time())->pluck('user_id')->toArray();
+        $model = new UserModel();
+        $list  = $model->getList(params: ['ids' => $ids], select: ['id', 'nickname', 'avatar']);
+
+        foreach ($list as $key => $val) {
+            $list[$key]['avatar']  = cdn_url($val['avatar']);
+            $list[$key]['chat_id'] = im_prefix($val['id']);
+        }
+
+        return AppResult::success('操作成功', $list);
+    }
+
+    // 封禁 黑名单
+    public function black_add(BlackAddRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+        if ($user['id'] == im_un_prefix($params['chat_id'])) {
+            return AppResult::error('无法对自己操作');
+        }
+        $model = new LiveRoomModel();
+        if (!$model->black_add($user['id'], (string)$params['room_no'], im_un_prefix($params['chat_id']), (int)($params['time'] ?? 86400))) {
+            return AppResult::error($model->getMessage() ?? '操作失败');
+        }
+        return AppResult::success('操作成功');
+    }
+
+    // 移除 封禁 黑名单
+    public function black_remove(BlackRemoveRequest $request)
+    {
+        $params = $request->validated();
+        $user   = AuthUser::getInstance()->get();
+        if ($user['id'] == im_un_prefix($params['chat_id'])) {
+            return AppResult::error('无法对自己操作');
+        }
+        $model = new LiveRoomModel();
+        if (!$model->black_remove($user['id'], (string)$params['room_no'], im_un_prefix($params['chat_id']))) {
+            return AppResult::error($model->getMessage() ?? '操作失败');
+        }
+        return AppResult::success('操作成功');
+    }
+
+    // 直播间观众列表(已关注的)
+    public function audience(AudienceRequest $request)
+    {
+        $params = $request->validated();// 获取校验参数结果
+        $model  = new LiveRoomFollowModel();
+        $model->setIsStatusSearchValue();
+        $list = $model->getList(
+            params : $params,
+            orderBy: ['id' => 'desc'],
+            with   : [
+                'user' => function ($with) use ($params) {
+                    $with->select(['id', 'nickname', 'avatar']);
+                }
+            ]
+        );
+        // 查询当前直播间管理员id
+        $adminIds = LiveRoomAdminModel::query()->where('room_no', $params['room_no'])->where('status', 1)->pluck('admin_id')->toArray();
+        foreach ($list as $key => $val) {
+            $list[$key]['is_admin'] = in_array($val['user_id'], $adminIds) ? 1 : 0;
+            $list[$key]['user']     = [
+                'chat_id'  => im_prefix($val['user']['id']),
+                'avatar'   => cdn_url($val['user']['avatar']),
+                'nickname' => $val['user']['nickname'],
+            ];
+        }
+        return AppResult::success('获取成功', $list);
+    }
+
+    // 直播间观众榜单
+    public function top(LikeRequest $request)
+    {
+        $params = $request->validated();// 获取校验参数结果
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        $room   = $model->getDetail(params: $params);
+        if (!$room) {
+            return AppResult::error('直播间异常');
+        }
+        $model  = new LiveRoomLogLikeModel();
+        $list   = $model->getTopList($room['room_no'], $room['user_id']);
+        $meRank = RedisUtil::getInstance(RedisKeyEnum::ROOM_USER_LIST, $room['room_no'])->zRevRank($user['id']);
+        return AppResult::success('success', [
+            'me'   => [
+                'id'  => $user['id'],
+                'chat_id'  => im_prefix($user['id']),
+                'nickname' => $user['nickname'],
+                'avatar'   => cdn_url($user['avatar']),
+                'rank'     => $meRank >= 0 ? $meRank + 1 : ''
+            ],
+            'list' => $list
+        ]);
+    }
+
+    // 点赞
+    public function like(LikeRequest $request)
+    {
+        $params = $request->validated();// 获取校验参数结果
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        $room   = $model->getDetail(params: $params);
+        if (!$room) {
+            return AppResult::error('直播间异常');
+        }
+        // 不可以赠送给自己
+        if ($user['id'] == $room['user_id']) {
+            return AppResult::error('不可以给自己点赞');
+        }
+
+        // 加入点赞队列
+        $this->service->liveRoomDataPush([
+            'user_id' => $user['id'],
+            'room_id' => $room['id'],
+            'room_no' => $room['room_no'],
+            'session' => $room['session'],
+            'like'    => 1
+        ]);
+
+        return AppResult::success('点赞成功');
+    }
+
+    // 关注 取消关注
+    public function follow(FollowRequest $request)
+    {
+        $params = $request->validated();// 获取校验参数结果
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        $room   = $model->getDetail(params: $params);
+        if (!$room) {
+            return AppResult::error('直播间异常');
+        }
+        // 不可以赠送给自己
+        if ($user['id'] == $room['user_id']) {
+            return AppResult::error('不可以关注自己');
+        }
+
+        $model = new LiveRoomFollowModel();
+
+        if ($params['status'] == 1) {
+            if (!$model->follow($user['id'], $room['id'], $room['room_no'], $room['session'])) {
+                return AppResult::error($model->getMessage() ?? '关注失败');
+            }
+        } else {
+            if (!$model->cancelFollow($user['id'], $room['id'], $room['room_no'])) {
+                return AppResult::error($model->getMessage() ?? '取关失败');
+            }
+        }
+
+        return AppResult::success($model->getMessage() ?? '操作成功');
+    }
+
+    // 发言权限
+    public function talk_set(TalkSetRequest $request)
+    {
+        $params = $request->validated();// 获取校验参数结果
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        $room   = $model->getDetail(params: $params);
+        if (!$room) {
+            return AppResult::error('直播间异常');
+        }
+        // 不可以赠送给自己
+        if ($user['id'] != $room['user_id']) {
+            $model = new LiveRoomAdminModel();
+            if (!$model->getAdmin($user['id'], $room['room_no'])) {
+                return AppResult::error('未拥有此权限');
+            }
+        }
+
+        $model = new LiveRoomModel();
+        if (!$model->query()->where('id', $room['id'])->update(['talk_status' => $params['talk_status'], 'update_time' => time()])) {
+            return AppResult::error('操作失败');
+        }
+
+        // 设置群属性
+        $counter[] = [
+            'key'   => 'talk_status',
+            'value' => (string)$params['talk_status']
+        ];
+        $im        = new TencentIm();
+        if (!$im->modify_group_attr($room['room_no'], $counter)) {
+            return AppResult::error($im->getMessage() ?? '操作成功', $im->getData());
+        }
+        return AppResult::success('操作成功');
+    }
+
+    // 卡顿上报
+    public function suggest(SuggestRequest $request)
+    {
+        $params = $request->validated();// 获取校验参数结果
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        $room   = $model->getDetail(params: $params);
+        if (!$room) {
+            return AppResult::error('直播间异常');
+        }
+        $params['room_id'] = $room['id'];
+        $model             = new LiveSuggestModel();
+        if (!$model->add($user['id'], $params)) {
+            return AppResult::error('操作失败');
+        }
+        return AppResult::success('上报成功');
+    }
+
+    // 举报
+    public function report(ReportRequest $request)
+    {
+        $params = $request->validated();// 获取校验参数结果
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        $room   = $model->getDetail(params: $params);
+        if (!$room) {
+            return AppResult::error('直播间异常');
+        }
+        $to_user = (new UserModel())->getDetail(params: ['id' => im_un_prefix($params['to_chat_id'])]);
+        if (!$to_user) {
+            return AppResult::error('举报用户不存在');
+        }
+        $params['room_id']    = $room['id'];
+        $params['to_user_id'] = im_un_prefix($params['to_chat_id']);
+        unset($params['to_chat_id']);
+        $model = new LiveReportModel();
+        if (!$model->add($user['id'], $params)) {
+            return AppResult::error('操作失败');
+        }
+        return AppResult::success('举报成功');
+    }
+
+    // 敏感词
+    public function keyword_filter_list(KeywordFilterListRequest $request)
+    {
+        $params = $request->validated();// 获取校验参数结果
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        $room   = $model->getDetail(params: $params);
+        if (!$room) {
+            return AppResult::error('直播间异常');
+        }
+
+        $model = new LiveRoomKeywordModel();
+        return AppResult::success('获取成功',$model->getList($params));
+    }
+
+    // 敏感词
+    public function keyword_filter_add(KeywordFilterAddRequest $request)
+    {
+        $params = $request->validated();// 获取校验参数结果
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        $room   = $model->getDetail(params: $params);
+        if (!$room) {
+            return AppResult::error('直播间异常');
+        }
+
+        if ($room['user_id'] != $user['id']) {
+            $model = new LiveRoomAdminModel();
+            if (!$model->getAdmin($user['id'], $params['room_no'])) {
+                return AppResult::error('未拥有此权限');
+            }
+        }
+
+        $model = new LiveRoomKeywordModel();
+        if (!$model->add($user['id'], $room['id'], $room['room_no'], $params['keyword'])) {
+            return AppResult::error($model->getMessage() ?? '操作失败');
+        }
+
+        return AppResult::success($model->getMessage() ?? '操作成功');
+    }
+
+    // 敏感词 删除
+    public function keyword_filter_del(KeywordFilterDelRequest $request)
+    {
+        $params = $request->validated();// 获取校验参数结果
+        $user   = AuthUser::getInstance()->get();
+        $model  = new LiveRoomModel();
+        $room   = $model->getDetail(params: $params);
+        if (!$room) {
+            return AppResult::error('直播间异常');
+        }
+
+        if ($room['user_id'] != $user['id']) {
+            $model = new LiveRoomAdminModel();
+            if (!$model->getAdmin($user['id'], $params['room_no'])) {
+                return AppResult::error('未拥有此权限');
+            }
+        }
+
+        $model = new LiveRoomKeywordModel();
+        if (!$model->del($params['keyword_id'])) {
+            return AppResult::error($model->getMessage() ?? '操作失败');
+        }
+
+        return AppResult::success($model->getMessage() ?? '操作成功');
+    }
+}

+ 62 - 0
app/Controller/Api/v1/PassportController.php

@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controller\Api\v1;
+
+use App\Controller\AbstractController;
+use App\Model\Arts\SmsCodeModel;
+use App\Model\Arts\UserModel;
+use App\Request\Api\v1\Passport\LoginMobileRequest;
+use App\Utils\AppResult;
+use App\Utils\Encrypt\TokenFast;
+use Hyperf\Stringable\Str;
+
+/**
+ * 通行证
+ * 示例
+ */
+class PassportController extends AbstractController
+{
+    // 日志模块名称
+    const LOG_MODULE = 'v1/PassportController';
+
+    /**
+     * 手机号登录注册
+     * @param LoginMobileRequest $request
+     * @return \Psr\Http\Message\MessageInterface|\Psr\Http\Message\ResponseInterface
+     * @throws \Psr\Container\ContainerExceptionInterface
+     * @throws \Psr\Container\NotFoundExceptionInterface
+     */
+    public function login_mobile(LoginMobileRequest $request)
+    {
+        $params = $request->validated();
+
+        // 校验验证码
+        $sms = new SmsCodeModel();
+        if (!$sms->check($params['mobile'], $params['captcha'], $params['event'], 50)) {
+            return AppResult::error($sms->getMessage());
+        }
+        $UserModel = new UserModel();
+        if ($user = $UserModel->getByMobile($params['mobile'])){
+            if ($user['status'] != 1){
+                return AppResult::error('账号被锁定,无法登录');
+            }
+
+        }else{
+            if (!$UserModel->register(mobile: $params['mobile'])){
+                return AppResult::error('注册失败');
+            }
+            $user = $UserModel->getData();
+        }
+        $token = (string)Str::uuid();
+        if (!TokenFast::set($token,$user['id'],2592000)){
+            return AppResult::error('获取token失败');
+        }
+        $sms->flush($params['mobile'],$params['event']);
+        return AppResult::success('登录成功',[
+            'token' => $token
+        ]);
+    }
+
+}

+ 32 - 0
app/Controller/Api/v1/SmsController.php

@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controller\Api\v1;
+
+use App\Controller\AbstractController;
+use App\Model\Arts\SmsCodeModel;
+use App\Request\Api\v1\Sms\SendRequest;
+use App\Utils\AppResult;
+
+/**
+ * 短信
+ * 示例
+ */
+class SmsController extends AbstractController
+{
+    // 日志模块名称
+    const LOG_MODULE = 'v1/SmsController';
+
+    public function send(SendRequest $request)
+    {
+        $params = $request->validated();
+        $sms = new SmsCodeModel();
+        if (!$sms->send($params['mobile'], $params['event'])) {
+            return AppResult::error($sms->getMessage());
+        }
+
+        return AppResult::success('发送成功');
+    }
+
+}

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

@@ -0,0 +1,170 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controller\Api\v1;
+
+use App\Controller\AbstractController;
+use App\Model\Arts\UserAddressModel;
+use App\Model\Arts\UserCouponModel;
+use App\Model\Arts\UserModel;
+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\EditRequest;
+use App\Utils\AppResult;
+use App\Utils\Control\AuthUser;
+
+/**
+ * 用户管理
+ * 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('删除成功');
+    }
+}

+ 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)
+    {
+        $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)
+    {
+        $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;
+    }
+}

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

@@ -0,0 +1,46 @@
+<?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();
+        // 统一返回格式
+        return AppResult::error($throwable->validator->errors()->first());
+//        /** @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;
+    }
+}

+ 36 - 0
app/Job/DemoJob.php

@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Job;
+
+use App\Master\Framework\Extend\BaseJob;
+use App\Utils\LogUtil;
+
+class DemoJob extends BaseJob
+{
+    //日志板块
+    protected string $LOG_MODULE = 'DemoJob';
+
+    /**
+     * 任务执行失败后的重试次数,即最大执行次数为 $maxAttempts+1 次
+     */
+    protected int $maxAttempts = 2;
+
+    public function __construct($params)
+    {
+        parent::__construct($params);
+    }
+
+    /**
+     * 执行
+     * @param $params
+     * @return bool
+     */
+    protected function do($params): bool
+    {
+        LogUtil::info('do', $this->LOG_MODULE, __FUNCTION__, '123123213');
+        // 业务代码
+        return $this->success('执行成功',$params);
+    }
+}

+ 87 - 0
app/Job/LiveRoomDataJob.php

@@ -0,0 +1,87 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Job;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Master\Framework\Extend\BaseJob;
+use App\Model\Arts\LiveRoomLogLikeModel;
+use App\Model\Arts\LiveRoomLogModel;
+use App\Utils\LogUtil;
+use App\Utils\RedisUtil;
+
+class LiveRoomDataJob extends BaseJob
+{
+    //日志板块
+    protected string $LOG_MODULE = 'LiveRoomDataJob';
+
+    /**
+     * 任务执行失败后的重试次数,即最大执行次数为 $maxAttempts+1 次
+     */
+    protected int $maxAttempts = 2;
+
+    public function __construct($params)
+    {
+        parent::__construct($params);
+    }
+
+    /**
+     * 执行
+     * @param $params
+     * @return bool
+     */
+    protected function do($params): bool
+    {
+        // 增加 点赞
+        if (!empty($params['like'])) {
+            LogUtil::info('增加点赞', $this->LOG_MODULE, __FUNCTION__);
+            if ($params['like'] > 0) {
+                $res = LiveRoomLogModel::where('session', $params['session'])->increment('like', $params['like']);
+                LiveRoomLogLikeModel::likes($params['user_id'], $params['room_id'], $params['room_no'], $params['session'], $params['like']);
+            } else {
+                $res = LiveRoomLogModel::where('session', $params['session'])->decrement('like', $params['like'] * -1);
+            }
+        }
+        // 增加ccu
+        if (!empty($params['ccu'])) {
+            LogUtil::info('增加ccu', $this->LOG_MODULE, __FUNCTION__);
+            if ($params['ccu'] > 0) {
+                $res = LiveRoomLogModel::where('session', $params['session'])->increment('ccu', $params['ccu']);
+            } else {
+                $res = LiveRoomLogModel::where('session', $params['session'])->decrement('ccu', $params['ccu'] * -1);
+            }
+        }
+        // 增加分享次数
+        if (!empty($params['share_num'])) {
+            LogUtil::info('增加分享次数', $this->LOG_MODULE, __FUNCTION__);
+            if ($params['share_num'] > 0) {
+                $res = LiveRoomLogModel::where('session', $params['session'])->increment('share_num', $params['share_num']);
+            } else {
+                $res = LiveRoomLogModel::where('session', $params['session'])->decrement('share_num', $params['share_num'] * -1);
+            }
+        }
+        // 增加关注量
+        if (!empty($params['follow_num'])) {
+            LogUtil::info('增加关注量', $this->LOG_MODULE, __FUNCTION__);
+            if ($params['follow_num'] > 0) {
+                $res = LiveRoomLogModel::where('session', $params['session'])->increment('follow_num', $params['follow_num']);
+            } else {
+                $res = LiveRoomLogModel::where('session', $params['session'])->decrement('follow_num', $params['follow_num'] * -1);
+            }
+        }
+        // 增加商品销量
+        if (!empty($params['goods_sales'])) {
+            LogUtil::info('增加商品销量', $this->LOG_MODULE, __FUNCTION__);
+            if ($params['goods_sales'] > 0) {
+                $res = LiveRoomLogModel::where('session', $params['session'])->increment('goods_sales', $params['goods_sales']);
+            } else {
+                $res = LiveRoomLogModel::where('session', $params['session'])->decrement('goods_sales', $params['goods_sales'] * -1);
+            }
+        }
+        if (!isset($res) || !$res) {
+            return $this->error('执行失败');
+        }
+        return $this->success('执行成功');
+    }
+}

+ 85 - 0
app/Job/LiveRoomSendJob.php

@@ -0,0 +1,85 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Job;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Master\Framework\Extend\BaseJob;
+use App\Master\Framework\Library\Tencent\TencentIm;
+use App\Model\Arts\LiveRoomLogLikeModel;
+use App\Model\Arts\LiveRoomLogModel;
+use App\Model\Arts\LiveRoomModel;
+use App\Utils\LogUtil;
+use App\Utils\RedisUtil;
+
+class LiveRoomSendJob extends BaseJob
+{
+    //日志板块
+    protected string $LOG_MODULE = 'LiveRoomSendJob';
+
+    /**
+     * 任务执行失败后的重试次数,即最大执行次数为 $maxAttempts+1 次
+     */
+    protected int $maxAttempts = 2;
+
+    public function __construct($params)
+    {
+        parent::__construct($params);
+    }
+
+    /**
+     * 执行
+     * @param $params
+     * @return bool
+     */
+    protected function do($params): bool
+    {
+        $im    = new TencentIm();
+        $model = new LiveRoomModel();
+        $room  = $model->getDetail(params: ['room_no' => $params['room_no']],with: ['log']);
+        if (!$room) {
+            return $this->error('直播间异常');
+        }
+
+        // 更新发言权
+        $counter[] = [
+            'key'   => 'talk_status',
+            'value' => (string)$room['talk_status']
+        ];
+
+        // 更新直播间右上角头像
+        $model     = new LiveRoomLogLikeModel();
+        $list      = $model->getTopList($room['room_no'], $room['user_id'], 4);
+        $avatar    = array_column($list, 'avatar');
+        $counter[] = [
+            'key'   => 'top_avatar',
+            'value' => !empty($avatar) ? implode(',', $avatar) : ''
+        ];
+
+        // 更新直播间点赞量
+        $counter[] = [
+            'key'   => 'like',
+            'value' => (string)($room['log']['like'] ?? 0)
+        ];
+
+        // 更新直播间ccu数
+        $im->get_online_member_num($room['room_no']);
+        $online_member = $im->getData()['OnlineMemberNum'] ?? 0;
+        $online_member = $online_member > 0 ? $online_member - 1 : 0;
+        LiveRoomLogModel::query()->where('room_no', $room['room_no'])->where('session', $room['session'])->update(['ccu' => $online_member]);
+        $counter[] = [
+            'key'   => 'online_member_num',
+            'value' => (string)$online_member
+        ];
+
+        $now = md5(json_encode($counter));
+        $old = RedisUtil::getInstance(RedisKeyEnum::ROOM_SEND_DATA, $params['room_no'])->get();
+        if ($now != $old){
+            // 统一更新群属性
+            $im->modify_group_attr($room['room_no'], $counter);
+        }
+        RedisUtil::getInstance(RedisKeyEnum::ROOM_SEND_DATA, $params['room_no'])->setex($now,10);
+        return $this->success('执行成功', $im->getData());
+    }
+}

+ 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';
+}

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

@@ -0,0 +1,31 @@
+<?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_TIMES           = 'SEND_SMS_TIMES:';          // 短信发送次数
+    const SEND_SMS_MOBILE          = 'SEND_SMS_MOBILE:';         // 手机号短信发送次数
+    const SMS_MOBILE_CHECK         = 'SMS_MOBILE_CHECK:';         // 手机号短信校验次数
+    const FA_SITE_SETUP            = 'FA_SITE_SETUP:';           // fastadmin site.php
+    const ROOM_USER_LIST           = 'ROOM_USER_LIST:';     // 直播间用户列表
+    const ROOM_USER_IN             = 'ROOM_USER_IN:';     // 直播间用户列表
+    const ROOM_SEND_DATA           = 'ROOM_SEND_DATA:';     // 直播间属性数据更新
+    const ROOM_BLACK               = 'ROOM_BLACK:';     // 直播间黑名单
+    const ROOM_SHUT_UP               = 'ROOM_SHUT_UP:';     // 直播间黑名单
+}

+ 113 - 0
app/Master/Framework/Extend/BaseJob.php

@@ -0,0 +1,113 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Master\Framework\Extend;
+
+use App\Utils\LogUtil;
+use Hyperf\AsyncQueue\Job;
+use Hyperf\Coroutine\Coroutine;
+
+class BaseJob extends Job
+{
+    //日志板块
+    protected string $LOG_MODULE = 'BaseJob';
+
+    public $params;
+
+    protected string $message = 'error';
+    protected mixed $data = [];
+
+    /**
+     * 任务执行失败后的重试次数,即最大执行次数为 $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/');//设置日志存入通道
+        Coroutine::defer(function () {
+            LogUtil::close();//协程结束后统一写入
+        });
+        LogUtil::info('开始处理', $this->LOG_MODULE, 'do', ['params' => $this->params]);
+        // 根据参数处理具体逻辑
+        // 通过具体参数获取模型等
+        // 这里的逻辑会在 ConsumerProcess 进程中执行
+        try {
+            $res = $this->do($this->params);
+            LogUtil::info('处理结果', $this->LOG_MODULE, 'do', [
+                'code' => $res,
+                'message' => $this->getMessage(),
+                'data' => $this->getData(),
+            ]);
+        } catch (\Exception $e){
+            LogUtil::error('执行失败',$this->LOG_MODULE,__FUNCTION__,$e);
+        }
+    }
+
+    /**
+     * @param $params
+     * @return true
+     */
+    protected function do($params)
+    {
+        // 业务代码
+        return $this->success('执行成功', $params);
+    }
+
+    /**
+     * 返回成功结果
+     * @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;
+    }
+}

+ 93 - 0
app/Master/Framework/Extend/BaseTask.php

@@ -0,0 +1,93 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Master\Framework\Extend;
+
+use App\Utils\LogUtil;
+use Hyperf\Coroutine\Coroutine;
+
+class BaseTask{
+    //日志板块
+    protected string $LOG_MODULE = 'BaseTask';
+    protected string $message = 'error';
+    protected mixed $data = [];
+
+    public function __construct()
+    {
+        //日志统一写入
+        LogUtil::getInstance('Task/');//设置日志存入通道
+        Coroutine::defer(function () {
+            LogUtil::close();//协程结束后统一写入
+        });
+    }
+
+    public function execute(): bool
+    {
+        LogUtil::info('开始处理', $this->LOG_MODULE, 'do');
+        // 根据参数处理具体逻辑
+        // 通过具体参数获取模型等
+        // 这里的逻辑会在 ConsumerProcess 进程中执行
+        $res = $this->do();
+        LogUtil::info('处理结果', $this->LOG_MODULE, 'do', [
+            'code' => $res,
+            'message' => $this->getMessage(),
+            'data' => $this->getData(),
+        ]);
+        return $res;
+    }
+
+    /**
+     * 开始工作
+     * @return true
+     */
+    protected function do(): bool
+    {
+        // 业务代码
+        return $this->success('执行成功');
+    }
+
+    /**
+     * 返回成功结果
+     * @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;
+    }
+}

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

@@ -0,0 +1,335 @@
+<?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;
+    }
+}
+
+/**
+ * 返回输入数组中某个单一列的值
+ * @Author PandaEyes
+ * @email joeyoung0314@qq.com
+ * @PHP_VERSION >=7.3
+ * @param array $array 多维数组
+ * @param string|null $column_keys 可以是索引数组的列的整数索引,或者是关联数组的列的字符串键值,该参数也可以是 NULL,此时将返回整个数组,配合index_key使用,注意,与array_column不同的是,此处可以返回多列,可用','分割
+ * @param string|null $index_key 取出数组中这一列当做返回数组的索引 注意,与array_column不同的是,此处将不会去重,而是将所有符合的数组编排到同一键中
+ * @return array
+ */
+if (!function_exists('array_columns')) {
+    function array_columns(array $array, string $column_keys = null, string $index_key = null): array
+    {
+        $result = [];
+        $keys   = isset($column_keys) ? explode(',', $column_keys) : [];
+
+        if ($array) {
+            foreach ($array as $item) {
+                // 指定返回列
+                if ($keys) {
+                    $tmp = [];
+                    foreach ($keys as $key) {
+                        $tmp[$key] = $item[$key];
+                    }
+                } else {
+                    $tmp = $item;
+                }
+                // 指定索引列
+                if (isset($index_key)) {
+                    $result[$item[$index_key]][] = $tmp;
+                } else {
+                    $result[] = $tmp;
+                }
+
+            }
+        }
+        return $result;
+    }
+}
+
+/**
+ * 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;
+    }
+}
+/**
+ * 关键词判断是否存在
+ * @param string $keyword
+ * @return bool
+ */
+if (!function_exists('keyword_exits')) {
+    function keyword_exits(string $keyword): bool
+    {
+        $keyword        = strtolower($keyword);
+        $keyword_filter = explode('|',trim(site('keyword_filter')));
+
+        // 检查是否包含敏感词
+        $is_true = false;
+        foreach ($keyword_filter as $word) {
+            if (strpos($keyword, $word) !== false) {
+                $is_true = true;
+                break;
+            }
+        }
+
+        return $is_true;
+    }
+}
+
+/**
+ * 关键词过滤
+ * @param string $keyword
+ * @return string mixed
+ */
+if (!function_exists('keyword_filter')) {
+    function keyword_filter(string $keyword): string
+    {
+        $keyword        = strtolower($keyword);
+        $keyword_filter = explode('|',trim(site('keyword_filter')));
+        $keyword_filter = array_unique($keyword_filter) ?? [];
+
+        //根据敏感词字数替换相同数量的'*'
+        $kong = [];
+        foreach ($keyword_filter as $k => $v) {
+            $x = '';
+            for ($i = 0; $i < mb_strlen($v, 'utf-8'); $i++) {
+                $x .= '*';
+            }
+            $kong[$k] = $x;
+            unset($x);
+        }
+
+        $replace = array_combine($keyword_filter, $kong);//将敏感词作为下标,将对应的'*'作为值
+        return strtr($keyword, $replace);//将敏感词替换为'*'
+    }
+}
+
+/**
+ * 自动拼im id前缀
+ * @param $user_id
+ * @return mixed
+ */
+if (!function_exists('im_prefix')) {
+    function im_prefix($user_id)
+    {
+        return \Hyperf\Config\config('tencent.im.chat_prefix').$user_id;
+    }
+}
+
+/**
+ * 将秒时间转换具体时间:秒转天时分秒
+ * @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)
+    {
+        if (empty($url)) return '';
+        $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;
+    }
+}
+
+/**
+ * 自动拼im id前缀
+ * @param $user_id
+ * @return string
+ */
+if (!function_exists('im_prefix')) {
+    function im_prefix($user_id): string
+    {
+        return \Hyperf\Config\config('tencent.im.chat_prefix').$user_id;
+    }
+}
+
+/**
+ * 自动拼im id前缀
+ * @param $user_id
+ * @return string
+ */
+if (!function_exists('im_un_prefix')) {
+    function im_un_prefix($user_id): int
+    {
+        return (int)str_replace(\Hyperf\Config\config('tencent.im.chat_prefix'),'',$user_id);
+    }
+}
+
+if (!function_exists('dd')) {
+    function dd(...$vars)
+    {
+        foreach ($vars as $v) {
+            var_dump($v);
+        }
+    }
+}

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

@@ -0,0 +1,92 @@
+<?php
+
+declare(strict_types=1);
+
+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('发送成功');
+    }
+}

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

@@ -0,0 +1,137 @@
+<?php
+
+declare(strict_types=1);
+
+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;
+    }
+}

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

@@ -0,0 +1,137 @@
+<?php
+
+declare(strict_types=1);
+
+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;
+    }
+}

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

@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Master\Framework\Library\Easywechat;
+
+use App\Master\Framework\Library\Extend\Module;
+use EasyWeChat\OfficialAccount\Application;
+
+/**
+ * 微信小程序开发组
+ * 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);
+    }
+}

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

@@ -0,0 +1,188 @@
+<?php
+
+declare(strict_types=1);
+
+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;
+        }
+    }
+}

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

@@ -0,0 +1,28 @@
+<?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;
+    }
+}

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

@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Master\Framework\Library\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'] ?? [];
+    }
+}

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

@@ -0,0 +1,172 @@
+<?php
+
+declare(strict_types=1);
+
+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);
+    }
+}

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

@@ -0,0 +1,267 @@
+<?php
+
+declare(strict_types=1);
+
+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";
+    }
+}

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

@@ -0,0 +1,100 @@
+<?php
+
+declare(strict_types=1);
+
+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'
+                ]
+            ),
+        ]);
+    }
+
+    /**
+     * 返回成功结果
+     * @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;
+    }
+}

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

@@ -0,0 +1,120 @@
+<?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 connect(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();
+    }
+}

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

@@ -0,0 +1,43 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Master\Framework\Library\Mqtt;
+
+use function Hyperf\Coroutine\co;
+
+class Subscribe{
+    /**
+     * MQTT 订阅
+     * @param array $topic
+     * @param $function
+     * @return mixed
+     */
+    public function endlessLoop(array $topic,$function): mixed
+    {
+        $client = new MqttClient();
+        $client->connect();
+        // 订阅一个主题或者多个主题
+        $client->subscribe($topic);
+
+        //时间
+        $timeSincePing = time();
+
+        while (true){
+            // 接收并处理消息
+            $message = $client->recv();// 订阅消息
+            if ($message && $message !== true) {
+                // 携程处理
+                co(function () use ($function,$client,$message,$topic){
+                    $function($client,$message,$topic);
+                });
+            }
+
+            //心跳
+            if ($timeSincePing <= (time() - $client->getKeepAlive())){
+                if ($client->ping()) {
+                    $timeSincePing = time();
+                }
+            }
+        }
+    }
+}

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

@@ -0,0 +1,331 @@
+<?php
+
+declare(strict_types=1);
+
+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 );
+    }
+}

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

@@ -0,0 +1,411 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Master\Framework\Library\Tencent;
+
+use App\Master\Framework\Library\Library;
+use Hyperf\Config\Annotation\Value;
+use function Hyperf\Config\config;
+
+class TencentIm extends Library
+{
+    public string $base_uri = "https://console.tim.qq.com";
+    #[Value("tencent.im")]
+    private array $config;
+
+    /**
+     * 创建群组
+     * @param string $user_chat_id
+     * @param string $group_no
+     * @param string $room_name
+     * @param string $type
+     * @param string $notification
+     * @return bool
+     */
+    public function create_group(string $user_chat_id,string $group_no,string $room_name,string $type = 'AVChatRoom', string $notification = '')
+    {
+        $data = [
+            'Owner_Account' => $user_chat_id, // 群主 ID
+            'Type'          => $type,         // 群组形态,包括 Public(陌生人社交群),Private(即 Work,好友工作群),ChatRoom(即 Meeting,会议群),AVChatRoom(直播群),Community(社群)
+            'GroupId'       => $group_no,     // 自定义群组 ID
+            'Name'          => $room_name,
+            'Introduction'  => json_encode([
+                ''
+            ]),// 群简介
+            'Notification'  => $notification,// 群公告
+        ];
+        $response  = $this->post('/v4/group_open_http_svc/create_group', $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 string $user_chat_id
+     * @param string $group_no
+     * @param string $room_name
+     * @param string $type
+     * @param string $notification
+     * @return bool
+     */
+    public function destroy_group(string $group_no)
+    {
+        $data = [
+            'GroupId'       => $group_no,     // 自定义群组 ID
+        ];
+        $response  = $this->post('/v4/group_open_http_svc/destroy_group', $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 string $group_no
+     * @param string $user_chat_id
+     * @return bool
+     */
+    public function delete_group_member(string $group_no,string $user_chat_id)
+    {
+        $data = [
+            'GroupId'       => $group_no,     // 自定义群组 ID
+            'MemberToDel_Account' => [$user_chat_id]
+        ];
+        $response  = $this->post('/v4/group_open_http_svc/delete_group_member', $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 string $group_no
+     * @param string $user_chat_id
+     * @return bool
+     */
+    public function modify_admin(string $group_no,string $user_chat_id,int $type = 1)
+    {
+        $data = [
+            'GroupId'       => $group_no,     // 自定义群组 ID
+            'CommandType' => $type,
+            'Admin_Account' => [$user_chat_id],
+        ];
+        $response  = $this->post('/v4/group_open_avchatroom_http_svc/modify_admin', $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);
+    }
+
+    /**
+     * 禁言 or 解除
+     * @param string $group_no
+     * @param string $user_chat_id
+     * @param $time // time = 0 解除
+     * @return bool
+     */
+    public function forbid_send_msg(string $group_no,string $user_chat_id,int $time = 0)
+    {
+        $data = [
+            'GroupId'         => $group_no,     // 自定义群组 ID
+            'Members_Account' => [$user_chat_id],
+            'MuteTime'        => $time
+        ];
+        $response  = $this->post('/v4/group_open_http_svc/forbid_send_msg', $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 string $group_no
+     * @return bool
+     */
+    public function get_group_muted_account(string $group_no)
+    {
+        $data = [
+            'GroupId'         => $group_no,     // 自定义群组 ID
+        ];
+        $response  = $this->post('/v4/group_open_http_svc/get_group_muted_account', $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 string $group_no
+     * @return bool
+     */
+    public function get_group_ban_member(string $group_no,int $limit = 100,int $Offset = 0)
+    {
+        $data = [
+            'GroupId'         => $group_no,     // 自定义群组 ID
+            'Limit'         => $limit,     // 单次拉取限制,最大为100
+            'Offset'         => $Offset,     // 分页标识,首次传0,如果封禁的成员数大于100,下一次拉取需要设置为后台回复的 NextOffset
+        ];
+        $response  = $this->post('/v4/group_open_http_svc/get_group_ban_member', $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 string $group_no
+     * @param string $user_chat_id
+     * @param $time // time = 0 解除
+     * @param $remark
+     * @return bool
+     */
+    public function ban_group_member(string $group_no,string $user_chat_id,int $time = 0, $remark = '违规操作')
+    {
+        $data = [
+            'GroupId'         => $group_no,
+            'Members_Account' => [$user_chat_id],
+            'Duration'        => $time, // 封禁时长,单位:秒
+            'Description'     => $remark// 封禁信息
+        ];
+        $response  = $this->post('/v4/group_open_http_svc/ban_group_member', $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 string $group_no
+     * @param string $user_chat_id
+     * @return bool
+     */
+    public function unban_group_member(string $group_no,string $user_chat_id)
+    {
+        $data = [
+            'GroupId'         => $group_no,
+            'Members_Account' => [$user_chat_id]
+        ];
+        $response  = $this->post('/v4/group_open_http_svc/unban_group_member', $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
+     * @return bool
+     */
+    public function get_online_member_num(string $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 string $room_no
+     * @param array $counter
+     * @return bool
+     */
+    public function update_group_counter(string $room_no, array $counter)
+    {
+        $data     = [
+            'GroupId'      => $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 string $room_no
+     * @param array $attr
+     * @return bool
+     */
+    public function modify_group_attr(string $room_no, array $attr)
+    {
+        $data     = [
+            'GroupId'   => $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);
+    }
+
+    /**
+     * 获取直播推流信息
+     * @param string $stream_name 直播间号
+     * @param string $time 过期时间
+     * @return string
+     */
+    public function getLivePushUrl(string $stream_name, string $time = '')
+    {
+        $key    = $this->config['push_key'];
+        $time   = !empty($time) ? $time : date('Y-m-d H:i:s', strtotime('+1 day'));
+        $domain = $this->config['push_domain'];
+        if ($key && $time) {
+            $txTime = strtoupper(base_convert((string)strtotime($time), 10, 16));
+            //txSecret = MD5( KEY + streamName + txTime )
+            $txSecret = md5($key . $stream_name . $txTime);
+            $ext_str  = "?" . http_build_query(["txSecret" => $txSecret, "txTime" => $txTime]);
+        }
+        return "rtmp://{$domain}/live/{$stream_name}" . ($ext_str ?? "");
+    }
+
+    /**
+     * 获取直播播放地址
+     * @param string $stream_name 您用来区别不同推流地址的唯一流名称
+     * @return string
+     */
+    public function getLivePlayUrl(string $stream_name)
+    {
+        return "rtmp://{$this->config['play_domain']}/live/".$stream_name;
+    }
+
+    /**
+     * 获取usersig签名-具体操作
+     */
+    public function userSig($user_id)
+    {
+        // 获取配置信息
+        $userSigObj = new GetUserSig($this->config["appid"], $this->config["key"]);
+        return $userSigObj->genUserSig($user_id);
+    }
+
+    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);
+    }
+
+    public function setConfig(array $config): TencentIm
+    {
+        $this->config = $config;
+        return $this;
+    }
+}

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

@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Master\Framework\Library\Twilio;
+
+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,
+        ]);
+    }
+}

+ 159 - 0
app/Middleware/ApiAgent.php

@@ -0,0 +1,159 @@
+<?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\TokenFast;
+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 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 = ActionUtil::getInstance()->actions($request,self::PROJECT);
+        $params = $this->request->all();
+        // 记录用户请求参数
+        LogUtil::info('请求参数', $this->action['controller'], $this->action['action'], $params);
+
+        //接口限流,写到中间件中
+        if (!RedisUtil::getInstance(RedisKeyEnum::API_REQUEST_TRAFFIC)->requestLimit("{$this->action['controller']}/{$this->action['action']}", 1, 10)) {
+            LogUtil::info('请求次数过多', $this->action['controller'], $this->action['action']);
+            return $this->response206();
+        }
+
+        $token = $this->request->header('token');
+        if (empty($token)) $token = $params['token'] ?? '';
+        if (!empty($token) && !in_array("{$this->action['controller']}/{$this->action['action']}", self::tokenWhiteList())) {
+            LogUtil::info('token 验证开始', $this->action['controller'], $this->action['action'], (string)$token);
+
+            // 校验token
+            if (!$user_id = $this->checkToken($token)){
+                return $this->response401();
+            }
+
+            // 查询并记录用户信息
+            $user = (new UserModel())->authUserInfo($user_id);
+            if (!$user) {
+                LogUtil::warning('账号不存在', $this->action['controller'], $this->action['action'], ['user_id', $user_id]);
+                return $this->response401();
+            }
+
+            LogUtil::info('用户编号', $this->action['controller'], $this->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 = TokenFast::get($token);
+        if (!$checkToken) {
+            LogUtil::warning('token 验证失败', $this->action['controller'], $this->action['action'], $checkToken);
+            return false;
+        }
+
+        //验证参数信息
+        if (empty($checkToken['user_id'])) {
+            LogUtil::warning('token 参数不全', $this->action['controller'], $this->action['action'], $checkToken);
+            return false;
+        }
+
+        return (int)$checkToken['user_id'];
+    }
+
+    /**
+     * 令牌失效
+     *
+     * @param string $message
+     * @param $result
+     * @return ResponseInterface
+     */
+    private function response401(string $message = '令牌失效,请稍后重试!', $result = null): ResponseInterface
+    {
+        // 记录令牌校验
+        LogUtil::info($message, $this->action['controller'], $this->action['action'], $result);
+        return AppResult::response_fast(401,$message, $result);
+    }
+
+    /**
+     * 请求频繁
+     *
+     * @param string $message
+     * @param $result
+     * @return ResponseInterface
+     */
+    private function response206(string $message = '当前访问人数过多,请稍后再试!', $result = null): ResponseInterface
+    {
+        // 记录令牌校验
+        LogUtil::info($message, $this->action['controller'], $this->action['action'], $result);
+        return AppResult::response_fast(206,$message, $result);
+    }
+
+    /**
+     * token校验黑名单
+     * @return string[]
+     */
+    public function tokenWhiteList(): array
+    {
+        return [
+            'v1/UserController/login',
+        ];
+    }
+}

+ 51 - 0
app/Middleware/ApiSign.php

@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Middleware;
+
+use App\Utils\AppResult;
+use App\Utils\Control\AuthUser;
+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 AppResult::response_fast(401,'未登录,令牌失效,请稍后重试!');
+        }
+
+        // 根据具体业务判断逻辑走向
+        return $handler->handle($request);
+    }
+}

+ 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 = [
+        '*'
+    ];
+
+}

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

@@ -0,0 +1,29 @@
+<?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';
+    public bool $timestamps = false;
+
+    /**
+     * 默认查询字段
+     *
+     * @var array|string[]
+     */
+    public array $select = [
+        '*'
+    ];
+}

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

@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Master\Framework\Library\Tencent\TencentIm;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+class LiveReportModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'live_report';
+
+    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 add(int $user_id,array $data)
+    {
+        $data['user_id'] = $user_id;
+        $data['create_time'] = time();
+        return $this->query()->insert($data);
+    }
+}

+ 142 - 0
app/Model/Arts/LiveRoomAdminModel.php

@@ -0,0 +1,142 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Framework\Library\Tencent\TencentIm;
+use App\Model\Model;
+
+class LiveRoomAdminModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'live_room_admin';
+
+    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 = [
+        '*'
+    ];
+
+    /**
+     * 添加场控
+     * @param int $user_id 主播ID
+     * @param string $room_no 房间号
+     * @param int $admin_id 场控ID
+     * @return bool
+     */
+    public function add(int $user_id, string $room_no, int $admin_id)
+    {
+        $model = new LiveRoomModel();
+        if (!$room = $model->getDetail(params: ['room_no' => $room_no])) {
+            return $this->error('直播间已关闭');
+        }
+        if ($room['user_id'] != $user_id) {
+            return $this->error('未拥有此权限');
+        }
+        if (!$admin = $this->getDetail(params: ['admin_id' => $admin_id, 'room_no' => $room_no])) {
+            // 没有直播间 创建一个
+            $insert = [
+                'room_id'     => $room['id'],
+                'room_no'     => $room_no,
+                'user_id'     => $user_id,
+                'admin_id'    => $admin_id,
+                'status'      => 1,
+                'create_time' => time(),
+            ];
+            if (!$this->insertGetId($insert)) {
+                return $this->error('设置失败');
+            }
+        } else {
+            // 更新状态
+            if (!$this->where('id', $admin['id'])->update(['status' => 1,'create_time' => time()])) {
+                return $this->error('设置失败');
+            }
+        }
+
+//        $im = new TencentIm();
+//        if (!$im->modify_admin($room_no,im_prefix($user_id))){
+//            return $this->error($im->getMessage() ?? '设置失败');
+//        }
+
+        return $this->success('成功');
+    }
+
+    /**
+     * 删除场控
+     * @param int $user_id 主播ID
+     * @param string $room_no 房间号
+     * @param int $admin_id 场控ID
+     * @return bool
+     */
+    public function del(int $user_id, string $room_no, int $admin_id)
+    {
+        $model = new LiveRoomModel();
+        if (!$room = $model->getDetail(params: ['room_no' => $room_no])) {
+            return $this->error('直播间已关闭');
+        }
+        if ($room['user_id'] != $user_id) {
+            return $this->error('未拥有此权限');
+        }
+        if ($admin = $this->getDetail(params: ['admin_id' => $admin_id, 'room_no' => $room_no])) {
+            // 更新状态
+            if (!$this->where('id', $admin['id'])->update(['status' => 0, 'create_time' => time()])) {
+                return $this->error('删除失败');
+            }
+        }
+//        $im = new TencentIm();
+//        if (!$im->modify_admin($room_no,im_prefix($user_id),2)){
+//            return $this->error($im->getMessage() ?? '设置失败');
+//        }
+        return $this->success('成功');
+    }
+
+    /**
+     * 获取场控信息
+     * @param $admin_id
+     * @param $room_no
+     * @return array
+     */
+    public function getAdmin($admin_id, $room_no)
+    {
+        $this->setIsStatusSearchValue(1);
+        return $this->getDetail(params: [
+            'admin_id' => $admin_id,
+            'room_no' => $room_no,
+        ]);
+    }
+
+    public function searchRoomNoAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('room_no', $value);
+    }
+
+    public function searchAdminIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('admin_id', $value);
+    }
+
+    public function user()
+    {
+        return $this->hasOne(UserModel::class, 'id', 'admin_id');
+    }
+}

+ 66 - 0
app/Model/Arts/LiveRoomBlackModel.php

@@ -0,0 +1,66 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Framework\Library\Tencent\TencentIm;
+use App\Model\Model;
+use Hyperf\DbConnection\Db;
+
+class LiveRoomBlackModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'live_room_black';
+
+    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 searchRoomNoAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('room_no', $value);
+    }
+
+    public function searchUserIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('user_id', $value);
+    }
+
+    public function searchKeywordAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->withWhereHas('user',function ($query) use($value) {
+            $query->where('nickname','like',"%{$value}%");
+        });
+    }
+
+    // 用户信息
+    public function user()
+    {
+        return $this->hasOne(UserModel::class, 'id', 'user_id');
+    }
+}

+ 127 - 0
app/Model/Arts/LiveRoomFollowModel.php

@@ -0,0 +1,127 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Framework\Library\Tencent\TencentIm;
+use App\Model\Model;
+use Hyperf\DbConnection\Db;
+
+class LiveRoomFollowModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'live_room_follow';
+
+    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 searchRoomNoAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('room_no', $value);
+    }
+
+    public function searchUserIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('user_id', $value);
+    }
+
+    public function searchKeywordAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->withWhereHas('user',function ($query) use($value) {
+            $query->where('nickname','like',"%{$value}%");
+        });
+    }
+
+    /**
+     * 关注直播间
+     * @param int $user_id
+     * @param int $room_id
+     * @param string $room_no
+     * @param string $session
+     * @return bool
+     */
+    public function follow(int $user_id,int $room_id,string $room_no,string $session): bool
+    {
+        if (!$info = $this->query()->where(['user_id'=>$user_id,'room_id'=>$room_id,'room_no'=>$room_no])->first()){
+            $log = [
+                'user_id'   => $user_id,
+                'room_id'   => $room_id,
+                'room_no'   => $room_no,
+                'session'   => $session,
+                'status'   => 1,
+                'create_time' => time(),
+            ];
+            if (!$this->query()->insertGetId($log)) {
+                return $this->error('关注失败');
+            }
+        }else{
+            if ($info->status == 1){
+                return $this->error('已关注过了');
+            }
+            if (!$this->query()->where('id',$info->id)->update(['status'=>1,'session'=>$session,'create_time'=>time()])){
+                return $this->error('关注失败了');
+            }
+        }
+        return $this->success('关注成功');
+    }
+
+    /**
+     * 取消关注
+     * @param int $user_id
+     * @param int $room_id
+     * @param string $room_no
+     * @return bool
+     */
+    public function cancelFollow(int $user_id,int $room_id,string $room_no)
+    {
+        if (!$info = $this->query()->where(['user_id'=>$user_id,'room_id'=>$room_id,'room_no'=>$room_no])->first()){
+            return $this->error('您未关注过');
+        }else{
+            if ($info->status != 1){
+                return $this->error('已取消关注');
+            }
+            if (!$this->query()->where('id',$info->id)->update(['status'=>0])){
+                return $this->error('操作失败');
+            }
+        }
+        return $this->success('取关成功');
+    }
+
+    // 开播日志
+    public function log()
+    {
+        return $this->hasOne(LiveRoomLogModel::class, 'session', 'session');
+    }
+
+    // 用户信息
+    public function user()
+    {
+        return $this->hasOne(UserModel::class, 'id', 'user_id');
+    }
+}

+ 49 - 0
app/Model/Arts/LiveRoomGoodsModel.php

@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Framework\Library\Tencent\TencentIm;
+use App\Model\Model;
+
+class LiveRoomGoodsModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'live_room_goods';
+
+    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 searchRoomNoAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('room_no', $value);
+    }
+
+    public function searchAdminIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('admin_id', $value);
+    }
+}

+ 95 - 0
app/Model/Arts/LiveRoomKeywordModel.php

@@ -0,0 +1,95 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Master\Framework\Library\Tencent\TencentIm;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+class LiveRoomKeywordModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'live_room_keyword';
+
+    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 searchRoomNoAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('room_no', $value);
+    }
+
+    public function searchUserIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('user_id', $value);
+    }
+
+    /**
+     * 添加关键词
+     * @param int $user_id
+     * @param int $room_id
+     * @param string $room_no
+     * @param string $keyword
+     * @return bool
+     */
+    public function add(int $user_id,int $room_id, string $room_no, string $keyword): bool
+    {
+        $data = [
+            'user_id' => $user_id,
+            'room_id' => $room_id,
+            'room_no' => $room_no,
+            'keyword' => $keyword,
+        ];
+        if (!$this->query()->insert($data)){
+            return $this->error('添加失败');
+        }
+
+        return $this->success('添加成功');
+    }
+
+    /**
+     * 添加关键词
+     * @param int $id
+     * @return bool
+     */
+    public function del(int $id): bool
+    {
+        if (!$this->query()->where('id',$id)->delete()){
+            return $this->error('删除失败');
+        }
+
+        return $this->success('删除成功');
+    }
+
+    // 主播信息
+    public function user()
+    {
+        return $this->hasOne(UserModel::class, 'id', 'user_id');
+    }
+}

+ 98 - 0
app/Model/Arts/LiveRoomLogLikeModel.php

@@ -0,0 +1,98 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+
+class LiveRoomLogLikeModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'live_room_log_like';
+
+    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 = [
+        '*'
+    ];
+
+    /**
+     * 记录用户点赞数量
+     * @param int $user_id
+     * @param int $room_id
+     * @param string $room_no
+     * @param string $session
+     * @param int $num
+     * @return bool|int
+     */
+    public static function likes(int $user_id, int $room_id, string $room_no, string $session, int $num = 1)
+    {
+        if (!$info = self::query()->where(['user_id' => $user_id, 'room_id' => $room_id, 'room_no' => $room_no, 'session' => $session])->first()) {
+            $res = self::query()->insert(['user_id' => $user_id, 'room_id' => $room_id, 'room_no' => $room_no, 'session' => $session, 'like' => $num]);
+        } else {
+            $res = self::query()->where('id', $info['id'])->increment('like', $num);
+        }
+        // redis 排行榜集合
+        RedisUtil::getInstance(RedisKeyEnum::ROOM_USER_LIST,$room_no)->zIncrBy($num,$user_id);
+        return $res;
+    }
+
+    /**
+     * 观众排行榜
+     * @param string $room_no
+     * @param int $user_id 主播ID
+     * @param int $num
+     * @return array
+     * @throws \Exception
+     */
+    public function getTopList(string $room_no, int $user_id, int $num = 50)
+    {
+        $onList = RedisUtil::getInstance(RedisKeyEnum::ROOM_USER_LIST, $room_no)->zRevRange(0, $num);
+        $key    = array_search($user_id, $onList);
+        if ($key !== false) {
+            unset($onList[$key]);
+        } else if (count($onList) > $num) {
+            // 如果没有主播自己,且数量大于 num 则需要去掉最后一个
+            array_pop($onList);
+        }
+
+        $model = new UserModel();
+        $list = $model->getList(params: ['ids' => $onList,'list_rows' => $num],select: ['id','nickname','avatar']);
+        $list = array_columns($list,'id,nickname,avatar','id');
+        $top = [];
+        foreach ($onList as $key => $val) {
+            if (isset($list[$val][0])){
+                $top[] = [
+                    'id' => (int)$val,
+                    'nickname' => $list[$val][0]['nickname'],
+                    'avatar' => cdn_url($list[$val][0]['avatar']),
+                    'chat_id' => im_prefix($list[$val][0]['id']),
+                    'rank' => $key + 1
+                ];
+            }
+        }
+        return $top;
+    }
+
+    // 开播日志
+    public function user()
+    {
+        return $this->hasOne(UserModel::class, 'id', 'user_id');
+    }
+}

+ 32 - 0
app/Model/Arts/LiveRoomLogModel.php

@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Model\Model;
+
+class LiveRoomLogModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'live_room_log';
+
+    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 = [
+        '*'
+    ];
+}

+ 322 - 0
app/Model/Arts/LiveRoomModel.php

@@ -0,0 +1,322 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Master\Framework\Library\Tencent\TencentIm;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+class LiveRoomModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'live_room';
+
+    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 searchRoomNoAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('room_no', $value);
+    }
+
+    public function searchUserIdAttribute($query, $value, array $params): mixed
+    {
+        if (empty($value)) {
+            return $query;
+        }
+        return $query->where('user_id', $value);
+    }
+
+    /**
+     * 创建直播间
+     * @param array $user auth_user
+     * @param string $room_name 直播间名称
+     * @return bool
+     */
+    public function createRoom(array $user,string $room_name = '', string $image = ''): bool
+    {
+        $this->setIsStatusSearchValue(0);
+        if (!$room = $this->getDetail(params: ['user_id' => $user['id']])) {
+            // 没有直播间 创建一个
+            $room_no = (rand(10, 99) . (sprintf("%06d", $user['id'])));
+            $room_session = $room_no . "_" . date('YmdHis');
+            $insert = [
+                'user_id'    => $user['id'],
+                'name'       => $room_name,
+                'logo'       => $user['avatar'],
+                'image'      => !empty($image) ? $image : $user['avatar'],
+                'room_no'    => $room_no,
+                'session'    => $room_session,
+                'status'     => 1,
+                'create_time' => time(),
+            ];
+            if (!$room_id = $this->query()->insertGetId($insert)) {
+                return $this->error('创建失败');
+            }
+        } else {
+            // 有直播间 更新直播状态
+            if ($room['status'] == 2) {
+                return $this->error('直播被封禁!');
+            }
+            // 已经开播了
+            if ($room['status'] == 1) {
+                $update = [
+                    'name'    => $room_name,
+                    'logo'    => $user['avatar'],
+                    'image'   => !empty($image) ? $image : $user['avatar'],
+                    'update_time' => time()
+                ];
+                if (!$this->query()->where('user_id', $user['id'])->update($update)) {
+                    return $this->error('开播失败');
+                }
+                return $this->success('开播成功', [
+                    'room_no'   => $room['room_no'],
+                    'room_name' => $room['name'],
+                    'talk_status' => $room['talk_status'],
+                    'status'    => 1,
+                ]);
+            }
+            $room_no      = $room['room_no'];
+            $room_session = $room_no . "_" . date('YmdHis');
+            $room_id      = $room['id'];
+            $room_name    = !empty($room_name) ? $room_name : $room['name'];
+            $update = [
+                'name'    => $room_name,
+                'logo'    => $user['avatar'],
+                'image'   => !empty($image) ? $image : $user['avatar'],
+                'session' => $room_session,
+                'status'  => 1,
+                'update_time' => time()
+            ];
+            if (!$this->query()->where('user_id', $user['id'])->update($update)) {
+                return $this->error('开播失败');
+            }
+        }
+
+        $log = [
+            'user_id'   => $user['id'],
+            'room_id'   => $room_id,
+            'room_no'   => $room_no,
+            'session'   => $room_session,
+            'open_time' => time(),
+        ];
+        if (!(new LiveRoomLogModel())->query()->insertGetId($log)) {
+            return $this->error('开播失败');
+        }
+
+        return $this->success('开播成功', [
+            'room_no'   => $room_no,
+            'room_name' => $room_name,
+            'talk_status' => $room['talk_status'] ?? 1,
+        ]);
+    }
+
+    /**
+     * 关闭房间
+     * @param $room_id
+     * @return bool
+     */
+    public function closeRoom($room_id)
+    {
+        if (!$room = $this->getDetail(params: ['id' => $room_id])) {
+            return $this->success('关闭成功');
+        }
+
+        if (!$this->where('id', $room_id)->update(['status' => 0])) {
+            return $this->error('关闭失败');
+        }
+
+        $time  = time();
+        $logUp = [
+            'duration'   => Db::raw("{$time} - open_time"),
+            'close_time' => $time,
+        ];
+        if (!(new LiveRoomLogModel())->where('session',$room['session'])->update($logUp)) {
+            return $this->error('开播失败');
+        }
+
+        // 解散直播间移除观众
+        RedisUtil::getInstance(RedisKeyEnum::ROOM_USER_LIST,$room['room_no'])->del();
+
+        return $this->success('关播成功');
+    }
+
+    /**
+     * 禁言
+     * @param int $user_id 主播ID
+     * @param string $room_no 房间号
+     * @param int $admin_id 观众ID
+     * @return bool
+     */
+    public function shut_up(int $user_id, string $room_no, int $audience_id, int $time = 86400)
+    {
+        if (!$room = $this->getDetail(params: ['room_no' => $room_no])) {
+            return $this->error('直播间已关闭');
+        }
+        if ($room['user_id'] != $user_id) {
+            $model = new LiveRoomAdminModel();
+            if (!$model->getAdmin($user_id, $room_no)) {
+                return $this->error('未拥有此权限');
+            }
+        }
+
+        RedisUtil::getInstance(RedisKeyEnum::ROOM_SHUT_UP,$room_no,im_prefix($audience_id))->setex(1,$time+1);
+
+        // 腾讯直播创建房间 && 创建群组
+        $im = new TencentIm();
+        if (!$im->forbid_send_msg($room_no, im_prefix($audience_id), $time)) {
+            return $this->error($im->getMessage() ?? '操作失败');
+        }
+        return $this->success('成功');
+    }
+
+    /**
+     * 封禁 黑名单
+     * @param int $user_id 主播ID
+     * @param string $room_no 房间号
+     * @param int $admin_id 观众ID
+     * @return bool
+     */
+    public function black_add(int $user_id, string $room_no, int $audience_id, int $time = 86400)
+    {
+        if (!$room = $this->getDetail(params: ['room_no' => $room_no])) {
+            return $this->error('直播间已关闭');
+        }
+        if ($room['user_id'] != $user_id) {
+            $model = new LiveRoomAdminModel();
+            if (!$model->getAdmin($user_id, $room_no)) {
+                return $this->error('未拥有此权限');
+            }
+        }
+
+        Db::beginTransaction();
+        if (!$info = LiveRoomBlackModel::query()->where(['user_id'=>$audience_id,'room_id'=>$room['id'],'room_no'=>$room_no])->first()){
+            $log = [
+                'user_id'   => $audience_id,
+                'room_id'   => $room['id'],
+                'room_no'   => $room_no,
+                'status'   => 1,
+                'create_time' => time(),
+                'end_time' => time() + $time,
+            ];
+            if (!LiveRoomBlackModel::query()->insertGetId($log)) {
+                Db::rollBack();
+                return $this->error('拉黑失败');
+            }
+        }else{
+            if ($info->status == 1 && $info->end_time > time()){
+                Db::rollBack();
+                return $this->error('已拉黑过了');
+            }
+            if (!LiveRoomBlackModel::query()->where('id',$info->id)->update(['status'=>1,'end_time'=>time() + $time,'create_time'=>time()])){
+                Db::rollBack();
+                return $this->error('拉黑失败了');
+            }
+        }
+
+        RedisUtil::getInstance(RedisKeyEnum::ROOM_BLACK,$room_no,im_prefix($audience_id))->setex(1,$time);
+
+        // 腾讯直播创建房间 && 创建群组
+        $im = new TencentIm();
+//        if (!$im->ban_group_member($room_no, im_prefix($audience_id), $time)) {
+//            return $this->error($im->getMessage() ?? '操作失败');
+//        }
+
+        if (!$im->delete_group_member($room_no, im_prefix($audience_id))) {
+            Db::rollBack();
+            return $this->error($im->getMessage() ?? '操作失败');
+        }
+        Db::commit();
+        return $this->success('成功');
+    }
+
+    /**
+     * 移除 封禁 黑名单
+     * @param int $user_id 主播ID
+     * @param string $room_no 房间号
+     * @param int $admin_id 观众ID
+     * @return bool
+     */
+    public function black_remove(int $user_id, string $room_no, int $audience_id)
+    {
+        if (!$room = $this->getDetail(params: ['room_no' => $room_no])) {
+            return $this->error('直播间已关闭');
+        }
+        if ($room['user_id'] != $user_id) {
+            $model = new LiveRoomAdminModel();
+            if (!$model->getAdmin($user_id, $room_no)) {
+                return $this->error('未拥有此权限');
+            }
+        }
+
+        if (!$info = LiveRoomBlackModel::query()->where(['user_id'=>$user_id,'room_id'=>$room['id'],'room_no'=>$room_no])->first()){
+            return $this->error('您未拉黑过');
+        }else{
+            if ($info->status != 1){
+                return $this->error('已取消拉黑');
+            }
+            if (!LiveRoomBlackModel::query()->where('id',$info->id)->update(['status'=>0])){
+                return $this->error('操作失败');
+            }
+        }
+
+        RedisUtil::getInstance(RedisKeyEnum::ROOM_BLACK,$room_no,im_prefix($audience_id))->del();
+        // 腾讯直播创建房间 && 创建群组
+//        $im = new TencentIm();
+//        if (!$im->unban_group_member($room_no, im_prefix($audience_id))) {
+//            return $this->error($im->getMessage() ?? '操作失败');
+//        }
+        return $this->success('成功');
+    }
+
+    /**
+     * 采集器
+     * @param $value
+     * @param $data
+     * @return mixed
+     */
+    public function dataLogoAttribute($value,$data)
+    {
+        return cdn_url($value);
+    }
+
+    public function dataImageAttribute($value,$data)
+    {
+        return cdn_url($value);
+    }
+
+    // 开播日志
+    public function log()
+    {
+        return $this->hasOne(LiveRoomLogModel::class, 'session', 'session');
+    }
+
+    // 主播信息
+    public function user()
+    {
+        return $this->hasOne(UserModel::class, 'id', 'user_id');
+    }
+}

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

@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Master\Enum\RedisKeyEnum;
+use App\Master\Framework\Library\Tencent\TencentIm;
+use App\Model\Model;
+use App\Utils\RedisUtil;
+use Hyperf\DbConnection\Db;
+
+class LiveSuggestModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'live_suggest';
+
+    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 add(int $user_id,array $data)
+    {
+        $data['user_id'] = $user_id;
+        $data['create_time'] = time();
+        return $this->query()->insert($data);
+    }
+}

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

@@ -0,0 +1,160 @@
+<?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;
+use function Hyperf\Coroutine\co;
+
+class SmsCodeModel extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var ?string
+     */
+    protected ?string $table = 'sms';
+
+    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 $mobile
+     * @param $code
+     * @param string $event
+     * @return bool
+     */
+    public function check($mobile, $code, string $event = 'default', int $currentLimit = 5, int $timeOut = 300)
+    {
+        // 测试验证码
+        if ($code == 1212) {
+            return $this->success();
+        }
+
+        if (!RedisUtil::getInstance(RedisKeyEnum::SMS_MOBILE_CHECK, $mobile)->tryTimes($timeOut,$currentLimit)){
+            return $this->error('校验过于频繁,已被锁定!');
+        }
+
+        if (!$info = self::query()->where(['mobile' => $mobile, 'event' => $event])->orderBy('id', 'desc')->first()) {
+            return $this->error('验证码错误');
+        }
+
+        if ($info->createtime < time() - 600) {
+            $this->flush($mobile, $event);
+            return $this->error('验证码已过期');
+        }
+
+        if ($info->code != $code) {
+            return $this->error('验证码错误!');
+        }
+
+        return $this->success();
+    }
+
+    /**
+     * 发送短信
+     * @param string $mobile
+     * @param $event
+     * @param int $currentLimit
+     * @param int $timeOut
+     * @return bool
+     * @throws \Exception
+     */
+    public function send(string $mobile, $event = 'default', int $currentLimit = 5, int $timeOut = 300)
+    {
+        $code = rand(1000, 9999);
+        if (!$this->create_code($mobile, $code, $event, $currentLimit, $timeOut)) {
+            return $this->error($this->getMessage());
+        }
+        // 第三方发送短信
+
+        return $this->success('发送成功', [
+            'code' => $code
+        ]);
+    }
+
+    /**
+     * 创建验证码
+     * @param string $mobile
+     * @param $code
+     * @param $event
+     * @param int $currentLimit
+     * @param int $timeOut
+     * @return bool
+     * @throws \Exception
+     */
+    private function create_code(string $mobile, $code, $event = 'default', int $currentLimit = 5, int $timeOut = 300)
+    {
+        $time  = time();
+        //验证缓存,如存在则继续限制发送,时间设置 300s
+        if (RedisUtil::getInstance(RedisKeyEnum::SEND_SMS_MOBILE, $mobile)->get()) {
+            return $this->error('您的发送过于频繁!');
+        }
+
+        //5分钟之内 次数超过5次,限制发送,并记录缓存
+        if (!RedisUtil::getInstance(RedisKeyEnum::SEND_SMS_TIMES, $mobile)->tryTimes($timeOut,$currentLimit)) {
+            //每次锁定 递增 锁定时间,达到最大锁定次数则将当日无法发送
+            $end_time = Common::todayTimeRemain();//设置次日凌晨过期
+            if (!$times = RedisUtil::getInstance(RedisKeyEnum::SEND_SMS_TIMEOUT_TIMES, $mobile)->tryTimes($end_time, $currentLimit)) {
+                $timeOut = $end_time;//如果达到最大锁定次数,则设置当日过期
+            } else {
+                $timeOut = $times * $timeOut;//如果未达到最大次数,则依次递增锁定时间
+
+                // 如果最大次数过期时间 超过 当日时间,则直接设置当日过期
+                if ($timeOut > $end_time) {
+                    $timeOut = $end_time;
+                }
+            }
+
+            RedisUtil::getInstance(RedisKeyEnum::SEND_SMS_MOBILE, $mobile)->setex('1', $timeOut);
+            $minutes = $timeOut / 60;
+            return $this->error("发送过于频繁,请{$minutes}分钟后再试");
+        }
+
+        $this->flush($mobile, $event);
+
+        $data = [
+            'event'      => $event,
+            'mobile'     => $mobile,
+            'code'       => $code,
+            'createtime' => $time
+        ];
+        if (!self::query()->insert($data)) {
+            return $this->error('发送失败');
+        }
+
+        return $this->success('发送成功');
+    }
+
+
+    /**
+     * 清除短信
+     * @param $mobile
+     * @param $event
+     * @return bool
+     */
+    public function flush($mobile, $event = 'default')
+    {
+        self::query()->where(['mobile' => $mobile, 'event' => $event])->delete();
+        return $this->success();
+    }
+}

+ 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,
+        ];
+    }
+}

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

@@ -0,0 +1,105 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Model\Arts;
+
+use App\Model\Model;
+use App\Utils\Common;
+use Hyperf\DbConnection\Db;
+
+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 searchMobileAttribute($query, $value, array $params): mixed
+    {
+        if (!isset($value)) {
+            return $query;
+        }
+        return $query->where('mobile', $value);
+    }
+
+    public function searchIdsAttribute($query, $value, array $params): mixed
+    {
+        if (!isset($value)) {
+            return $query;
+        }
+        return $query->whereIn('id', $value);
+    }
+    public function authUserInfo(int $user_id)
+    {
+        return (new UserModel())->getDetail(params: ['id' => $user_id]);
+    }
+
+    public function getByMobile(string $mobile)
+    {
+        $this->is_status_search = 0;
+        return $this->getDetail(params: ['mobile' => $mobile]);
+    }
+
+    /**
+     * 注册
+     * @param string $mobile
+     * @return bool
+     */
+    public function register(string $mobile)
+    {
+        $time = time();
+        $params = [
+            'avatar'     => '',
+            'nickname'   => Common::getRandNickName(),
+            'mobile'     => $mobile,
+            'status'     => 1,
+            'createtime' => $time
+        ];
+
+        //账号注册时需要开启事务,避免出现垃圾数据
+        Db::beginTransaction();
+        if (!$user_id = $this->query()->insertGetId($params)){
+            Db::rollBack();
+            return $this->error('注册失败');
+        }
+        //注册钱包
+        if (!Db::table('user_wallet')->insert(['user_id'=>$user_id])){
+            Db::rollBack();
+            return $this->error('注册失败');
+        }
+        Db::commit();
+
+        return $this->success('注册成功',array_merge($params,[
+            'id' => $user_id
+        ]));
+    }
+
+    // 开播点赞
+    public function roomLike()
+    {
+        return $this->hasOne(LiveRoomLogLikeModel::class, 'user_id', 'id');
+    }
+}

+ 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(!empty($select) ? $select : $this->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(!empty($select) ? $select : $this->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['list_rows'])){
+            return $this;
+        }
+        $page = intval($params['page'] ?? 1);
+        $size = intval($params['list_rows'] ?? 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;
+    }
+}

+ 73 - 0
app/Process/MqttProcess.php

@@ -0,0 +1,73 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Process;
+
+use App\Master\Framework\Library\Mqtt\Subscribe;
+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/");//设置日志存入通道
+
+        $topic = [
+            '/test/subscribe' => 0
+        ];
+
+        $subscribe = new Subscribe();
+        $subscribe->endlessLoop($topic, function ($cline,$message, array $topic) {
+            // 日志统一写入
+            LogUtil::getInstance("Mqtt/");//设置日志存入通道
+            // 接收订阅消息
+            LogUtil::info('订阅主题', self::LOG_MODULE, self::LOG_FUNCTION, $message);
+            // 日志统一写入
+            LogUtil::close();
+        });
+    }
+
+    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'
+        ];
+    }
+}

+ 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:strict',
+            'list_rows'    => 'required|integer:strict',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    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['list_rows']) && $params['page'] <= 0) {
+                return $validator->errors()->add('list_rows', 'list_rows 数据有误');
+            }
+
+        });
+    }
+}

+ 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'
+        ];
+    }
+}

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

@@ -0,0 +1,71 @@
+<?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:strict',
+            'list_rows'    => 'nullable|integer:strict',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    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['list_rows']) && $params['page'] <= 0) {
+                return $validator->errors()->add('list_rows', 'list_rows 数据有误');
+            }
+
+        });
+    }
+}

+ 49 - 0
app/Request/Api/v1/Live/AdminListRequest.php

@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class AdminListRequest 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 [
+            'room_no'    => 'required|string'
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间号',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间号'
+        ];
+    }
+}

+ 52 - 0
app/Request/Api/v1/Live/AdminSetRequest.php

@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class AdminSetRequest 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 [
+            'room_no'    => 'required|string',
+            'chat_id'    => 'required|string',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间号',
+            'chat_id.required' => '请选择操作用户',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间号',
+            'chat_id' => '操作用户',
+        ];
+    }
+}

+ 55 - 0
app/Request/Api/v1/Live/AudienceRequest.php

@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class AudienceRequest 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 [
+            'keyword' => 'nullable|string',
+            'room_no' => 'required|string',
+            'page'    => 'nullable|integer:strict',
+            'list_rows'    => 'nullable|integer:strict',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间号',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'keyword' => '关键词',
+            'room_no' => '直播间号',
+            'page' => '页码',
+            'list_rows' => '条数',
+        ];
+    }
+}

+ 54 - 0
app/Request/Api/v1/Live/BlackAddRequest.php

@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class BlackAddRequest 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 [
+            'room_no'    => 'required|string',
+            'chat_id'    => 'required|string',
+            'time'    => 'nullable|integer:strict',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间号',
+            'chat_id.required' => '请选择操作用户',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间号',
+            'chat_id' => '操作用户',
+            'time' => '禁言时间',
+        ];
+    }
+}

+ 52 - 0
app/Request/Api/v1/Live/BlackRemoveRequest.php

@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class BlackRemoveRequest 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 [
+            'room_no'    => 'required|string',
+            'chat_id'    => 'required|string',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间号',
+            'chat_id.required' => '请选择操作用户',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间号',
+            'chat_id' => '操作用户',
+        ];
+    }
+}

+ 50 - 0
app/Request/Api/v1/Live/FollowRequest.php

@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class FollowRequest 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 [
+            'room_no'    => 'required|string',
+            'status'    => 'required|integer:strict',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间编号',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间编号',
+        ];
+    }
+}

+ 50 - 0
app/Request/Api/v1/Live/KeywordFilterAddRequest.php

@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class KeywordFilterAddRequest 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 [
+            'room_no'    => 'required|string',
+            'keyword'    => 'required|string',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间编号',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间编号',
+        ];
+    }
+}

+ 50 - 0
app/Request/Api/v1/Live/KeywordFilterDelRequest.php

@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class KeywordFilterDelRequest 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 [
+            'room_no'    => 'required|string',
+            'keyword_id'    => 'required|integer:strict',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间编号',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间编号',
+        ];
+    }
+}

+ 51 - 0
app/Request/Api/v1/Live/KeywordFilterListRequest.php

@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class KeywordFilterListRequest 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 [
+            'room_no'    => 'required|string',
+            'page'    => 'nullable|integer:strict',
+            'list_rows'    => 'nullable|integer:strict',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间编号',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间编号',
+        ];
+    }
+}

+ 49 - 0
app/Request/Api/v1/Live/LikeRequest.php

@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class LikeRequest 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 [
+            'room_no'    => 'required|string',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间编号',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间编号',
+        ];
+    }
+}

+ 52 - 0
app/Request/Api/v1/Live/ReportRequest.php

@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class ReportRequest 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 [
+            'room_no'    => 'required|string',
+            'to_chat_id' => 'required|string',
+            'reason'     => 'required|string',
+            'image'      => 'nullable|string',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间编号',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间编号',
+        ];
+    }
+}

+ 51 - 0
app/Request/Api/v1/Live/RoomAddRequest.php

@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class RoomAddRequest 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 [
+            'name'    => 'required|string',
+            'image'   => 'nullable|string',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'name.required' => '请填写直播间名称',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'name' => '直播间名称',
+            'image' => '直播间封面图',
+        ];
+    }
+}

+ 49 - 0
app/Request/Api/v1/Live/RoomCloseRequest.php

@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class RoomCloseRequest 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 [
+            'room_no'    => 'required|string',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间编号',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间编号',
+        ];
+    }
+}

+ 49 - 0
app/Request/Api/v1/Live/RoomDetailRequest.php

@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class RoomDetailRequest 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 [
+            'room_no'    => 'required|string',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间编号',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间编号',
+        ];
+    }
+}

+ 49 - 0
app/Request/Api/v1/Live/RoomJoinRequest.php

@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class RoomJoinRequest 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 [
+            'room_no'    => 'required|string'
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间号',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间号'
+        ];
+    }
+}

+ 71 - 0
app/Request/Api/v1/Live/RoomListRequest.php

@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class RoomListRequest 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:strict',
+            'list_rows'    => 'nullable|integer:strict',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    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['list_rows']) && $params['page'] <= 0) {
+                return $validator->errors()->add('list_rows', 'list_rows 数据有误');
+            }
+
+        });
+    }
+}

+ 49 - 0
app/Request/Api/v1/Live/ShutUpListRequest.php

@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class ShutUpListRequest 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 [
+            'room_no'    => 'required|string'
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间号',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间号',
+        ];
+    }
+}

+ 54 - 0
app/Request/Api/v1/Live/ShutUpRequest.php

@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class ShutUpRequest 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 [
+            'room_no'    => 'required|string',
+            'chat_id'    => 'required|string',
+            'time'    => 'nullable|integer:strict',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间号',
+            'chat_id.required' => '请选择操作用户',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间号',
+            'chat_id' => '操作用户',
+            'time' => '禁言时间',
+        ];
+    }
+}

+ 52 - 0
app/Request/Api/v1/Live/SuggestRequest.php

@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class SuggestRequest 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 [
+            'room_no'    => 'required|string',
+            'title'    => 'required|string',
+            'content'    => 'required|string',
+            'time_desc'    => 'required|string',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间编号',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间编号',
+        ];
+    }
+}

+ 50 - 0
app/Request/Api/v1/Live/TalkSetRequest.php

@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class TalkSetRequest 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 [
+            'room_no'    => 'required|string',
+            'talk_status'    => 'required|integer:strict',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间编号',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间编号',
+        ];
+    }
+}

+ 52 - 0
app/Request/Api/v1/Live/UserInfoRequest.php

@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Request\Api\v1\Live;
+
+use Hyperf\Validation\Request\FormRequest;
+
+class UserInfoRequest 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 [
+            'room_no'    => 'required|string',
+            'chat_id'    => 'required|string',
+        ];
+    }
+
+    /**
+     * 获取已定义验证规则的错误消息
+     */
+    public function messages(): array
+    {
+        return [
+            'room_no.required' => '请填写直播间号',
+            'chat_id.required' => '请选择操作用户',
+        ];
+    }
+
+    /**
+     * 验证的各字段的含义
+     * @return array|string[]
+     */
+    public function attributes(): array
+    {
+        return [
+            'room_no' => '直播间号',
+            'chat_id' => '操作用户',
+        ];
+    }
+}

部分文件因为文件数量过多而无法显示