Răsfoiți Sursa

初始化仓库:添加基础代码

lip 4 luni în urmă
comite
449430f8ac
100 a modificat fișierele cu 13986 adăugiri și 0 ștergeri
  1. 6 0
      .gitignore
  2. 1 0
      .htaccess
  3. 42 0
      .travis.yml
  4. 32 0
      LICENSE.txt
  5. 147 0
      README.en.md
  6. 227 0
      README.md
  7. 1 0
      app/.htaccess
  8. 22 0
      app/AppService.php
  9. 141 0
      app/BaseController.php
  10. 116 0
      app/BaseModel.php
  11. 58 0
      app/ExceptionHandle.php
  12. 8 0
      app/Request.php
  13. 1117 0
      app/common.php
  14. 105 0
      app/common/controller/Api.php
  15. 401 0
      app/common/controller/Pub.php
  16. 347 0
      app/common/controller/Upload.php
  17. 54 0
      app/common/listener/GreenText.php
  18. 9 0
      app/common/listener/GroupChange.php
  19. 144 0
      app/common/listener/UserRegister.php
  20. 4 0
      app/common/middleware.php
  21. 35 0
      app/common/middleware/ApiAuth.php
  22. 52 0
      app/common/middleware/CheckAuth.php
  23. 31 0
      app/common/middleware/Locale.php
  24. 43 0
      app/common/middleware/ManageAuth.php
  25. 76 0
      app/common/task/ClearMessage.php
  26. 52 0
      app/common/task/SetAtRead.php
  27. 99 0
      app/enterprise/controller/Emoji.php
  28. 92 0
      app/enterprise/controller/Files.php
  29. 200 0
      app/enterprise/controller/Friend.php
  30. 524 0
      app/enterprise/controller/Group.php
  31. 985 0
      app/enterprise/controller/Im.php
  32. 49 0
      app/enterprise/listener/GroupChange.php
  33. 5 0
      app/enterprise/middleware.php
  34. 29 0
      app/enterprise/model/ChatDelog.php
  35. 12 0
      app/enterprise/model/Emoji.php
  36. 14 0
      app/enterprise/model/File.php
  37. 28 0
      app/enterprise/model/Friend.php
  38. 25 0
      app/enterprise/model/Group.php
  39. 74 0
      app/enterprise/model/GroupUser.php
  40. 277 0
      app/enterprise/model/Message.php
  41. 765 0
      app/enterprise/model/User.php
  42. 24 0
      app/enterprise/validate/User.php
  43. 20 0
      app/event.php
  44. 212 0
      app/index/controller/Index.php
  45. 570 0
      app/index/controller/Install.php
  46. 17 0
      app/index/route/app.php
  47. 207 0
      app/job/Work.php
  48. 62 0
      app/lang/en_us copy.php
  49. 162 0
      app/lang/en_us.php
  50. 161 0
      app/lang/zh_cn.php
  51. 104 0
      app/manage/controller/Config.php
  52. 165 0
      app/manage/controller/Group.php
  53. 126 0
      app/manage/controller/Index.php
  54. 210 0
      app/manage/controller/Message.php
  55. 190 0
      app/manage/controller/Task.php
  56. 204 0
      app/manage/controller/User.php
  57. 6 0
      app/manage/middleware.php
  58. 45 0
      app/manage/model/Config.php
  59. 19 0
      app/middleware.php
  60. 9 0
      app/provider.php
  61. 9 0
      app/service.php
  62. 105 0
      app/worker/Application.php
  63. 181 0
      app/worker/Events.php
  64. 201 0
      app/worker/command/GatewayWorker.php
  65. 68 0
      composer.json
  66. 3548 0
      composer.lock
  67. 51 0
      config/app.php
  68. 38 0
      config/cache.php
  69. 19 0
      config/captcha.php
  70. 14 0
      config/console.php
  71. 18 0
      config/cookie.php
  72. 8 0
      config/cron.php
  73. 62 0
      config/database.php
  74. 42 0
      config/filesystem.php
  75. 33 0
      config/gateway.php
  76. 16 0
      config/hashids.php
  77. 21 0
      config/jwt.php
  78. 33 0
      config/lang.php
  79. 45 0
      config/log.php
  80. 13 0
      config/middleware.php
  81. 8 0
      config/oss.php
  82. 6 0
      config/preview.php
  83. 41 0
      config/queue.php
  84. 45 0
      config/route.php
  85. 19 0
      config/session.php
  86. 159 0
      config/sms.php
  87. 10 0
      config/trace.php
  88. 45 0
      config/version.php
  89. 28 0
      config/view.php
  90. 12 0
      crontab.txt
  91. 114 0
      example.env
  92. 2 0
      extend/.gitignore
  93. 7 0
      public/404.html
  94. 1 0
      public/assets/css/133.7f9367b6.css
  95. 0 0
      public/assets/css/547.abaee743.css
  96. 1 0
      public/assets/css/567.08ec972a.css
  97. 0 0
      public/assets/css/687.d8d9e482.css
  98. 1 0
      public/assets/css/761.338eef9a.css
  99. 0 0
      public/assets/css/789.62ab164a.css
  100. 0 0
      public/assets/css/982.f6ad2033.css

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+.env
+public/nginx.htaccess
+public/unpackage
+public/.htaccess
+public/h5
+vender/

+ 1 - 0
.htaccess

@@ -0,0 +1 @@
+ 

+ 42 - 0
.travis.yml

@@ -0,0 +1,42 @@
+sudo: false
+
+language: php
+
+branches:
+  only:
+    - stable
+
+cache:
+  directories:
+    - $HOME/.composer/cache
+
+before_install:
+  - composer self-update
+
+install:
+  - composer install --no-dev --no-interaction --ignore-platform-reqs
+  - zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Core.zip .
+  - composer require --update-no-dev --no-interaction "topthink/think-image:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-migration:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-captcha:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-mongo:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-worker:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-helper:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-queue:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-angular:^1.0"
+  - composer require --dev --update-no-dev --no-interaction "topthink/think-testing:^1.0"
+  - zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Full.zip .
+
+script:
+  - php think unit
+
+deploy:
+  provider: releases
+  api_key:
+    secure: TSF6bnl2JYN72UQOORAJYL+CqIryP2gHVKt6grfveQ7d9rleAEoxlq6PWxbvTI4jZ5nrPpUcBUpWIJHNgVcs+bzLFtyh5THaLqm39uCgBbrW7M8rI26L8sBh/6nsdtGgdeQrO/cLu31QoTzbwuz1WfAVoCdCkOSZeXyT/CclH99qV6RYyQYqaD2wpRjrhA5O4fSsEkiPVuk0GaOogFlrQHx+C+lHnf6pa1KxEoN1A0UxxVfGX6K4y5g4WQDO5zT4bLeubkWOXK0G51XSvACDOZVIyLdjApaOFTwamPcD3S1tfvuxRWWvsCD5ljFvb2kSmx5BIBNwN80MzuBmrGIC27XLGOxyMerwKxB6DskNUO9PflKHDPI61DRq0FTy1fv70SFMSiAtUv9aJRT41NQh9iJJ0vC8dl+xcxrWIjU1GG6+l/ZcRqVx9V1VuGQsLKndGhja7SQ+X1slHl76fRq223sMOql7MFCd0vvvxVQ2V39CcFKao/LB1aPH3VhODDEyxwx6aXoTznvC/QPepgWsHOWQzKj9ftsgDbsNiyFlXL4cu8DWUty6rQy8zT2b4O8b1xjcwSUCsy+auEjBamzQkMJFNlZAIUrukL/NbUhQU37TAbwsFyz7X0E/u/VMle/nBCNAzgkMwAUjiHM6FqrKKBRWFbPrSIixjfjkCnrMEPw=
+  file:
+    - ThinkPHP_Core.zip
+    - ThinkPHP_Full.zip
+  skip_cleanup: true
+  on:
+    tags: true

+ 32 - 0
LICENSE.txt

@@ -0,0 +1,32 @@
+
+ThinkPHP遵循Apache2开源协议发布,并提供免费使用。
+版权所有Copyright © 2006-2016 by ThinkPHP (http://thinkphp.cn)
+All rights reserved。
+ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
+
+Apache Licence是著名的非盈利开源组织Apache采用的协议。
+该协议和BSD类似,鼓励代码共享和尊重原作者的著作权,
+允许代码修改,再作为开源或商业软件发布。需要满足
+的条件: 
+1. 需要给代码的用户一份Apache Licence ;
+2. 如果你修改了代码,需要在被修改的文件中说明;
+3. 在延伸的代码中(修改和有源代码衍生的代码中)需要
+带有原来代码中的协议,商标,专利声明和其他原来作者规
+定需要包含的说明;
+4. 如果再发布的产品中包含一个Notice文件,则在Notice文
+件中需要带有本协议内容。你可以在Notice中增加自己的
+许可,但不可以表现为对Apache Licence构成更改。 
+具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.

+ 147 - 0
README.en.md

@@ -0,0 +1,147 @@
+[简体中文](./README.md) | English 
+
+# Instant Messaging
+
+### Introduce
+Raingad-IM is an open source instant communication demo, which needs to be used together with the front and back ends. It is mainly used for learning and communication, and provides you with the development ideas of instant communication. Many functions need to be developed by itself, and the original intention of development is to quickly establish the internal communication system, Intranet communication and community communication.
+
+|  type | url |
+| --------- | ---- |
+| Front-end source code    | https://gitee.com/raingad/im-chat-front |
+| Back-end source code | https://gitee.com/raingad/im-instant-chat |
+| Web Demo | http://im.raingad.com/index.html |
+| H5 Demo | http://im.raingad.com/h5 |
+
+
+Dmeo account:13800000002  password:123456
+
+The ending number can be modified to 2、3、4......18、19、20 
+
+Dmeo account:13800000020  password:123456 
+
+### Supported features
+
+-Supports single chat and group chat, and supports sending emoticons, images, voice, video, and file messages
+-Single chat supports displaying the status of messages that have been read but not read, and displaying online status
+-Group chat creation, deletion, group member management, group announcements, group bans, etc
+-Support for top contacts and message privacy;
+-Support for setting new message sound reminders and browser notifications
+-Support administrator to recall group member messages
+-Support group members cannot add friends to each other
+-Supports one-on-one audio and video calls (connected to both web and mobile devices, not supported by mini programs)
+-Supports online preview of files, images, and most media files
+-Support for mobile devices (H5, APP, and mini programs, some functions are not compatible)
+-New support for enterprise mode and community mode, with community mode supporting registration and adding friends functions
+-The app supports online and offline push of single chat messages (requires self application for unipush service)
+-Support simple backend management, including user management, group management, system settings, etc
+
+
+### Software architecture
+
+Back-end technology stack:`thinkphp6+workerman+redis`
+
+Front-end technology stack:`vue2+Lemon-IMUI+element-UI`
+
+
+### Installation
+
+> Due to the particularity of instant messaging, it is strictly prohibited to use the source code for Trojan horses, viruses, pornography, gambling, fraud and other industries in violation of local laws and regulations, as well as to engage in criminal activities, such as the use of this software for illegal activities, will be reported to the relevant departments and assist the relevant administrative law enforcement agencies to check!
+
+> The installation program needs to have some experience in PHP and server operation and maintenance, if not, please join the communication group to contact the author, the author provides paid deployment services!
+
+#### Preparatory work
+You need to install the running environment first. The BAOTA server is recommended. The LNMP architecture is recommended. The following software needs to be installed:
+
+|  environment | version | remark | Recommended |
+| --------- | ---- | ---- | ---|
+| linux(centOS)    | >= 7.0 |  The following versions were not tested   | 7.9 |
+| nginx    | >= 1.17 |     | latest |
+| php | >= 7.1 |  incompatible php8    | 7.3 |
+| mysql    | >= 5.7 | Must be 5.7 and above     | 5.7 |
+| redis    | >= 5.0 |     | 7.0 |
+
+**Important operation**
+
+1. PHP needs to install an extension : `redis` `fileinfo`
+
+2. PHP needs to undisable the function : `shell_exec` `chown` `exec` `putenv` `proc_open` `pcntl_exec` `pcntl_alarm` `pcntl_fork` `pcntl_waitpid` `pcntl_wait` `pcntl_signal` `pcntl_signal_dispatch`
+
+#### Source code download
+Download the full source code and put it on your own server. Take a look at the  [(releases)](https://gitee.com/raingad/im-instant-chat/releases) at the top of the gitee project home page and download the latest release in the distribution.
+
+#### Start installation
+1. Create a website by pointing the site's running directory to the 'public' directory in the project root.
+
+
+
+2. Enable pseudo-static and set the reverse proxy, the following only shows the pseudo-static and reverse proxy configuration of nginx, Apache is not supported for the time being.
+
+
+``` 
+location ~* (runtime|application)/{
+	return 403;
+}
+location / {
+	if (!-e $request_filename){
+		rewrite  ^(.*)$  /index.php?s=$1  last;   break;
+	}
+}
+
+#Reverse proxy port 8282, no modification required
+
+location /wss
+    {
+      proxy_pass http://127.0.0.1:8282;
+      proxy_http_version 1.1;
+      proxy_set_header Upgrade $http_upgrade;
+      proxy_set_header Connection "Upgrade";
+      proxy_set_header X-Real-IP $remote_addr;
+    }
+```
+
+3. If you have a domain name and want to use services such as audio and video calls and voice messaging, you also need to configure a certificate to enable HTTPS. You can use a free 'Let's Encrypt' certificate. If you don't need these services, you can directly use the HTTP protocol, but the functionality will be limited.
+   
+4. Access your IP or domain name to enter the custom installation wizard.
+   
+
+#### If installation fails
+1.  Enter  `public\sql\database.sql`  to import the database into your own database.
+
+2.  Enter the project root directory, modify  `example.env` to `.env` , and modify the corresponding database parameters. **Please carefully read the configuration instructions in env**.
+
+> if you want to save chat files to oss, you need to configure them in the background. Do not modify the environment configuration files after configuration.
+
+### Start the message push service
+As the chat software requires the use of WebSocket, we need to start Workerman. The system has already built-in the corresponding service, and you can run the service from the backend management homepage ( **accessible from the bottom-left corner after logging in with an administrator account** ). If the backend service fails to start successfully, you need to perform the following debugging steps:
+
+**Reasons for system service startup failure**:
+
+1. If you have started the `php think worker:gateway start` or `php start.php start` commands in the terminal, please run `killall -9 php` in the terminal or restart the server before running again.
+
+2. It could be that PHP is not the default version. Execute `php -v` in the terminal to check if the version number is inconsistent with the selected PHP version when creating the website. If inconsistent, you need to modify the website's version to the default PHP version and install the corresponding dependencies and remove disabled functions for PHP.
+
+3. It could be due to insufficient directory permissions for execution. Reset all directory permissions to `755` for user `www` and try again.
+
+**If the startup fails, you can perform debugging**
+
+4. Enter the project root directory to run `php think worker:gateway start`, or run `php start.php start` to run the message service. Do not use `- d` during testing. Under windows, run the `start_for_ win.bat` file in the root directory directly. Since there are many restrictions on the use of Workerman under Windows, it is recommended to use Linux system in formal environment, while windows system is only recommended for development environment.
+
+5. The message service needs to release port 8282. If you need to modify it, please modify the corresponding parameters in the `WORKER` section of the environment configuration file. For windows users, please modify port 8282 in [`app\worker\start_gateway.php`]. The port number needs to be changed according to the situation.
+   
+6. The system uses the domain name as the address of the websocket service directly, so it needs to configure the proxy in the nginx of the website and listen to port 8282. The parameters of the proxy configuration have been written in the pseudo-static.
+
+7. For more information about the use of workerman, please visit [workerman official website](https://www.workerman.net/) official website.
+
+8. After the deployment, the password for the administrator account is: `administrator``123456`, and the management entry is located in the lower left corner of the chat interface.
+
+### Install deployment Services
+
+The author provides the installation services of the system, including the back-end and front-end deployment to the online, to ensure the perfect operation of the project, 200 yuan per time, the installation service can provide detailed installation tutorials and interface documents, if necessary, you can contact the author!
+
+### QQ Communication group
+If you have any questions, please leave a message or join our QQ group!
+
+It's not easy to create. Click a star.
+
+[QQ Communication group:1031495465](https://qm.qq.com/q/RgHdvLGiMk)
+

+ 227 - 0
README.md

@@ -0,0 +1,227 @@
+简体中文 | [English](./README.en.md)
+
+# IM即时聊天
+
+### 介绍
+Raingad-IM是一个开源的即时通信demo,需要前后端配合使用,主要用于学习交流,为大家提供即时通讯的开发思路,许多功能需要自行开发,开发的初衷旨在快速建立企业内部通讯系统、内网交流、社区交流。
+
+|  类型 | 链接 |备注|
+| --------- | ---- |---- |
+| 技术分享站上线(官网)    | https://www.shooyu.cn | 书瑜网 |
+| 前端源码(含后台管理)    | https://gitee.com/raingad/im-chat-front | 只维护后台管理 |
+| 后端源码(已编译前端) | https://gitee.com/raingad/im-instant-chat |  |
+| web端演示 | http://im.raingad.com/index.html |  |
+| 移动端H5演示 | http://im.raingad.com/h5 | 需要付费获取 |
+| 桌面端/安卓端 | 请进前端演示页下载 |  需要付费获取 |
+
+
+体验账号:13800000002  密码:123456
+
+尾号2、3、4......18、19、20 都是
+
+体验账号:13800000020  密码:123456 
+
+### 支持功能
+
+- 支持单聊和群聊,支持发送表情、图片、语音、视频和文件消息
+- 单聊支持消息已读未读的状态显示,在线状态显示
+- 群聊创建、删除和群成员管理、群公告、群禁言、@群成员等
+- 支持置顶联系人,消息免打扰;
+- 支持设置新消息声音提醒,浏览器通知
+- 支持管理员撤回群成员消息,支持群成员不能互相添加好友
+- 支持一对一音视频通话(已打通web端和移动端,小程序不支持)
+- 支持文件、图片和绝大部分媒体文件在线预览
+- 支持移动端(由uniapp开发,可打包H5、APP和小程序)
+- 全新支持企业模式和社区模式,社区模式支持注册、添加好友功能
+- APP支持单聊消息在线、离线推送(需要自行申请unipush服务)
+- 支持简易后台管理,包括用户管理、群组管理、系统设置等
+
+### 最新更新
+请查看右侧发行版更新日志
+
+**v6.0.0** (2025年5月20日) 
+
+1. 新增移动端和桌面端国际化,可以自由新增语言。
+2. 新增群头像、消息已读、消息转发等消息队列处理;采用默认头像,取消后台生成文字头像,提升性能。
+3. 新增定时清理消息可以删除文件,避免文件占用太大空间。
+4. 移动端调整大量的UI界面和配色,更换了导航栏图标。
+5. 移动端更换了新的保活插件,支持IOS,支持APP提示直接去系统设置消息通知权限和调整省电策略。
+6. 移动端会话列表支持下拉刷新消息,支持双击导航栏消息图标,快速定位未读会话和@我的会话。
+7. 移动端聊天记录列表采用虚拟列表,再多的数据也不对卡顿,采用z-paging,可以丝滑加载更多聊天记录。
+8. 移动端聊天记录滚动时,可以看到新消息数量。
+9. web端后台管理新增建议用户数据统计,可以查看在线用户数和设备数。
+10. 桌面端新增音视频通话窗口移动,支持最小化到右下角,让聊天无遮挡,通话体验更佳!
+11. 优化Lemon-IMUI组件的国际化,以及更新chatarea组件为最新版本。
+12. 修复若干BUG!
+
+### 软件架构
+
+后端技术栈:`thinkphp6+workerman+redis`
+
+前端技术栈:`vue2+Lemon-IMUI+element-UI` [仅更新后台管理,体验完整功能需要购买桌面端]
+
+桌面端:`vue2+Lemon-IMUI+element-UI + electron` [联系作者,捐赠获取]
+
+移动端:`uniapp for vue3 + pinia` [联系作者,捐赠获取]
+
+### 安装教程
+
+<b style="color:red">由于即时通讯的特殊性,严禁将源码用于木马、病毒、色情、赌博、诈骗等违反国家法律法规的行业,以及从事违法犯罪活动,如发现有使用本软件进行非法活动,将向有关部门举报和协助相关行政执法机关清查!</b>
+
+> 本安装教程主要是服务端的安装,请认真仔细按照安装教程来,不要自作主张根据自己的想法来。请结合各个项目中根目录的项目说明文件`README.md`进行相应端的开发和发布。
+
+> 安装程序需要有一定的PHP经验和服务器运维经验,如果没有请加入交流群联系作者,作者提供付费部署服务!
+
+#### 准备工作
+
+需要先安装好运行环境,推荐使用宝塔服务器【服务器最低配置2核2G】,安装LNMP的架构,建议使用nginx作为服务器,不建议使用apache。需要安装以下软件:
+|  所需环境 | 版本 | 备注 | 推荐版本 |
+| --------- | ---- | ---- | ---|
+| linux    | >= 7.0 |  以下的版本未做测试   | 7.9 |
+| nginx    | >= 1.17 |     | 最新的 |
+| php | >= 7.1 |  不兼容8    | 7.3 |
+| mysql    | >= 5.7 | 必须要5.7及以上     | 5.7 |
+| redis    | >= 5.0 |     | 7.0 |
+
+**重要操作**
+>使用宝塔面板更易安装
+
+<b style="color:red">以下内容,非常重要!!!!!如果出现错误,重新检查是否以满足条件。</b>
+
+1、PHP需要安装扩展:`redis` `fileinfo`
+
+2、PHP需要取消禁用函数:`shell_exec` `chown` `exec` `putenv` `proc_open` `pcntl_exec` `pcntl_alarm` `pcntl_fork` `pcntl_waitpid` `pcntl_wait` `pcntl_signal` `pcntl_signal_dispatch`
+
+<b style="color:red">以上内容,非常重要!!!!!如果出现错误,重新检查是否以满足条件。</b>
+
+#### 源码下载
+
+下载发行版【推荐】
+- 【推荐使用】下载完整源码放到自己的服务器上。请注意看gitee项目主页顶部右侧 [发行版链接(戳我下载)](https://gitee.com/raingad/im-instant-chat/releases) ,请在发行版中下载最新发布的版本。
+
+
+
+#### 开始安装
+1. 把代码上传至服务器,将整个目录权限给 `www` 用户,并赋予 `755`权限,创建网站,把网站的运行目录指向项目根目录下的 `public` 目录,运行目录一定要是这个。
+
+2. 设置伪静态和反向代理,下面只展示nginx的伪静态和反向代理配置,apache的请自行百度或者使用chatGPT转换。【直接复制下面全部代码到伪静态中即可】
+
+
+``` 
+#伪静态配置
+location ~* (runtime|application)/{
+	return 403;
+}
+location / {
+	if (!-e $request_filename){
+		rewrite  ^(.*)$  /index.php?s=$1  last;   break;
+	}
+}
+
+#反向代理配置,如果有修改端口,需要替换下方的8282端口
+
+location /wss
+    {
+      proxy_pass http://127.0.0.1:8282;
+      proxy_http_version 1.1;
+      proxy_set_header Upgrade $http_upgrade;
+      proxy_set_header Connection "Upgrade";
+      proxy_set_header X-Real-IP $remote_addr;
+    }
+```
+
+3. 如果有域名并且要使用音视频通话、语音消息等服务还需要配置证书来开启HTTPS,可以使用免费的 `Let's Encrypt` 证书,如果不需要这些服务,可以直接使用HTTP协议和IP地址访问,但是功能会受限。
+   
+4. 访问你的ip或者域名即可进入自定义安装向导,如果访问出错,就是自己的准备工作没有按照要求进行,请检查。
+
+5. 部署完成之后管理员账号密码为:`administrator`  `123456`,管理入口在聊天界面的左下角。
+   
+6. 安装成功后是无法实时接收消息的,请参考下一章“启动消息推送服务”。
+
+#### 如果安装失败
+1.  进入 `public\sql\database.sql` 将数据库导入自己的数据库。
+
+2.  进入项目根目录,修改 `example.env` 为 `.env` ,并修改数据库相应的参数,**请仔细阅读env中的配置说明**。
+
+PS:如需开启聊天文件存入oss,需要在后台中进行配置,配置后不要再对环境配置文件进行修改。
+
+### 启动消息推送服务
+因为是聊天软件需要用到websockt,所以我们需要启动workerman,系统已经内置了相应的服务,可以在后台管理首页 **(管理员账号登录后,从左下角进入)** 进行运行服务。如果后台运行启动不成功,需要进行以下调试:
+
+**系统服务启动失败的原因:**
+
+1. 你可能在终端启动了 `php think worker:gateway start` 或者 `php start.php start` 这两个命令,导致了重复启动消息推送造成了冲突,请在终端中运行 ` killall -9 php ` 或者重启启动服务器再重新到后台管理中启动系统服务。
+
+2. 可能是php不是默认版本,终端执行 `php -v` 查看版本号是否和创建网站是选定的php版本不一致,如果不一致,需要把网站的版本修改为默认的php版本,并php安装对应的依赖和取消禁用函数。
+
+3. 可能是执行的目录权限不够。重新对所有目录设置为 用户 `www` 权限 `755` ,再次重试。
+
+**如果启动失败可进行调试**
+
+1. 进入项目根目录 运行 `php think worker:gateway start` 即可运行消息服务,如果运行失败,请查看上方的准备工作。由于Workerman在Windows下有诸多使用限制,舍弃windows环境。如果没有出现报错,请将运行的命令终止,或者执行`php think worker:gateway stop` ,调试了没有问题的话,可以直接在管理后台启动消息推送服务
+
+2. 消息服务需要放行 8282 端口,如需修改,请修改环境配置文件中`WORKER` 板块的相应参数。
+   
+3. 系统采用直接用域名作为websocket服务的地址,所以需要在网站的nginx中配置代理并监听8282端口,已在伪静态中写了代理配置的参数。
+
+4. 更多关于workerman的使用,请进入[workerman官网](https://www.workerman.net/)官网进行查阅。
+
+
+### 视频教程(无声)
+
+[哟,原来有视频教程!!!](https://files.raingad.com/static/video/jiaocheng.mp4)
+
+### 安装部署服务
+
+作者提供本系统的安装服务,包括后端和前端部署到线上,保证项目的完美运行,200元/次,安装服务可赠送详细的安装教程以及接口文档,如有需要可以进群联系作者!
+
+### 交流群
+请先认真查看本页文档,如果有什么问题,可以留言,有购买移动端需求可以加入我们的QQ群。
+
+【仅限有问题或者购买移动端需求才可以申请加入交流群(长时间不活跃的将被定期清理),加群前请先点Star,否则不予通过】
+
+[QQ 交流群:1031495465](https://qm.qq.com/q/RgHdvLGiMk)
+
+
+## 免责声明
+
+### 1. 基本声明
+
+本软件作为开源项目提供,在法律允许的最大范围内,开发者不对软件的功能性、安全性或适用性作出任何形式的保证,无论是明示的还是暗示的。
+
+### 2. 使用风险声明
+
+2.1 本软件按"现状"提供,使用者需自行承担使用本软件的全部风险。  
+2.2 开发者不对软件的运行可靠性、适用性或与特定需求的兼容性提供任何保证。  
+2.3 使用者应在充分评估风险的基础上决定是否使用本软件。
+
+### 3. 责任限制与豁免
+
+在任何情况下,开发者及其关联方均不对因使用或无法使用本软件而导致的任何损失或损害承担责任,包括但不限于:
+
+- 数据丢失或泄露
+- 利润损失
+- 系统中断
+- 商业机会损失
+- 其他直接、间接或衍生性损失
+
+### 4. 用户义务与责任
+
+4.1 使用者应确保其对本软件的使用符合所有适用的法律法规要求。  
+4.2 对本软件进行修改、分发或二次开发的使用者,需自行承担由此产生的全部责任,包括但不限于:
+
+- 法律风险
+- 知识产权风险
+- 安全风险
+- 数据保护责任
+
+### 5. 开发者权利
+
+5.1 开发者保留对本软件进行更新、修改、调整或停止维护的权利。  
+5.2 开发者可能在不事先通知的情况下修改本软件或相关服务。  
+5.3 开发者保留对本免责声明进行修改的权利。
+
+### 6. 其他条款
+
+6.1 本免责声明的任何部分被认定为无效或不可执行时,其余部分仍然有效。  
+6.2 本免责声明的最终解释权归开发者所有。

+ 1 - 0
app/.htaccess

@@ -0,0 +1 @@
+deny from all

+ 22 - 0
app/AppService.php

@@ -0,0 +1,22 @@
+<?php
+declare (strict_types = 1);
+
+namespace app;
+
+use think\Service;
+
+/**
+ * 应用服务类
+ */
+class AppService extends Service
+{
+    public function register()
+    {
+        // 服务注册
+    }
+
+    public function boot()
+    {
+        // 服务启动
+    }
+}

+ 141 - 0
app/BaseController.php

@@ -0,0 +1,141 @@
+<?php
+declare (strict_types = 1);
+
+namespace app;
+
+use think\App;
+use think\exception\ValidateException;
+use think\Validate;
+use app\manage\model\{Config};
+use think\facade\Cache;
+use thans\jwt\facade\JWTAuth;
+/**
+ * 控制器基础类
+ */
+abstract class BaseController
+{
+    /**
+     * Request实例
+     * @var \think\Request
+     */
+    protected $request;
+
+    /**
+     * 应用实例
+     * @var \think\App
+     */
+    protected $app;
+
+    /**
+     * 是否批量验证
+     * @var bool
+     */
+    protected $batchValidate = false;
+
+    /**
+     * 控制器中间件
+     * @var array
+     */
+    protected $middleware = [];
+
+    /**
+     * 是否批量验证
+     * @var bool
+     */
+    protected $userInfo = [];
+
+        /**
+     * 接收的post数据
+     * @var bool
+     */
+    protected $postData = [];
+
+    protected $uid = 0;
+
+    protected $globalConfig = [];
+
+    protected $chatSetting = [];
+
+    /**
+     * 构造方法
+     * @access public
+     * @param  App  $app  应用对象
+     */
+    public function __construct(App $app)
+    {
+        $this->app     = $app;
+        $this->request = $this->app->request;
+        // 控制器初始化
+        $this->initialize();
+    }
+
+    // 初始化
+    protected function initialize()
+    {
+        $this->userInfo=$this->request->userInfo;
+        $this->uid=$this->userInfo['user_id'] ?? 0;
+        $config=Config::getSystemInfo();
+        if($config){
+            $this->globalConfig = $config;
+            $this->chatSetting = $config['chatInfo'] ?? [];
+        }
+        // 验证版本,如果不一致,就需要退出重新登陆
+        $version =config('app.app_version');
+        $oldVersion=Cache::get('app_version');
+        if($version!=$oldVersion){
+            Cache::set('app_version',$version);
+            JWTAuth::refresh();
+            Cache::delete('systemInfo');
+        }
+    }
+
+    /**
+     * 验证数据
+     * @access protected
+     * @param  array        $data     数据
+     * @param  string|array $validate 验证器名或者验证规则数组
+     * @param  array        $message  提示信息
+     * @param  bool         $batch    是否批量验证
+     * @return array|string|true
+     * @throws ValidateException
+     */
+    protected function validate(array $data, $validate, array $message = [], bool $batch = false)
+    {
+        if (is_array($validate)) {
+            $v = new Validate();
+            $v->rule($validate);
+        } else {
+            if (strpos($validate, '.')) {
+                // 支持场景
+                [$validate, $scene] = explode('.', $validate);
+            }
+            $class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
+            $v     = new $class();
+            if (!empty($scene)) {
+                $v->scene($scene);
+            }
+        }
+
+        $v->message($message);
+
+        // 是否批量验证
+        if ($batch || $this->batchValidate) {
+            $v->batch(true);
+        }
+
+        return $v->failException(true)->check($data);
+    }
+
+    
+    /**
+     * 自动获取前端传递的分页数量
+     * @param \think\Model|\think\model\relation\HasMany $model
+     * @return \think\Paginator
+     */
+    protected function paginate($model)
+    {
+        $limit = $this->request->param('limit', 20);
+        return $model->paginate($limit);
+    }
+
+}

+ 116 - 0
app/BaseModel.php

@@ -0,0 +1,116 @@
+<?php
+/**
+ * Created by PhpStorm
+ * User xiekunyu@kaishanlaw.com
+ * Date 2021/7/9 16:15
+ */
+
+namespace app;
+
+use think\facade\Db;
+use think\Model;
+
+class BaseModel extends Model
+{
+    protected        $defaultSoftDelete = 0;
+    protected        $error             = '';
+    protected static $db_prefix         = 'yu_';
+    protected static $userInfo          = null;
+    protected static $uid          = null;
+
+
+    protected static function init()
+    {
+        self::$db_prefix = config('database.connections.mysql.prefix') ?: "yu_";
+        self::initModel();
+    }
+
+    // 加载模型自动处理
+    public static function initModel()
+    {
+        self::$userInfo=request()->userInfo ?? null;
+        self::$uid=request()->userInfo['user_id'] ?? null;
+    }
+
+    /**
+     * 获取树状信息
+     * @param array $config
+     */
+    public static function getCheckNode($arr, $pid, $field = "parent_id", $table = '')
+    {
+        if (!$table) {
+            $res = self::find($pid);
+        } else {
+            $res = Db::name($table)->find($pid);
+        }
+        if ($res) {
+            if ($res[$field] > 0) {
+                array_unshift($arr, $res[$field]);
+                return self::getCheckNode($arr, $res[$field], $field, $table);
+            }
+        }
+        return $arr;
+    }
+
+    // 获取错误信息
+    public function getError()
+    {
+        return $this->error;
+    }
+
+    /**
+     * 获取模型的json字段数组
+     * @return array
+     */
+    public function getJsonFieldName(): array
+    {
+        return $this->json;
+    }
+
+     // 匹配列表信息
+     public static function filterIdr($data, $many, $field)
+     {
+         if ($many) {
+             $idr = \utils\Arr::arrayToString($data, $field, false);
+         } else {
+             $idr = [];
+             if (is_array($field)) {
+                 foreach ($field as $v) {
+                     $idr[] = $data[$v];
+                 }
+             } else {
+                 $idr = [$data[$field]];
+             }
+         }
+         $key = array_search(0, $idr);
+         if ($key) {
+             array_splice($idr, $key, 1);
+         }
+         $idr  = array_unique($idr);
+ 
+         return $idr ? : [];
+     }
+
+     // 将列表信息
+     public static function matchListKey($data, $field)
+     {
+        $list=[];
+        if(!$data){
+            return $list;
+        }
+        foreach ($data as $k => $v) {
+            $list[$v[$field]]= $v;
+        }
+        return $list;
+     }
+
+    //  获取某一项数据的统计
+     public static function getTotal($map,$where=[],$field,$group){
+        return self::field($field)
+            ->where($map)
+            ->where($where)
+            ->group($group)
+            ->select()->toArray();
+    }
+    
+}

+ 58 - 0
app/ExceptionHandle.php

@@ -0,0 +1,58 @@
+<?php
+namespace app;
+
+use think\db\exception\DataNotFoundException;
+use think\db\exception\ModelNotFoundException;
+use think\exception\Handle;
+use think\exception\HttpException;
+use think\exception\HttpResponseException;
+use think\exception\ValidateException;
+use think\Response;
+use Throwable;
+
+/**
+ * 应用异常处理类
+ */
+class ExceptionHandle extends Handle
+{
+    /**
+     * 不需要记录信息(日志)的异常类列表
+     * @var array
+     */
+    protected $ignoreReport = [
+        HttpException::class,
+        HttpResponseException::class,
+        ModelNotFoundException::class,
+        DataNotFoundException::class,
+        ValidateException::class,
+    ];
+
+    /**
+     * 记录异常信息(包括日志或者其它方式记录)
+     *
+     * @access public
+     * @param  Throwable $exception
+     * @return void
+     */
+    public function report(Throwable $exception): void
+    {
+        // 使用内置的方式记录异常日志
+        parent::report($exception);
+    }
+
+    /**
+     * Render an exception into an HTTP response.
+     *
+     * @access public
+     * @param \think\Request   $request
+     * @param Throwable $e
+     * @return Response
+     */
+    public function render($request, Throwable $e): Response
+    {
+        // 添加自定义异常处理机制
+
+        // 其他错误交给系统处理
+        return parent::render($request, $e);
+    }
+}

+ 8 - 0
app/Request.php

@@ -0,0 +1,8 @@
+<?php
+namespace app;
+
+// 应用请求对象类
+class Request extends \think\Request
+{
+
+}

+ 1117 - 0
app/common.php

@@ -0,0 +1,1117 @@
+<?php
+// 应用公共文件
+use SingKa\Sms\SkSms;
+use GatewayClient\Gateway;
+use \utils\Str;
+use think\facade\Queue;
+/**
+ * 框架内部默认ajax返回
+ * @param string $msg      提示信息
+ * @param string $redirect 重定向类型 current|parent|''
+ * @param string $alert    父层弹框信息
+ * @param bool $close      是否关闭当前层
+ * @param string $url      重定向地址
+ * @param string $data     附加数据
+ * @param int $code        错误码
+ * @param array $extend    扩展数据
+ * @param int $count    总数
+ */
+function success($msg, $data = '', $count = 0, $page = 1, $code = 0)
+{
+    return ret($code, $msg, $data, $count, $page);
+}
+
+/**
+ * 返回警告json信息
+ */
+function warning($msg, $data = '', $count = 0, $page = 1 , $code = 400)
+{
+    return success($msg ? : lang('system.fail'), $data, $count, $page, $code);
+}
+
+/**
+ * 返回错误json信息
+ */
+function error($msg, $code = 502)
+{
+    return ret($code, lang('system.error').':'.$msg ? : lang('system.fail'));
+}
+
+/**
+ * 提前终止信息
+ */
+function shutdown($msg, $code = 401)
+{
+    exit(json_encode(['code' => $code, 'msg' => $msg?:lang('system.forbidden'), 'data' => []]));
+}
+
+
+/**
+ * ajax数据返回,规范格式
+ * @param array $data   返回的数据,默认空数组
+ * @param string $msg   信息
+ * @param int $code     错误码,0-未出现错误|其他出现错误
+ * @param array $extend 扩展数据
+ */
+function ret($code, $msg = "",$data = [],$count=0, $page=0)
+{
+    $ret = ["code" =>$code, "msg" => $msg,'count'=>$count, "data" => $data,'page'=>$page];
+    return json($ret);
+}
+
+
+/* @param string $string 原文或者密文
+* @param string $operation 操作(ENCODE | DECODE), 默认为 DECODE
+* @param string $key 密钥
+* @param int $expiry 密文有效期, 加密时候有效, 单位 秒,0 为永久有效
+* @return string 处理后的 原文或者 经过 base64_encode 处理后的密文
+*
+* @example
+*
+*  $a = authcode('abc', 'ENCODE', 'key');
+*  $b = authcode($a, 'DECODE', 'key');  // $b(abc)
+*
+*  $a = authcode('abc', 'ENCODE', 'key', 3600);
+*  $b = authcode('abc', 'DECODE', 'key'); // 在一个小时内,$b(abc),否则 $b 为空
+*/
+function authcode($string, $operation = 'DECODE', $key = '', $expiry = 3600) {
+
+    $ckey_length = 4;   
+    // 随机密钥长度 取值 0-32;
+    // 加入随机密钥,可以令密文无任何规律,即便是原文和密钥完全相同,加密结果也会每次不同,增大破解难度。
+    // 取值越大,密文变动规律越大,密文变化 = 16 的 $ckey_length 次方
+    // 当此值为 0 时,则不产生随机密钥
+
+    $key = md5($key ? $key : 'default_key'); //这里可以填写默认key值
+    $keya = md5(substr($key, 0, 16));
+    $keyb = md5(substr($key, 16, 16));
+    $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';
+
+    $cryptkey = $keya.md5($keya.$keyc);
+    $key_length = strlen($cryptkey);
+
+    $string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
+    $string_length = strlen($string);
+
+    $result = '';
+    $box = range(0, 255);
+
+    $rndkey = array();
+    for($i = 0; $i <= 255; $i++) {
+        $rndkey[$i] = ord($cryptkey[$i % $key_length]);
+    }
+
+    for($j = $i = 0; $i < 256; $i++) {
+        $j = ($j + $box[$i] + $rndkey[$i]) % 256;
+        $tmp = $box[$i];
+        $box[$i] = $box[$j];
+        $box[$j] = $tmp;
+    }
+
+    for($a = $j = $i = 0; $i < $string_length; $i++) {
+        $a = ($a + 1) % 256;
+        $j = ($j + $box[$a]) % 256;
+        $tmp = $box[$a];
+        $box[$a] = $box[$j];
+        $box[$j] = $tmp;
+        $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
+    }
+     
+    if($operation == 'DECODE') {
+            if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
+                return substr($result, 26);
+            } else {
+                return '';
+            }
+        } else {
+            return $keyc.str_replace('=', '', base64_encode($result));
+    }
+}
+
+function ssoTokenEncode($str,$key='lvzhesso',$expire=0){
+    $ids=encryptIds($str);
+   return authcode($ids,"ENCODE",$key,$expire);
+}
+
+function ssoTokenDecode($str,$key='lvzhesso'){
+    $ids=authcode($str,"DECODE",$key);
+    try{
+        return decryptIds($ids);
+    }catch(\Exception $e){
+        return '';
+    }
+}
+
+
+//id加密
+function encryptIds($str)
+{
+    $hash = config('hashids');
+    return \Hashids\Hashids::instance($hash['length'], $hash['salt'])->encode($str);
+}
+
+//id解密
+function decryptIds($str)
+{
+    $hash = config('hashids');
+    return \Hashids\Hashids::instance($hash['length'], $hash['salt'])->decode($str);
+}
+
+    /**
+    * 短信发送示例
+    *
+    * @mobile  短信发送对象手机号码
+    * @action  短信发送场景,会自动传入短信模板
+    * @parme   短信内容数组
+    */
+    function sendSms($mobile, $action, $parme)
+    {
+        $config = config('sms');
+        //$this->SmsDefaultDriver是从数据库中读取的短信默认驱动
+        $driver = $config['driver'] ?: 'aliyun'; 
+        $conf=$config[$driver];
+        $sms = new SkSms($driver, $conf);//传入短信驱动和配置信息
+        //判断短信发送驱动,非阿里云和七牛云,需将内容数组主键序号化
+        if ($driver == 'aliyun') {
+            $result = $sms->$action($mobile, $parme);
+        } elseif ($driver == 'qiniu') {
+            $result = $sms->$action([$mobile], $parme);
+        } elseif ($driver == 'upyun') {
+            $result = $sms->$action($mobile, implode('|', restoreArray($parme)));
+        } else {
+            $result = $sms->$action($mobile, restoreArray($parme));
+        }
+        if ($result['code'] == 200) {
+            $data['code'] = 200;
+            $data['msg'] = lang('system.sendOK');
+        } else {
+            $data['code'] = $result['code'];
+            $data['msg'] = $result['msg'];
+        }
+        return $data;
+    }
+  	
+    /**
+    * 数组主键序号化
+    *
+    * @arr  需要转换的数组
+    */
+    function restoreArray($arr)
+    {
+        if (!is_array($arr)){
+            return $arr;
+        }
+        $c = 0;
+        $new = [];
+        foreach ($arr as $key => $value) {
+            $new[$c] = $value;
+            $c++;
+        }
+        return $new;
+    }
+
+//密码生成规则
+function password_hash_tp($password,$salt)
+{
+    return md5($salt.$password.$salt);
+}
+
+// 获取主域名
+function getMainHost(){
+    $host=config('app.app_host','');
+    if($host){
+        return $host;
+    }
+    $port=request()->port();
+    $domain=request()->domain();
+    // halt($domain);
+    // 判断url是否有端口
+    if(!hasPort($domain)){
+        if($port!=80  && $port !=443){
+            return request()->domain().":".$port;
+        }
+    }
+    return $domain;
+}
+
+function hasPort($domainOrIp) { 
+     // 查找冒号的位置 
+     $colonPos = strrpos($domainOrIp, ':'); 
+     if ($colonPos!== false) { 
+         // 获取冒号后面的字符串 
+         $portPart = substr($domainOrIp, $colonPos + 1); 
+         // 判断冒号后面的字符串是否为纯数字 
+         return ctype_digit($portPart); 
+     } 
+     return false; 
+} 
+
+// 获取url中的主机名
+function getHost($url){ 
+    if(!preg_match('/http[s]:\/\/[\w.]+[\w\/]*[\w.]*\??[\w=&\+\%]*/is',$url)){
+        return '';
+    }
+    $search = '~^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?~i';
+    $url = trim($url);
+    preg_match_all($search, $url ,$rr);
+    return $rr[4][0];
+}
+
+//根据姓名画头像
+function circleAvatar($str,$s,$uid=0,$is_save=0,$save_path=''){
+    //定义输出为图像类型
+    header("content-type:image/png");
+    $str =$str?:"A";
+    $uid =$uid?:rand(0,10);
+    $text=\utils\Str::getLastName($str,2);
+    $width = $height = $s?:80;
+    if($width<40 or $width>120){
+        $width = $height =80;
+    }
+    $colors=['#F56C6C','#E6A23C','#fbbd08','#67C23A','#39b54a','#1cbbb4','#409EFF','#6739b6','#e239ff','#e03997'];
+    $color=hex2rgb($colors[(int)$uid%10]);
+    $size=$width/4;
+    $textLeft=($height/2)-$size-$width/10;
+    if($width<=80){
+        $text=\utils\Str::getLastName($str,1);
+        $size=$width/2;
+        $textLeft=$size/3;
+    }
+//新建图象
+    $pic=imagecreate($width,$height);
+//定义黑白颜色
+    $background=imagecolorallocate($pic,$color['r'],$color['g'],$color['b']);
+    $textColor=imagecolorallocate($pic,255,255,255);
+    imagefill($pic,0,0,$background);//填充背景色
+//定义字体
+    $font=root_path()."/public/static/fonts/PingFangHeavy.ttf";
+    //写 TTF 文字到图中
+    imagettftext($pic,$size,0,$textLeft,($height/2)+$size/2,$textColor,$font,$text);
+    if($is_save){
+        $path=$save_path."/".$uid.".png";
+        $dir = pathinfo($path,PATHINFO_DIRNAME);
+        if(!is_dir($dir)){
+            $file_create_res = mkdir($dir,0777,true);
+            if(!$file_create_res){
+                imagedestroy($pic);
+                return false;//没有创建成功
+            }
+        }
+        imagepng($pic,$path);
+        imagedestroy($pic);
+        return $path;
+    }else{
+        //输出图象
+        imagepng($pic);
+        //结束图形,释放内存空间
+        imagedestroy($pic);
+        return $pic;
+    }
+}
+
+//头像拼接
+function avatarUrl($path, $str = "雨",$uid=0,$s=80,$is_group=0)
+{
+    if ($path) {
+        // 判断头像路径中是否有http
+        if (strpos($path, 'http') !== false) {
+            $url = $path;
+        } else {
+            $url = getDiskUrl() .'/'. ltrim($path,'/') ;
+        }
+    }else {
+        if($is_group){
+            $url=getMainHost()."/static/img/group.png";
+        }else{
+            $url=getMainHost()."/static/img/avatar.png";;
+        }
+    }
+    return $url;
+    // $str = Str::strFilter($str);
+    // preg_match_all('/[\x{4e00}-\x{9fff}]+/u', $str, $matches);
+    // $str=implode('', $matches[0]);
+    // if($str==''){
+    //     $str="无";
+    // }
+    // if ($path) {
+    //     // 判断头像路径中是否有http
+    //     if (strpos($path, 'http') !== false) {
+    //         $url = $path;
+    //     } else {
+    //         $url = getDiskUrl() .'/'. ltrim($path,'/') ;
+    //     }
+    // }else {
+    //     if($str){
+    //         $url=getMainHost()."/avatar/".$str.'/'.$s.'/'.$uid;
+    //     }else{
+    //         $url='';
+    //     }
+    // }
+    // return $url;
+}
+
+// 获取文件的地址
+function getFileUrl($path){
+    if (strpos($path, 'http') !== false) {
+        return $path;
+    }
+    return getDiskUrl() .'/'. ltrim($path,'/') ;
+}
+
+/**
+ * 十六进制 转 RGB
+ */
+function hex2rgb($hexColor)
+{
+    $color = str_replace('#', '', $hexColor);
+    if (strlen($color) > 3) {
+        $rgb = array(
+            'r' => hexdec(substr($color, 0, 2)),
+            'g' => hexdec(substr($color, 2, 2)),
+            'b' => hexdec(substr($color, 4, 2))
+        );
+    } else {
+        $color = $hexColor;
+        $r = substr($color, 0, 1) . substr($color, 0, 1);
+        $g = substr($color, 1, 1) . substr($color, 1, 1);
+        $b = substr($color, 2, 1) . substr($color, 2, 1);
+        $rgb = array(
+            'r' => hexdec($r),
+            'g' => hexdec($g),
+            'b' => hexdec($b)
+        );
+    }
+    return $rgb;
+}
+
+/**
+ * 将数组按字母A-Z排序
+ * @return [type] [description]
+ */
+function chartSort($array, $field,$isGroup=true,$chart='chart')
+{
+    $newArray = [];
+    foreach ($array as $k => &$v) {
+        $v[$chart] = getFirstChart($v[$field]);
+        $newArray[] = $v;
+    }
+    $data = [];
+    if($isGroup){
+        foreach ($newArray as $k => $v) {
+            if (array_key_exists($v[$chart], $data)) {
+                $data[$v[$chart]][] = $v;
+            } else {
+                $data[$v[$chart]] = [];
+                $data[$v[$chart]][] = $v;
+            }
+        }
+        ksort($data);
+    }else{
+       return $newArray;
+    }
+    return $data;
+}
+
+/**
+ * 返回取汉字的第一个字的首字母
+ * @param  [type] $str [string]
+ * @return [type]      [strind]
+ */
+function getFirstChart($str)
+{
+    $str = str_replace(' ', '', $str);
+    // 过滤特殊符号
+    $str = preg_replace('/[^\x{4e00}-\x{9fa5}A-Za-z0-9]/u', '', $str);
+    if (empty($str) || is_numeric($str)) {
+        return '#';
+    }
+    $char = ord($str[0]);
+    if ($char >= ord('A') && $char <= ord('z')) {
+        return strtoupper($str[0]);
+    }
+    $s1 = iconv('UTF-8', 'gb2312//IGNORE', $str);
+    $s2 = iconv('gb2312', 'UTF-8//IGNORE', $s1);
+    $s = $s2 == $str ? $s1 : $str;
+    $asc = ord($s[0]) * 256 + ord($s[1]) - 65536;
+    if ($asc >= -20319 && $asc <= -20284) return 'A';
+    if ($asc >= -20283 && $asc <= -19776) return 'B';
+    if ($asc >= -19775 && $asc <= -19219) return 'C';
+    if ($asc >= -19218 && $asc <= -18711) return 'D';
+    if ($asc >= -18710 && $asc <= -18527) return 'E';
+    if ($asc >= -18526 && $asc <= -18240) return 'F';
+    if ($asc >= -18239 && $asc <= -17923) return 'G';
+    if ($asc >= -17922 && $asc <= -17418) return 'H';
+    if ($asc >= -17417 && $asc <= -16475) return 'J';
+    if ($asc >= -16474 && $asc <= -16213) return 'K';
+    if ($asc >= -16212 && $asc <= -15641) return 'L';
+    if ($asc >= -15640 && $asc <= -15166) return 'M';
+    if ($asc >= -15165 && $asc <= -14923) return 'N';
+    if ($asc >= -14922 && $asc <= -14915) return 'O';
+    if ($asc >= -14914 && $asc <= -14631) return 'P';
+    if ($asc >= -14630 && $asc <= -14150) return 'Q';
+    if ($asc >= -14149 && $asc <= -14091) return 'R';
+    if ($asc >= -14090 && $asc <= -13319) return 'S';
+    if ($asc >= -13318 && $asc <= -12839) return 'T';
+    if ($asc >= -12838 && $asc <= -12557) return 'W';
+    if ($asc >= -12556 && $asc <= -11848) return 'X';
+    if ($asc >= -11847 && $asc <= -11056) return 'Y';
+    if ($asc >= -11055 && $asc <= -10247) return 'Z';
+    return "#";
+}
+
+// 拼接聊天对象
+function chat_identify($from_user,$to_user){
+    $identify=[$from_user,$to_user];
+    sort($identify);
+    return implode('-',$identify);
+}
+
+//数组中获取ID字符串
+function arrayToString($array,$field,$isStr=true){
+    $idArr = [];
+    foreach ($array as $k => $v) {
+        if(is_array($field)){
+            foreach($field as $val){
+                $idArr[]=$v[$val];
+            }
+        }else{
+            $idArr[] = $v[$field];
+        }
+    }
+    if ($isStr) {
+        $idStr = implode(',', $idArr);
+        return $idStr;
+    } else {
+        return $idArr;
+    }
+
+}
+
+// 根据文件后缀进行分类
+function getFileType($ext,$rst=false){
+    $ext=strtolower($ext);
+    $image=['jpg','jpeg','png','bmp','gif','webp','ico'];
+    $radio=['mp3','wav','wmv','amr'];
+    $video=['mp4','3gp','avi','m2v','mkv','mov'];
+    $doc=['ppt','pptx','doc','docx','xls','xlsx','pdf','txt','md'];
+    $msgType='file';
+    if(in_array($ext,$doc)){
+        $fileType=1;
+    }elseif(in_array($ext,$image)){
+        $fileType=2;
+        $msgType='image';
+    }elseif(in_array($ext,$radio)){
+        $fileType=3;
+        $msgType='voice';
+    }elseif(in_array($ext,$video)){
+        $fileType=4;
+        $msgType='video';
+    }else{
+        $fileType=9;
+    }
+    if($rst){
+        return $msgType;
+    }else{
+        return $fileType;
+    }
+}
+
+/**
+ * 二位数组排序
+ * $array 需要排序的数组
+ * $sort_key 需要排序的字段
+ * $sort_order 正序还是倒序
+ * $sort_type  排序的类型:数字,字母
+ */
+function sortArray($arrays, $sort_key, $sort_order = SORT_ASC, $sort_type = SORT_NUMERIC)
+{
+    if (is_array($arrays)) {
+        foreach ($arrays as $array) {
+            if (is_array($array)) {
+                $key_arrays[] = $array[$sort_key];
+            } else {
+                return false;
+            }
+        }
+    } else {
+        return false;
+    }
+    array_multisort($key_arrays, $sort_order, $sort_type, $arrays);
+    return $arrays;
+}
+
+//gateway向web页面推送消息
+function wsSendMsg($user, $type,  $data, $isGroup=0)
+{
+    $message = json_encode([
+        'type' => $type,
+        'time' => time(),
+        'data' => $data
+    ]);
+    try{
+        Gateway::$registerAddress = config('gateway.registerAddress');
+        if (!$user) {
+            Gateway::sendToAll($message);
+        } else {
+            if (!$isGroup) {
+                $send = 'sendToUid';
+                // 如果是单聊和语音通话需要使用unipush推送
+                $event=$data['extends']['event'] ?? '';
+                if(in_array($type,['simple']) || ($event=='calling' && $type=='webrtc')){
+                    unipush($user,$data);
+                }
+            } else {
+                $send = "sendToGroup";
+            }
+            Gateway::$send($user, $message);
+        }
+     }catch(\Exception $e){
+        //忽略错误
+     }
+    
+}
+
+// 绑定unipush的cid
+function bindCid($uid,$cid){
+    $url=env('unipush.url','');
+    if(!$url){
+        return false;
+    }
+    $data=[
+        'type'=>'bindCid',
+        'alias'=>[[
+            'cid'=>$cid,
+            'alias'=>$uid
+        ]]
+        
+    ];
+    try{
+        $data=json_encode($data);
+        utils\Curl::curl_post($url,$data,true,["Content-Type: application/json"]);
+    }catch(\Exception $e){
+        //忽略错误
+    }
+    
+}
+
+// unipush推送
+function unipush($toUser,$data){
+    $url=env('unipush.url','');
+    if(!$url){
+        return false;
+    }
+    $content='';
+    if($data['type']=='text'){
+        $content=Str::subStr($data['content'],0,50);
+    }else{
+        $content=getMsgType($data['type'],$data['extends']['type'] ?? 0);
+    }
+    // 这个推送不需要发给发送人
+    $fromUser=$data['fromUser']['id'] ?? '';
+    if(is_array($toUser)){
+        $toUser=array_diff($toUser,[$fromUser]);
+    }
+    $is_force=env('unipush.is_force',false);
+    $data=[
+        'type'=>'push',
+        'toUser'=>$toUser,
+        'title'=>$data['fromUser']['displayName'],
+        'content'=>$content,
+        'force_notification'=>$is_force,
+        'payload'=>$data
+    ];
+    try{
+        $data=json_encode($data);
+        utils\Curl::curl_post($url,$data,true,["Content-Type: application/json"]);
+    }catch(\Exception $e){
+        //忽略错误
+    }
+}
+
+// 预览文件
+function previewUrl($url){
+    $previewUrl=env('preview.own','');
+    // $preview='';
+    // $suffix=explode('.',$url);
+    // $ext=$suffix[count($suffix)-1];
+    // $media=['jpg','jpeg','png','bmp','gif','pdf','mp3','wav','wmv','amr','mp4','3gp','avi','m2v','mkv','mov','webp'];
+    // $doc=['ppt','pptx','doc','docx','xls','xlsx','pdf'];
+    // if(in_array($ext,$media) && $previewConf['own']){
+    //     $preview=$previewConf['own']."view.html?src=".$url;
+    // }elseif(in_array($ext,$doc) && $previewConf['yzdcs']){
+    //     $preview=$previewConf['yzdcs'].'?k='.$previewConf['keycode'].'&url='.$url;
+    // }else{
+        
+    // }
+    if($previewUrl){
+        $preview=$previewUrl.$url;
+    }else{
+        $preview=getMainHost()."/view.html?src=".$url;
+    }
+    return $preview;
+}
+
+/**
+ * 解析sql语句
+ * @param  string $content sql内容
+ * @param  int $limit  如果为1,则只返回一条sql语句,默认返回所有
+ * @param  array $prefix 替换表前缀
+ * @return array|string 除去注释之后的sql语句数组或一条语句
+ */
+function parse_sql($sql = '', $limit = 0, $prefix = []) {
+    // 被替换的前缀
+    $from = '';
+    // 要替换的前缀
+    $to = '';
+    // 替换表前缀
+    if (!empty($prefix)) {
+        $to   = current($prefix);
+        $from = current(array_flip($prefix));
+    }
+    if ($sql != '') {
+        // 纯sql内容
+        $pure_sql = [];
+        // 多行注释标记
+        $comment = false;
+        // 按行分割,兼容多个平台
+        $sql = str_replace(["\r\n", "\r"], "\n", $sql);
+        $sql = explode("\n", trim($sql));
+        // 循环处理每一行
+        foreach ($sql as $key => $line) {
+            // 跳过空行
+            if ($line == '') {
+                continue;
+            }
+            // 跳过以#或者--开头的单行注释
+            if (preg_match("/^(#|--)/", $line)) {
+                continue;
+            }
+            // 跳过以/**/包裹起来的单行注释
+            if (preg_match("/^\/\*(.*?)\*\//", $line)) {
+                continue;
+            }
+            // 多行注释开始
+            if (substr($line, 0, 2) == '/*') {
+                $comment = true;
+                continue;
+            }
+            // 多行注释结束
+            if (substr($line, -2) == '*/') {
+                $comment = false;
+                continue;
+            }
+            // 多行注释没有结束,继续跳过
+            if ($comment) {
+                continue;
+            }
+            // 替换表前缀
+            if ($from != '') {
+                $line = str_replace('`'.$from, '`'.$to, $line);
+            }
+            if ($line == 'BEGIN;' || $line =='COMMIT;') {
+                continue;
+            }
+            // sql语句
+            array_push($pure_sql, $line);
+        }
+        // 只返回一条语句
+        if ($limit == 1) {
+            return implode("",$pure_sql);
+        }
+        // 以数组形式返回sql语句
+        $pure_sql = implode("\n",$pure_sql);
+        $pure_sql = explode(";\n", $pure_sql);
+        return $pure_sql;
+    } else {
+        return $limit == 1 ? '' : [];
+    }
+}
+
+/**
+ * 更新或添加环境变量
+ *
+ * @param string $key 环境变量的键
+ * @param string $value 环境变量的值
+ * @return bool 成功返回 true,失败返回 false
+ */
+function updateEnv($key, $value)
+{
+    $envFile = app()->getRootPath() . '.env';
+    if (!file_exists($envFile) || !is_writable($envFile)){
+        return false;
+    }
+
+    // 读取 .env 文件内容
+    $envContent = file_get_contents($envFile);
+    $keyPattern = preg_quote($key, '/');
+    $pattern = "/^{$keyPattern}=(.*)\$/m";
+
+    if (preg_match($pattern, $envContent)) {
+        // 如果找到了键值对,替换其值
+        $replacement = "{$key}={$value}";
+        $newEnvContent = preg_replace($pattern, $replacement, $envContent);
+    } else {
+        // 如果没有找到键值对,添加新的键值对
+        $newEnvContent = $envContent . PHP_EOL . "{$key}={$value}";
+    }
+    // 保存更新后的 .env 文件内容
+    return file_put_contents($envFile, $newEnvContent) !== false;
+}
+
+// 获取文件的域名
+function getDiskUrl(){
+    $disk=env('filesystem.driver','local');
+    $url=getMainHost();
+    if($disk=='aliyun'){
+        $url=env('filesystem.aliyun_url','');
+    }elseif($disk=='qiniu'){
+        $url=env('filesystem.qiniu_url','');
+    }elseif($disk=='qcloud'){
+        $url=env('filesystem.qcloud_cdn','');
+    }
+    $url=rtrim($url,'/');
+    return $url;
+}
+
+/**
+ * 合成图片
+ * @param  array   $pic_list  [图片列表数组]
+ * @param  boolean $is_save   [是否保存,true保存,false输出到浏览器]
+ * @param  string  $save_path [保存路径]
+ * @return boolean|string
+ */
+function getGroupAvatar($pic_list=array(),$is_save=false,$save_path=''){
+    //验证参数
+    if(empty($pic_list) || empty($save_path)){
+        return false;
+    }
+    if($is_save){
+        //如果需要保存,需要传保存地址
+        if(empty($save_path)){
+            return false;
+        }
+    }
+    // 只操作前9个图片
+    $pic_list = array_slice($pic_list, 0, 9);
+    //设置背景图片宽高
+    $bg_w = 150; // 背景图片宽度
+    $bg_h = 150; // 背景图片高度
+    //新建一个真彩色图像作为背景
+    $background = imagecreatetruecolor($bg_w,$bg_h);
+    //为真彩色画布创建白灰色背景,再设置为透明
+    $color = imagecolorallocate($background, 202, 201, 201);
+    imagefill($background, 0, 0, $color);
+    imageColorTransparent($background, $color);
+    //根据图片个数设置图片位置
+    $pic_count = count($pic_list);
+    $lineArr = array();//需要换行的位置
+    $space_x = 3;
+    $space_y = 3;
+    $line_x = 0;
+    switch($pic_count) {
+        case 1: // 正中间
+            $start_x = intval($bg_w/4); // 开始位置X
+            $start_y = intval($bg_h/4); // 开始位置Y
+            $pic_w = intval($bg_w/2); // 宽度
+            $pic_h = intval($bg_h/2); // 高度
+            break;
+        case 2: // 中间位置并排
+            $start_x = 2;
+            $start_y = intval($bg_h/4) + 3;
+            $pic_w = intval($bg_w/2) - 5;
+            $pic_h = intval($bg_h/2) - 5;
+            $space_x = 5;
+            break;
+        case 3:
+            $start_x = 40; // 开始位置X
+            $start_y = 5; // 开始位置Y
+            $pic_w = intval($bg_w/2) - 5; // 宽度
+            $pic_h = intval($bg_h/2) - 5; // 高度
+            $lineArr = array(2);
+            $line_x = 4;
+            break;
+        case 4:
+            $start_x = 4; // 开始位置X
+            $start_y = 5; // 开始位置Y
+            $pic_w = intval($bg_w/2) - 5; // 宽度
+            $pic_h = intval($bg_h/2) - 5; // 高度
+            $lineArr = array(3);
+            $line_x = 4;
+            break;
+        case 5:
+            $start_x = 30; // 开始位置X
+            $start_y = 30; // 开始位置Y
+            $pic_w = intval($bg_w/3) - 5; // 宽度
+            $pic_h = intval($bg_h/3) - 5; // 高度
+            $lineArr = array(3);
+            $line_x = 5;
+            break;
+        case 6:
+            $start_x = 5; // 开始位置X
+            $start_y = 30; // 开始位置Y
+            $pic_w = intval($bg_w/3) - 5; // 宽度
+            $pic_h = intval($bg_h/3) - 5; // 高度
+            $lineArr = array(4);
+            $line_x = 5;
+            break;
+        case 7:
+            $start_x = 53; // 开始位置X
+            $start_y = 5; // 开始位置Y
+            $pic_w = intval($bg_w/3) - 5; // 宽度
+            $pic_h = intval($bg_h/3) - 5; // 高度
+            $lineArr = array(2,5);
+            $line_x = 5;
+            break;
+        case 8:
+            $start_x = 30; // 开始位置X
+            $start_y = 5; // 开始位置Y
+            $pic_w = intval($bg_w/3) - 5; // 宽度
+            $pic_h = intval($bg_h/3) - 5; // 高度
+            $lineArr = array(3,6);
+            $line_x = 5;
+            break;
+        case 9:
+            $start_x = 5; // 开始位置X
+            $start_y = 5; // 开始位置Y
+            $pic_w = intval($bg_w/3) - 5; // 宽度
+            $pic_h = intval($bg_h/3) - 5; // 高度
+            $lineArr = array(4,7);
+            $line_x = 5;
+            break;
+    }
+    foreach( $pic_list as $k=>$pic_path ) {
+        $kk = $k + 1;
+        if ( in_array($kk, $lineArr) ) {
+            $start_x = $line_x;
+            $start_y = $start_y + $pic_h + $space_y;
+        }
+        //获取图片文件扩展类型和mime类型,判断是否是正常图片文件
+        //非正常图片文件,相应位置空着,跳过处理
+        $image_mime_info = @getimagesize($pic_path);
+        if($image_mime_info && !empty($image_mime_info['mime'])){
+            $mime_arr = explode('/',$image_mime_info['mime']);
+            if(is_array($mime_arr) && $mime_arr[0] == 'image' && !empty($mime_arr[1])){
+                switch($mime_arr[1]) {
+                    case 'jpg':
+                    case 'jpeg':
+                        $imagecreatefromjpeg = 'imagecreatefromjpeg';
+                        break;
+                    case 'png':
+                        $imagecreatefromjpeg = 'imagecreatefrompng';
+                        break;
+                    case 'gif':
+                    default:
+                        $imagecreatefromjpeg = 'imagecreatefromstring';
+                        $pic_path = file_get_contents($pic_path);
+                        break;
+                }
+                //创建一个新图像
+                $resource = $imagecreatefromjpeg($pic_path);
+                //将图像中的一块矩形区域拷贝到另一个背景图像中
+                // $start_x,$start_y 放置在背景中的起始位置
+                // 0,0 裁剪的源头像的起点位置
+                // $pic_w,$pic_h copy后的高度和宽度
+                imagecopyresized($background,$resource,$start_x,$start_y,0,0,$pic_w,$pic_h,imagesx($resource),imagesy($resource));
+            }
+        }
+        // 最后两个参数为原始图片宽度和高度,倒数两个参数为copy时的图片宽度和高度
+        $start_x = $start_x + $pic_w + $space_x;
+    }
+    if($is_save){
+        $dir = pathinfo($save_path,PATHINFO_DIRNAME);
+        if(!is_dir($dir)){
+            $file_create_res = mkdir($dir,0777,true);
+            if(!$file_create_res){
+                return false;//没有创建成功
+            }
+        }
+        $res = imagejpeg($background,$save_path);
+        imagedestroy($background);
+        if($res){
+            return true;
+        }else{
+            return false;
+        }
+    }else{
+        //直接输出
+        header("Content-type: image/jpg");
+        imagejpeg($background);
+        imagedestroy($background);
+    }
+}
+
+/**
+ * 获取一个唯一token
+ * @return string
+ */
+function getOnlyToken()
+{
+    return md5(uniqid(md5(microtime(true)), true));
+}
+
+// 设置排序规则
+function orderBy($field, $type, $prefix = '', $default = 'update_time')
+{
+    $type=is_numeric($type)?($type==1?'asc':'desc'):$type;
+    if ($field) {
+        $order = $prefix . $field . ' ' . $type;
+    } else {
+        $order = $prefix . $default . ' desc';
+    }
+    return $order;
+}
+
+// 获取文件后缀图片
+function getExtUrl($path){
+    $ext=explode('.',$path);
+    $ext=end($ext);
+    // 如果是图片文件,就直接返回图片地址
+    $image=['jpg','jpeg','png','bmp','gif','webp'];
+    if(in_array($ext,$image)){
+        return getFileUrl($path);
+    }
+    $extUrl='/static/img/ext/'.strtoupper($ext).'.png';
+    // 判断文件是否存在
+    if(!file_exists(public_path().$extUrl)){
+        $extUrl='/static/img/ext/folder.png';
+    }
+    return getMainHost().$extUrl;
+}
+
+// 字符串内容加解密函数
+function str_encipher($str,$encode=true,$key=''){
+    if($key==''){
+         $key=config('app.aes_chat_key');
+    }
+    if($key=='' || $str==''){
+        return $str;
+    }
+    if($encode){
+        $s=\utils\Aes::encrypt($str,$key);
+    }else{
+        $s=\utils\Aes::decrypt($str,$key) ?:'';
+    }
+    return $s;
+}
+
+// 推送时获取消息的类型
+function getMsgType($type,$callVideo=false){
+    $msgName=lang('messageType.other');
+    switch($type){
+        case 'image':
+            $msgName=lang('messageType.image');
+            break;
+        case 'voice':
+            $msgName=lang('messageType.voice');
+            break;
+        case 'emoji':
+            $msgName=lang('messageType.emoji');
+            break;
+        case 'video':
+            $msgName=lang('messageType.video');
+            break;
+        case 'file':
+            $msgName=lang('messageType.file');
+            break;
+        case 'webrtc':
+            if($callVideo){
+                $msgName=lang('messageType.webrtcAudio');
+            }else{
+                $msgName=lang('messageType.webrtcVideo');
+            }
+            break;
+    }
+    return $msgName;
+}
+
+// 获取app的下载链接
+function getAppDowmUrl($platform='android'){
+    $config=config('version.'.$platform);
+    $name=config('version.app_name');
+    if($platform=='windows'){
+        $packageName=$name."_Setup_".$config['version'].".exe";
+        $path="/downloadApp/windows";
+    }elseif($platform=='mac'){
+        $packageName=$name."_Setup_".$config['version'].".dmg";
+        $path="/downloadApp/mac";
+    }else{
+        $packageName=$name."_Setup_".$config['version'].".apk";
+        $path="/downloadApp/android";
+    }
+    if(is_file(PACKAGE_PATH . $packageName)){
+        return getMainHost().$path;
+    }else{
+        return '';
+    }
+}
+
+// php匹配文本中的所有url
+function getAllUrl($text){
+    // 使用正则表达式匹配带有或不带有协议头的URL
+    $pattern = '/\b(?:https?:\/\/)?[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/i';
+    // 使用preg_replace()函数将URL转换为<a>标签
+    $replaced_text = preg_replace_callback($pattern, function($matches) {
+        $url = $matches[0];
+        if(utils\Regular::is_url($url)){
+            $newUrl=$url;
+            if (!preg_match("~^(?:f|ht)tps?://~i", $url)) {
+                $newUrl = "http://" . $url;
+            }
+            return '<a href="' . $newUrl . '">' . $url . '</a>';
+        }else{
+            return $url;
+        }
+        
+    }, $text);
+    return $replaced_text;
+}
+
+
+// 将链接转成可点击的标签
+function preg_link($text){
+    // 判断文本中是否有img标签
+    if(preg_match('/<img[^>]+>/i', $text)){
+        return $text;
+    }
+    // 匹配更广泛的 URL 的正则表达式
+    $pattern ='/\b(?:https?:\/\/|ftp:\/\/)?([a-z0-9-+&@#\/%?=~_|!:,.;]*\.[a-z]{2,}(?:\/[a-z0-9-+&@#\/%?=~_|!:,.;]*)*)\b/i';
+    // 使用preg_replace()函数将URL转换为<a>标签
+    $replaced_text = preg_replace_callback($pattern, function($matches) {
+        $url = $matches[0];
+		$isUrl=preg_match('/\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]|[a-z0-9-+&@#\/%?=~_|!:,.;]*\.[a-z]{2,}\b/i',$url) ? true : false;
+        if($isUrl){
+            $newUrl=$url;
+            if (!preg_match("~^(?:f|ht)tps?://~i", $url)) {
+                $newUrl = "https://" . $url;
+            }
+            return '<a href="' . $newUrl . '" target="_blank">' . $url . '</a>';
+        }else{
+            return $url;
+        }
+        
+    }, $text);
+    
+    return $replaced_text;
+
+}
+
+//消息队列think-queue
+function queuePush($data, $delay = 0, $job = "Work", $queue = "im")
+{
+    $data['job']   = $data['job'] ?? $job;
+    $data['queue'] = $data['queue'] ?? $queue;
+    $data['data']  = $data['data'] ?? [];
+    try {
+        if ($data) {
+            if ($delay == 0) {
+                Queue::push($job, $data, $queue);
+            } else {
+                Queue::later($delay, $job, $data, $queue);
+            }
+        }
+        return true;
+    } catch (Exception $e) {
+        return false;
+    }
+}
+

+ 105 - 0
app/common/controller/Api.php

@@ -0,0 +1,105 @@
+<?php
+
+namespace app\common\controller;
+
+use think\App;
+use app\enterprise\model\{User,Group};
+use app\index\controller\Extension;
+use think\facade\Session;
+use think\facade\Cache;
+use think\facade\Db;
+use GatewayClient\Gateway;
+use app\manage\model\Config;
+use thans\jwt\facade\JWTAuth;
+
+
+/**
+ * API接口类
+ */
+class Api
+{
+    /**
+     * Request实例
+     * @var \think\Request
+     */
+    protected $request;
+
+    /**
+     * 应用实例
+     * @var \think\App
+     */
+    protected $app;
+
+    protected $middleware=['apiAuth'];
+
+    /**
+     * 构造方法
+     * @access public
+     * @param  App  $app  应用对象
+     */
+    public function __construct(App $app)
+    {
+        $this->app     = $app;
+        $this->request = $this->app->request;
+    }
+
+    // 创建用户
+    public function createUser()
+    {
+        $data = $this->request->param();
+        if(!isset($data['account']) || !isset($data['realname'])){
+            return warning(lang('system.parameterError'));
+        }
+        $user=new User();
+        $verify=$user->checkAccount($data);
+        if(!$verify){
+            return success(lang('user.exist'));
+        }
+        $salt=\utils\Str::random(4);
+        $data['password'] = password_hash_tp(rand(100000,999999),$salt);
+        $data['salt'] =$salt;
+        $data['register_ip'] =$this->request->ip();
+        $data['name_py'] = pinyin_sentence($data['realname']);
+        $user->save($data);
+        $data['user_id']=$user->user_id;
+        $data['open_id']=encryptIds($user->user_id);
+        // 监听用户注册后的操作
+        event('UserRegister',$data);
+        return success(lang('user.registerOk'), $data);
+    }
+
+    // 用户登录
+    public function login()
+    {
+        $param=$this->request->param();
+        $isMobile=$param['is_mobile'] ?? false;
+        if(!isset($param['account']) || !isset($param['open_id'])){
+            return warning(lang('system.parameterError'));
+        }
+        $userInfo=User::where(['account'=> $param['account']])->withoutField('register_ip,login_count,update_time,create_time')->find();
+        if(!$userInfo){
+            return warning(lang('user.exist'));
+        }
+        try{
+            $hash_id=decryptIds($param['open_id']);
+            if($hash_id!=$userInfo['user_id']){
+                return warning(lang('user.exist'));
+            }
+        }catch (\Exception $e){
+            return error($e->getMessage());
+        }
+        $md5=md5(json_encode($userInfo));
+        // 将用户信息缓存5分钟
+        Cache::set($md5,$userInfo,300);
+        // 生成Url
+        if($isMobile){
+            $url=getMainHost().'/h5/#/pages/login/index?token='.$md5;
+        }else{
+            $url=getMainHost().'/#/login?token='.$md5;
+        }
+        return success(lang('user.loginOk'),$url);
+        
+    }
+   
+    
+}

+ 401 - 0
app/common/controller/Pub.php

@@ -0,0 +1,401 @@
+<?php
+
+namespace app\common\controller;
+
+use think\App;
+use app\enterprise\model\{User,Group,GroupUser};
+use think\facade\Session;
+use think\facade\Cache;
+use think\facade\Db;
+use GatewayClient\Gateway;
+use app\manage\model\Config;
+use thans\jwt\facade\JWTAuth;
+
+/**
+ * 控制器基础类
+ */
+class Pub
+{
+    /**
+     * Request实例
+     * @var \think\Request
+     */
+    protected $request;
+
+    /**
+     * 应用实例
+     * @var \think\App
+     */
+    protected $app;
+
+    /**
+     * 构造方法
+     * @access public
+     * @param  App  $app  应用对象
+     */
+    public function __construct(App $app)
+    {
+        Gateway::$registerAddress = config('gateway.registerAddress');
+        $this->app     = $app;
+        $this->request = $this->app->request;
+
+        // 控制器初始化
+        // $this->initialize();
+    }
+
+    public function login(){
+        $param=request()->param();
+        $token=$param['token'] ?? '';
+        // token一键登录
+        if($token){
+            $apiStatus=config('app.api_status');
+            if(!$apiStatus){
+                return warning(lang('system.apiClose'));
+            }
+            $userInfo=Cache::get($token);
+            if(!$userInfo){
+                return warning(lang('user.tokenFailure'));
+            }
+        }else{
+            $verifyTime=md5(request()->ip());
+            $hasError=Cache::get($verifyTime);
+            if($hasError && $hasError>5){
+                return warning(lang('user.loginLimit'));
+            }
+            $userInfo=User::where(['account'=> $param['account']])->withoutField('register_ip,login_count,update_time,create_time')->find();
+            if($userInfo==null){
+                return warning(lang('user.exist'));
+            }
+            if($userInfo['status']==0){
+                return warning(lang('user.forbid'));
+            }
+            $password=password_hash_tp($param['password'],$userInfo['salt']);
+            $code=$param['code'] ?? '';
+            if($code){
+                if($code!=Cache::get($param['account'])){
+                    return warning(lang('user.codeErr'));
+                }
+                Cache::delete($param['account']);
+            }else{
+                if($password!=$userInfo['password']){
+                    $hasError++;
+                    Cache::set($verifyTime,$hasError,300);
+                    return warning(lang('user.passError'));
+                }
+            }
+        }
+        $userInfo['avatar']=avatarUrl($userInfo['avatar'],$userInfo['realname'],$userInfo['user_id']);
+        //    如果用户已经有设置
+        $setting=$userInfo['setting'] ?: '';
+        if($setting){
+            $setting['hideMessageName']= $setting['hideMessageName']=='true' ? true : false;
+            $setting['hideMessageTime']= $setting['hideMessageTime']=='true' ? true : false;
+            $setting['avatarCricle']= $setting['avatarCricle']=='true' ? true : false;
+            $setting['isVoice']= $setting['isVoice']=='true' ? true : false;
+            $setting['sendKey']=(int)$setting['sendKey'];
+            $userInfo['setting']=$setting;
+        }
+        //如果登录信息中含有client——id则自动进行绑定
+        $client_id=$this->request->param('client_id');
+        if($client_id){
+            $cid=$this->request->header('cid','');
+            $this->doBindUid($userInfo['user_id'],$client_id,$cid);
+        }
+        $update=[
+            'last_login_time'=>time(),
+            'last_login_ip'=>$this->request->ip(),
+            'login_count'=>Db::raw('login_count+1')
+        ];
+        User::where('user_id',$userInfo['user_id'])->update($update);
+        $userInfo['qrUrl']=getMainHost().'/scan/u/'.encryptIds($userInfo['user_id']);
+        unset($userInfo['password'],$userInfo['salt']);
+        $userInfo['displayName']=$userInfo['realname'];
+        $userInfo['id']=$userInfo['user_id'];
+        $authToken=User::refreshToken($userInfo,$param['terminal'] ?? 'web');
+        $data=[
+            'sessionId'=>Session::getId(),
+            'authToken'=>$authToken,
+            'userInfo'=>$userInfo
+        ];
+        return success(lang('user.loginOk'),$data);
+   }
+
+    //退出登录
+    public function logout(){
+        try {
+            $jwtData = JWTAuth::auth();
+        } catch (\Exception $e) {
+            return success(lang('user.logoutOk'));
+        }
+
+        $userInfo = $jwtData['info']->getValue();
+        //解密token中的用户信息
+        $userInfo = str_encipher($userInfo,false, config('app.aes_token_key'));
+
+        if (!$userInfo) {
+            return success(lang('user.logoutOk'));
+        }
+        //解析json
+        $userInfo = (array)json_decode($userInfo, true);
+        if($userInfo){
+            $client_id=$this->request->param('client_id','');
+            if($client_id){
+                Gateway::unbindUid($client_id,$userInfo['user_id']);
+                        // 查询团队,如果有团队则加入团队
+                $group=Group::getMyGroup(['gu.user_id'=>$userInfo['user_id'],'gu.status'=>1]);
+                if($group){
+                    $group=$group->toArray();
+                    $group_ids=arrayToString($group,'group_id',false);
+                    foreach($group_ids as $v){
+                        Gateway::leaveGroup($client_id, $v); 
+                    }
+                }
+            }
+            wsSendMsg(0,'isOnline',['id'=>$userInfo['user_id'],'is_online'=>0]);
+        }
+        JWTAuth::invalidate(JWTAuth::token()->get());
+        return success(lang('user.logoutOk'));
+    }
+
+    // 注册用户
+    public function register(){
+        if(env('app.demon_mode',false)){
+            return warning(lang('system.demoMode'));
+        }
+        try{
+            $data = $this->request->param();
+            $ip = $this->request->ip();
+            $systemInfo=Config::getSystemInfo();
+            $registerInterval=$systemInfo['sysInfo']['registerInterval'] ? : 0;
+            if(Cache::has('register_'.md5($ip)) && $registerInterval>0){
+                return warning(lang('user.registerLimit',['time'=>floor($registerInterval/60)]));
+            }
+            // 判断系统是否开启注册
+            if($systemInfo['sysInfo']['regtype']==2){
+                $inviteCode=$data['inviteCode'] ?? '';
+                if(!$inviteCode){
+                    return warning(lang('user.closeRegister'));
+                }
+                if(!Cache::get($inviteCode)){
+                    return warning(lang('user.inviteCode'));
+                }
+            }
+            $code=$data['code'] ?? '';
+            if($code){
+                if($code!=Cache::get($data['account'])){
+                    return warning(lang('user.codeErr'));
+                }
+                Cache::delete($data['account']);
+            }
+            // 接入用户名检测服务
+            event('GreenText',['content'=>$data['realname'],'service'=>"nickname_detection"]);
+            $user=new User();
+            $verify=$user->checkAccount($data);
+            if(!$verify){
+                return warning($user->getError());
+            }
+            $salt=\utils\Str::random(4);
+            if(isset($data['user_id'])){
+                unset($data['user_id']);
+            }
+            if(isset($data['role'])){
+                unset($data['role']);
+            }
+            $data['password'] = password_hash_tp($data['password'],$salt);
+            $data['salt'] =$salt;
+            $data['register_ip'] =$this->request->ip();
+            $data['name_py'] = pinyin_sentence($data['realname']);
+            $user->save($data);
+            $data['user_id']=$user->user_id;
+            // 监听用户注册后的操作
+            event('UserRegister',$data);
+            // x分钟后才能再注册
+            if($registerInterval){
+                Cache::set('register_'.md5($ip),$ip,$registerInterval);
+            }
+            return success(lang('user.registerOk'), $data);
+        }catch (\Exception $e){
+            return error($e->getMessage());
+        }
+
+    }
+
+    //头像生成
+    public function avatar(){
+        //   图像生成函数
+        circleAvatar(input('str'),input('s')?:80,input('uid'));die;
+    }
+
+    /**
+     * 将用户UId绑定到消息推送服务中
+     * @return \think\response\Json
+     */
+    public function bindUid(){
+        $client_id=$this->request->param('client_id');
+        $user_id=$this->request->param('user_id');
+        $cid=$this->request->param('cid','');
+        try{
+            $this->doBindUid($user_id,$client_id,$cid);
+        }catch(\Exception $e){
+            // 未找到用户
+        }
+        return success('');
+    }
+
+    // 执行绑定
+    public function doBindUid($user_id,$client_id,$cid=''){
+        // 如果当前ID在线,将其他地方登陆挤兑下线
+        if(Gateway::isUidOnline($user_id)){
+            wsSendMsg($user_id,'offline',['id'=>$user_id,'client_id'=>$client_id,'isMobile'=>$this->request->isMobile()]);
+        }
+        Gateway::bindUid($client_id, $user_id);
+        // 查询团队,如果有团队则加入团队
+        $group=Group::getMyGroup(['gu.user_id'=>$user_id,'gu.status'=>1]);
+        if($group){
+            $group=$group->toArray();
+            $group_ids=arrayToString($group,'group_id',false);
+            foreach($group_ids as $v){
+                Gateway::joinGroup($client_id, $v); 
+            }
+        }
+        if($cid){
+            bindCid($user_id,$cid);
+        }
+        wsSendMsg(0,'isOnline',['id'=>$user_id,'is_online'=>1]);
+    }
+
+    // 下线通知
+    public function offline(){
+        $user_id=input('user_id');
+        try{
+            $client_ids=Gateway::getClientIdByUid($user_id);
+            // 一个终端登录时才发送下线通知
+            if(count($client_ids)<2){
+                wsSendMsg(0,'isOnline',['id'=>$user_id,'is_online'=>0]);
+            }
+        }catch(\Exception $e){
+            // 未找到用户
+        }
+        return success('');
+    }
+  
+ /**
+     * 将用户团队绑定到消息推送服务中
+     * @return \think\response\Json
+     */
+    public function bindGroup(){
+        $client_id=input('client_id');
+        $group_id=input('group_id');
+        $group_id = explode('-', $group_id)[1];
+        $user_id=Gateway::getUidByClientId($client_id);
+        if($user_id){
+            // 是群成员才能加入群组推送
+            $groupUser=GroupUser::where(['group_id'=>$group_id,'status'=>1,'user_id'=>$user_id])->find();
+            if($groupUser){
+                Gateway::joinGroup($client_id, $group_id); 
+            }
+        }
+        return success('');
+    }
+
+    // 获取系统配置信息
+    public function getSystemInfo(){
+        $systemInfo=Config::getSystemInfo();
+        $systemInfo['demon_mode']=env('app.demon_mode',false);
+        return success('',$systemInfo);
+    }
+
+    // 发送验证码
+    public function sendCode(){
+        $account=$this->request->param('account');
+        $type=$this->request->param('type',1);
+        if(in_array($type,[3,4]) && !$account){
+            $userInfo=request()->userInfo;
+            $acType=\utils\Regular::check_account($userInfo['account']);
+            if($acType){
+                $account=$userInfo['account'];
+            }else{
+                $account=$userInfo['email'];
+            }
+        };
+        $acType=\utils\Regular::check_account($account);
+        if(!$acType){
+            return warning(lang('user.accountVerify'));
+        }
+        if(Cache::get($account.'_time')) return warning(lang('user.waitMinute'));
+        if($type==1){
+            $text=lang('user.loginAccount');
+            $actions="login";
+        }elseif($type==2){
+            $text=lang('user.registerAccount');
+            $actions="register";
+        }elseif($type==3){
+            $text=lang('user.editPass');
+            $actions="changePassword";
+        }else{
+            $text=lang('user.editAccount');
+            $actions="changeUserinfo";
+        }
+        $code=rand(100000,999999);
+        Cache::set($account,$code,300);
+        Cache::set($account.'_time',$code,60);
+        if($acType==2){
+            $conf=Config::where(['name'=>'smtp'])->value('value');
+            $conf['temp']='code';
+            $mail=new \mail\Mail($conf);
+            $mail->sendEmail([$account],$text,$code);
+            return success(lang('system.sendOk'));
+        }else{
+            $parmes=[
+                'code'=>$code
+            ];
+            $res=sendSms($account,$actions,$parmes);
+            return success($res['msg']);
+        }
+    }
+
+    // 检查app版本升级
+    public function checkVersion(){
+        $oldRelease=$this->request->param('release',0);
+        $setupPage=$this->request->param('setupPage',false);
+        $platform=$this->request->param('type',1101);
+        $name=config('version.app_name');
+        $packageName='';
+        if($platform==1101){
+            $teminal='android';
+        }else{
+            $teminal='ios';
+        }
+        $versionInfo=config('version.'.$teminal);
+        $data=[
+            'versionName'=>$versionInfo['version'],
+            'versionCode'=>$versionInfo['release'],
+            'updateType'=>$versionInfo['update_type'],
+            'versionInfo'=>$versionInfo['update_info'],
+            'downloadUrl'=>'',
+        ];
+        // 是否手动检测更新,是的话就不能强制更新或者静默更新
+        if($setupPage){
+            $data['updateType']='solicit';
+        }
+        // 如果旧版本大于等于当前版本则不更新
+        if($oldRelease>=$versionInfo['release']){
+            return success('',$data);
+        }
+        $downUrl='';
+        $android='';
+        // 如果是ios则返回ios地址
+        if($platform==1101){
+            $packageName=$name."_Setup_".$versionInfo['version'].".apk";
+            if(is_file(PACKAGE_PATH . $packageName)){
+                $android = getMainHost().'/unpackage/'.$packageName;
+            }
+            $downUrl=env('app.android_webclip','') ? : $android;
+        }else{
+            $downUrl=env('app.ios_webclip','');
+        }
+        $data['downloadUrl']=$downUrl;
+        return success('',$data);
+    }
+    
+}

+ 347 - 0
app/common/controller/Upload.php

@@ -0,0 +1,347 @@
+<?php
+/**
+ * lvzheAdmin [a web admin based ThinkPHP5]
+ * @author    xiekunyu<raingad@foxmail.com>
+ */
+namespace app\common\controller;
+
+use app\BaseController;
+use app\enterprise\model\{File as FileModel,Message,User,Emoji}; 
+use app\manage\model\{Config}; 
+use think\facade\Filesystem;
+use think\facade\Request;
+use think\File;
+use FFMpeg\FFMpeg;
+use FFMpeg\FFProbe;
+use FFMpeg\Coordinate\TimeCode;
+
+class Upload extends BaseController
+{
+    protected $middleware = ['checkAuth'];
+    protected $disk='';
+    protected $url='';
+
+    public function __construct()
+    {
+        parent::__construct(app());
+        $this->disk=env('filesystem.driver','local');
+        $this->url=getDiskUrl().'/';
+        
+    }
+
+    /**
+     * 文件上传
+     */
+    public function upload($data,$path,$prefix = "",$fileObj = true)
+    {
+        $message=$data['message'] ?? '';
+        if($message){
+            $message=json_decode($message,true);
+        }
+        $uid=request()->userInfo['user_id'] ?? 1;
+        if($fileObj){
+            $filePath = $path;
+        }else{
+            $filePath = new File($path);
+        }
+        $info=$this->getFileInfo($filePath,$path,$fileObj);
+        if($info['ext']=='' && $message){
+            $pathInfo        = pathinfo($message['fileName'] ?? '');
+            $info['ext']     = $pathInfo['extension'];
+            $info['name']    =$message['fileName'] ?? '';
+        }
+        $conf=Config::where(['name'=>'fileUpload'])->value('value');
+        if($conf['size']*1024*1024 < $info['size']){
+            return shutdown(lang('file.uploadLimit',['size'=>$conf['size']]));
+        }
+        // 兼容uniapp文件上传
+        if($info['ext']=='' && isset($data['ext'])){
+            $info['ext']=$data['ext'];
+        }
+        $info['ext']=strtolower($info['ext']);
+        if(!in_array($info['ext'],$conf['fileExt'])){
+            return shutdown(lang('file.typeNotSupport'));
+        }
+        $fileType=getFileType($info['ext']);
+        $imageInfo=[];
+        if($fileType==2){
+            $filecate="image";
+            $imageInfo=$this->getImageSizeInfo($info['path']);
+        }elseif($fileType==3){
+            $msgType=$message['type'] ?? '';
+            // 如果是语音消息,类型才为语音,否者为文件,主要是兼容发送音频文件
+            if($msgType=='voice'){
+                $filecate="voice";
+            }else{
+                $filecate="file";
+            }
+        }elseif($fileType==4){
+            $filecate="video";
+        }else{
+            $filecate="file";
+        }
+        if(!$prefix){
+            $prefix=$filecate.'/'.date('Y-m-d').'/'.$uid."/";
+        }
+        $name=str_replace('.'.$info['ext'],'',$info['name']);
+        $file=FileModel::where(['md5'=>$info['md5']])->find();
+        // 判断文件是否存在,如果有则不再上传
+        if(!$file){
+            $newName   = uniqid() . '.' . $info['ext'];
+            $object = $prefix . $newName;
+            if($this->disk=='local'){
+                $object='storage/'.$object;
+            }
+            Filesystem::disk($this->disk)->putFileAs($prefix, $filePath, $newName);
+        }else{
+            $object = $file['src'];
+        }
+        // 把左边的/去掉再加上,避免有些有/有些没有
+        $object='/'.ltrim($object,'/');
+        $ret = [
+            "src"      => $object,
+            "name"     => $name,
+            "cate" => $fileType,
+            "size"     => $info['size'],
+            "md5"     => $info['md5'],
+            "file_type"     => $info['mime'],
+            "ext"     => $info['ext'],
+            "type"     =>2,
+            'user_id'=>$uid,
+            'videoInfo'=>$imageInfo
+        ];
+        
+        if($message){
+            
+            // 如果发送的文件是图片、视频、音频则将消息类型改为对应的类型
+            if(in_array($fileType,[2,3,4])){
+                $message['type']=$filecate;
+            }
+            if($message['type']=='image'){
+                $message['extends']=$imageInfo;
+            }
+            // 自动获取视频第一帧,视频并且是使用的阿里云
+            if($message['type']=='video'){
+                $videoInfo=$this->getVideoCover($filePath);
+                if($videoInfo){
+                    $extends=$videoInfo['videoInfo'];
+                    $extends['poster']=$this->url.$videoInfo['src'];
+                    $message['extends']=$extends;
+                }else{
+                    $message['extends']['poster']=getMainHost().'/static/common/img/video.png';
+                }
+                // if($this->disk=='aliyun'){
+                //     $message['extends']['poster']=$this->url.$ret['src'].'?x-oss-process=video/snapshot,t_1000,m_fast,w_800,f_png';
+                // }else{
+                //     $message['extends']['poster']=getMainHost().'/static/common/img/video.png';
+                // }
+            }
+            $newFile=new FileModel;
+            // 录音就不保存了
+            if($message['type']!='voice'){
+                $newFile->save($ret);
+            }
+            $message['content']=$ret['src'];
+            $message['file_id']=$newFile->file_id ?? 0;
+            $message['file_cate']=$fileType;
+            $message['file_size']=$info['size'];
+            $message['file_name']= $name.'.'.$info['ext'];
+            $message['user_id']= $uid;
+            $messageModel=new Message();
+            $data=$messageModel->sendMessage($message,$this->globalConfig);
+            if(!$data){
+                return shutdown($messageModel->getError());
+            }
+            return $data;
+        }else{
+            return $ret;
+        }
+        
+    }
+
+    // 上传一般文件
+    public function uploadFile(){
+        $param=$this->request->param();
+        try{
+            $file=request()->file('file');
+            $info=$this->upload($param,$file);
+            return success(lang('file.uploadOk'),$info);
+        } catch(\Exception $e) {
+            return error($e->getMessage().$e->getLine());
+        }
+    }
+
+
+    // 获取上传文件的信息
+    protected function getFileInfo($file,$path,$isObj=false){
+        $info= [
+            'path'=>$file->getRealPath(),
+            'size'=>$file->getSize(),
+            'mime'=>$file->getMime(),
+            'ext'=>$file->extension(),
+            'md5'=>$file->md5(),
+        ];
+        if($isObj){
+            $info['name']=$file->getOriginalName();
+        }else{
+            // 根据路径获取文件名
+            $pathInfo        = pathinfo($path);
+            $info['name']    = $pathInfo['basename'];
+        }
+        return $info;
+        
+    }
+
+    // 上传图片
+    public function uploadImage(){
+        $param=request::param();
+        try{
+            $file=request()->file('file');
+            $info=$this->upload($param,$file,'image/'.date('Y-m-d').'/');
+            $url=$this->url.$info['src'];
+            return success(lang('file.uploadOk'),$url);
+        } catch(\Exception $e) {
+            return error($e->getMessage());
+        }
+    }
+
+    // 普通上传头像
+    public function uploadAvatar(){
+        $param=request::param();
+        try{
+            $file=request()->file('file');
+            $uid=request()->userInfo['user_id'];
+            $info=$this->upload($param,$file,'avatar/'.$uid.'/');
+            User::where(['user_id'=>$uid])->update(['avatar'=>$info['src']]);
+            $url=$this->url.$info['src'];
+            return success(lang('file.uploadOk'),$url);
+        } catch(\Exception $e) {
+            return error($e->getMessage());
+        }
+    }
+
+    // 服务器上传头像
+    public function uploadLocalAvatar($file,$param,$uid){
+        try{
+            $info=$this->upload($param,$file,'avatar/'.$uid.'/',false);
+            return $info['src'];
+        } catch(\Exception $e) {
+            return $e->getMessage().$e->getLine();
+        }
+    }
+
+    // 上传表情
+    public function uploadEmoji(){
+        $param=request::param();
+        try{
+            $file=request()->file('file');
+            $filePath = $file;
+            $uid=request()->userInfo['user_id'] ?? 1;
+            $info=$this->getFileInfo($filePath,$file,true);
+            if($info['ext']==''){
+                $pathInfo        = pathinfo($message['fileName'] ?? '');
+                $info['ext']     = $pathInfo['extension'];
+                $info['name']    =$message['fileName'] ?? '';
+            }
+            // 表情不能大于1m
+            if(2*1024*1024 < $info['size']){
+                return shutdown(lang('file.uploadLimit',['size'=>2]));
+            }
+            // 兼容uniapp文件上传
+            if($info['ext']=='' && isset($param['ext'])){
+                $info['ext']=$param['ext'];
+            }
+            $info['ext']=strtolower($info['ext']);
+            if(!in_array($info['ext'],['jpg','jpeg','gif','png'])){
+                return shutdown(lang('file.typeNotSupport'));
+            }
+            $prefix='emoji/'.$uid.'/';
+            $name=str_replace('.'.$info['ext'],'',$info['name']);
+            $fileInfo=FileModel::where(['md5'=>$info['md5']])->find();
+            // 判断文件是否存在,如果有则不再上传
+            if(!$fileInfo){
+                $newName   = uniqid() . '.' . $info['ext'];
+                $object = $prefix . $newName;
+                if($this->disk=='local'){
+                    $object='storage/'.$object;
+                }
+                Filesystem::disk($this->disk)->putFileAs($prefix, $filePath, $newName);
+                $ret = [
+                    "src"      => $object,
+                    "name"     => $name,
+                    "cate" => 1,
+                    "size"     => $info['size'],
+                    "md5"     => $info['md5'],
+                    "file_type"     => $info['mime'],
+                    "ext"     => $info['ext'],
+                    "type"     =>2,
+                    'user_id'=>$uid,
+                ];
+                $fileInfo=new FileModel;
+                $fileInfo->save($ret);
+            }else{
+                $object = $fileInfo->src;
+            }
+            // 把左边的/去掉再加上,避免有些有/有些没有
+            $object='/'.ltrim($object,'/');
+            $emojiInfo=[
+                'user_id'  => $uid,
+                "src"      => $object,
+                "name"     => $name,
+                "type"     => 2,
+                "file_id"  => $fileInfo->file_id,
+            ];
+            Emoji::create($emojiInfo);
+            return success('',$this->url.$object);
+        } catch(\Exception $e) {
+            return $e->getMessage().$e->getLine();
+        }
+    }
+
+    // 获取图片的尺寸
+    protected function getImageSizeInfo($file){
+        $extends=[];
+        // 如果图片获取图片的尺寸
+        $imageSize = getimagesize($file);
+        $extends['width']=$imageSize[0];
+        $extends['height']=$imageSize[1];
+        // 如果宽大于高则为横图,宽度填充模式,否则为竖图,高度填充模式
+        if($imageSize[0]>=$imageSize[1]){
+            $extends['fixMode']=1;  // 宽度填充
+        }else{
+            $extends['fixMode']=2;  // 高度填充
+        }
+        if($imageSize[0]<200 && $imageSize[1]<240){
+            $extends['fixMode']=3;    // 小图
+        }
+        return $extends;
+    }
+
+    // 获取视频封面
+    public function getVideoCover($filePath){
+        $fileName=pathinfo($filePath,PATHINFO_FILENAME).'.jpg';
+        $ffmpegPath=env('ffmpeg.bin_path','');
+        if(!$ffmpegPath){
+            return false;
+        }
+        $path=array(
+            'ffmpeg.binaries'  => $ffmpegPath.'/ffmpeg',
+            'ffprobe.binaries' => $ffmpegPath.'/ffprobe',
+            'timeout'          => 3600, // 进程超时时间
+            'ffmpeg.threads'   => 12,   // FFMpeg应使用的线程数
+        );
+        $ffmpeg = FFMpeg::create($path);
+        $ffprobe = FFProbe::create($path);
+        $duration=$ffprobe->format($filePath)->get('duration');// 获取 duration 属性
+        $video = $ffmpeg->open($filePath);
+        $frame = $video->frame(TimeCode::fromSeconds(1));
+        $tempPath=root_path().'public/temp';
+        $savePath=$tempPath. '/' .$fileName;
+        $frame->save($savePath);
+        $info=$this->upload([],$savePath,'cover/'.date('Y-m-d').'/',false);
+        $info['videoInfo']['duration']= ceil($duration);
+        unlink($savePath);
+        return $info;
+    }
+
+}

+ 54 - 0
app/common/listener/GreenText.php

@@ -0,0 +1,54 @@
+<?php
+namespace app\common\listener;
+
+use think\api\Client;
+
+// 检测敏感词
+class GreenText
+{
+    protected $contentTypes = [  
+        'ad' => '广告引流',  
+        'political_content' => '涉政内容',  
+        'profanity' => '辱骂内容',  
+        'contraband' => '违禁内容',  
+        'sexual_content' => '色情内容',  
+        'violence' => '暴恐内容',  
+        'nonsense' => '无意义内容',  
+        'negative_content' => '不良内容',  
+        'religion' => '宗教内容',  
+        'cyberbullying' => '网络暴力',  
+        'ad_compliance' => '广告法合规'  
+    ];
+
+    public function handle($data){
+        $token = config('app.thinkapi_token');
+        if(!$token){
+            return true;
+        }
+        $client = new Client($token);
+        $pattern = '/<[^>]*>/';
+        $content = preg_replace($pattern, '', $data['content']);
+        $result = $client->greenText()
+            ->withService($data['service'])
+            ->withContent(\utils\Str::msubstr($content,0,500))
+            ->request();
+        
+        if($result && $result['code']==0){
+            $data=$result['data'] ?? '';
+            $labels=$data['labels'] ?? '';
+            if(!$labels){
+                return true;
+            }
+            $labels=explode(',',$labels);
+            $keywords=[];
+            if($labels){
+                foreach($labels as $v){
+                    $keywords[]=$this->contentTypes[$v] ?? $v;
+                }
+                $msg="您发布的内容包含".implode('、',$keywords)."等违规内容,系统已自动清除!";
+                return shutdown($msg,500);
+            }
+        }
+        return true;
+    }
+}

+ 9 - 0
app/common/listener/GroupChange.php

@@ -0,0 +1,9 @@
+<?php
+namespace app\common\listener;
+
+// 群聊变更
+class GroupChange
+{
+    public function handle($data){
+    }
+}

+ 144 - 0
app/common/listener/UserRegister.php

@@ -0,0 +1,144 @@
+<?php
+namespace app\common\listener;
+
+use app\enterprise\model\{User,Message,Group,GroupUser,Friend};
+use app\manage\model\{Config};
+
+// 监听用户注册后的操作
+class UserRegister
+{
+
+    public function handle(User $user,$data){
+        try{
+            // 查询相关配置信息
+            $sysInfo=Config::where(['name'=>'sysInfo'])->value('value');
+            if($sysInfo['runMode']!=2){
+                return true;
+            }
+            // 获取聊天设置
+            $chatInfo=Config::where(['name'=>'chatInfo'])->value('value');
+            $autoAdduser=$chatInfo['autoAddUser'] ?? [];
+            $autoTask=$this->createConf();
+            // 是否开启自动客服
+            if($autoAdduser && $autoAdduser['status']==1){
+                // 获取客服ID
+                if($autoTask['user_id']!=0){
+                    $autoTask['user_id']=$this->findNextOrFirstId($autoAdduser['user_ids'], $autoTask['user_id'] ? : $autoAdduser['user_ids'][0]);
+                }else{
+                    $autoTask['user_id']=$autoAdduser['user_ids'][0] ?? '';
+                }
+                if($autoTask['user_id']){
+                    $user->update(['cs_uid'=>$autoTask['user_id']],['user_id'=>$data['user_id']]);
+                    // 设置双方为好友
+                    Friend::create(['create_user'=>$data['user_id'],'friend_user_id'=>$autoTask['user_id'],'status'=>1]);
+                    Friend::create(['create_user'=>$autoTask['user_id'],'friend_user_id'=>$data['user_id'],'status'=>1]);
+                    // 如果设置了欢迎语则发送欢迎语
+                    if($autoAdduser['welcome'] ?? ''){
+                        $userInfo=$user->field('user_id,realname,avatar')->where(['user_id'=>$autoTask['user_id']])->find();
+                        if($userInfo){
+                            $userInfo['dispalayName']=$userInfo['realname'];
+                            $userInfo['id']=$userInfo['user_id'];
+                            $userInfo['avatar']=avatarUrl($userInfo['avatar'],$userInfo['realname'],$userInfo['user_id']);
+                            $msg=[
+                                'id'=>\utils\Str::getUuid(),
+                                'user_id'=>$autoTask['user_id'],
+                                'content'=>$autoAdduser['welcome'],
+                                'toContactId'=>$data['user_id'],
+                                'sendTime'=>time()*1000,
+                                'type'=>'text',
+                                'is_group'=>0,
+                                'status'=>'succeed',
+                                'fromUser'=>$userInfo,
+                                'at'=>[]
+                            ];
+                            Message::sendMsg($msg,0,1);
+                        }
+                    }
+                }
+            }
+            $autoAddGroup=$chatInfo['autoAddGroup'] ?? [];
+            // 是否自动加入群聊
+            if($autoAddGroup && $autoAddGroup['status']==1){
+                $group_id=$autoTask['group_id']??0;
+                $uid=$autoAddGroup['owner_uid'] ?? 0;
+                // 未设置群主就不能生成群
+                if($uid){
+                    $userInfo=$user->field('user_id,realname,avatar')->where(['user_id'=>$uid])->find();
+                    // 成员不存在也不进行操作了
+                    if(!$userInfo){
+                        return false;
+                    }
+                    $groupInfo=Group::where(['group_id'=>$group_id])->find();
+                    // 如果没有群ID就需要创建
+                    if(!$groupInfo){
+                        $groupInfo=$this->createGroup($autoAddGroup,$userInfo,$autoTask['group_num']);
+                    }
+                    $groupUserCount=GroupUser::where(['group_id'=>$group_id,'status'=>1])->count();
+                    if($groupUserCount > ($autoAddGroup['userMax'] ?? 100) - 1 ){
+                        // 创建下一个群聊
+                        $groupInfo=$this->createGroup($autoAddGroup,$userInfo,++$autoTask['group_num']);
+                    }
+                    $groupInfo['joinerName']=$data['realname'];
+                    // 进入群聊并发送通知
+                    GroupUser::joinGroup($data['user_id'],$uid,$groupInfo,'autoAddGroup');
+                    // 记录本次的群聊ID
+                    $autoTask['group_id']=$groupInfo['group_id'];
+                }
+            }
+            Config::update(['value'=>$autoTask],['name'=>'autoTask']);
+            return true;
+        }catch(\Exception $e){
+            return shutdown($e->getMessage().$e->getLine());
+        }
+
+    }
+
+    // 创建配置文件
+    public function createConf(){
+        $autoTask=Config::where(['name'=>'autoTask'])->value('value');
+        if($autoTask){
+            return $autoTask;
+        }else{
+            $autoTask=[
+                'group_id'=>0, //群聊ID
+                'group_num'=>1, //群聊序号
+                'user_id'=>0,  //上一次的客服ID
+            ];
+            Config::create(['name'=>'autoTask','value'=>$autoTask]);
+            return $autoTask;
+        }
+    }
+
+    // 查找ID的下一个值,如果未找到则使用第一个ID  
+    public function findNextOrFirstId($ids, $searchId) { 
+        if(!$ids){
+            return 0;
+        }
+        // 遍历数组查找$searchId  
+        foreach ($ids as $k => $v) {  
+            // 如果找到了$searchId,则返回它的下一个值(如果存在)  
+            if ($v == $searchId && isset($ids[$k + 1])) {  
+                return $ids[$k + 1];  
+            }  
+        }  
+        // 如果未找到$searchId,则返回第一个元素  
+        return $ids[0];
+    }  
+
+    // 自动创建群聊
+    public function createGroup($autoAddGroup,$userInfo,$group_num){ 
+        $data=[
+            'create_user'=>$userInfo['user_id'],
+            'owner_id'=>$userInfo['user_id'],
+            'name'=>($autoAddGroup['name'] ?? lang('group.name')).$group_num,
+            'name_py'=>pinyin_sentence($autoAddGroup['name'].$group_num),
+            'setting'=>json_encode(['manage' => 0, 'invite' => 1, 'nospeak' => 0]),
+         ];
+         $group=new Group;
+         $group->save($data);
+        //  群主首先进群
+         $group->joinerName=$userInfo['realname'];
+         GroupUser::joinGroup($userInfo['user_id'],$userInfo['user_id'],$group,'autoCreateGroup');
+         return $group;
+    }
+}

+ 4 - 0
app/common/middleware.php

@@ -0,0 +1,4 @@
+<?php
+return [
+"locale"
+];

+ 35 - 0
app/common/middleware/ApiAuth.php

@@ -0,0 +1,35 @@
+<?php
+namespace app\common\middleware;
+
+//验证权限
+class ApiAuth
+{
+    public function handle($request, \Closure $next)
+    {
+        $apiStatus=config('app.api_status');
+        if(!$apiStatus){
+            return shutdown(lang('system.apiClose'));
+        }
+        $appId=config('app.app_id');
+        $appSecret=config('app.app_secret');
+        $header = $request->header();
+        $app_id=$header['x-im-appid'] ?? '';
+        $timeStamp=$header['x-im-timestamp'] ?? 0;
+        $sign=$header['x-im-sign'] ?? '';
+        if(!$app_id || !$timeStamp || !$sign){
+            return shutdown(lang('system.parameterError'));
+        }
+        // 时间戳不能大约60秒
+        if(time()-$timeStamp>60){
+            return shutdown(lang('system.longTime'));
+        }
+        if($appId!=$app_id){
+            return shutdown(lang('system.appIdError'));
+        }
+        $signStr=md5($appId.$timeStamp.$appSecret);
+        if($sign!=$signStr){
+            return shutdown(lang('system.signError'));
+        }
+        return $next($request);
+    }
+}

+ 52 - 0
app/common/middleware/CheckAuth.php

@@ -0,0 +1,52 @@
+<?php
+namespace app\common\middleware;
+
+use Exception;
+use thans\jwt\exception\TokenInvalidException;
+use thans\jwt\facade\JWTAuth;
+use think\facade\Cache;
+//验证权限
+class CheckAuth
+{
+    public function handle($request, \Closure $next)
+    {
+        try {
+            $jwtData = JWTAuth::auth();
+        } catch (Exception $exception) {
+
+            //token有误
+            if (get_class($exception) == TokenInvalidException::class) {
+                return shutdown(lang('user.loginError'), -1);
+            }
+
+            $errorMsgArr = [
+                'Must have token' => lang('user.mustToken'),
+                'The token is in blacklist.' => lang('user.blacklist'),
+                'The token is expired.' => lang('user.expired'),
+                'The token is in blacklist grace period list.' => lang('user.expired')
+            ];
+            return shutdown($errorMsgArr[$exception->getMessage()] ?? $exception->getMessage(), -1);
+        }
+
+        $userInfo = $jwtData['info']->getValue();
+        //解密token中的用户信息
+        $userInfo = str_encipher($userInfo,false, config('app.aes_token_key'));
+
+        if (!$userInfo) {
+            return shutdown(lang('user.loginError'), -1);
+        }
+        //解析json
+        $userInfo = (array)json_decode($userInfo, true);
+        
+        if(cache('forbidUser_'.$userInfo['id'])){
+            JWTAuth::invalidate(JWTAuth::token()->get());
+            Cache::delete('forbidUser_'.$userInfo['id']);
+            return shutdown(lang('user.forbid'), -1);
+        }
+        //已经登陆,将用户信息存入请求头
+        $request->userInfo  = $userInfo;
+        $request->uid       = $userInfo['id'];
+        $request->userToken = JWTAuth::token()->get();
+        return $next($request);
+    }
+}

+ 31 - 0
app/common/middleware/Locale.php

@@ -0,0 +1,31 @@
+<?php
+ 
+namespace app\common\middleware;
+use think\facade\Lang; 
+class Locale
+{
+    public function handle($request, \Closure $next)
+    {
+        $locale = $request->header('Accept-Language'); // 从HTTP头获取语言设置
+        $config=lang::getConfig();
+        if ($locale) {
+            $extLang=$config['extend_list'];
+            if(!isset($extLang[$locale])){
+                $lang=$config['default_lang'];
+            }
+            $accept_lang=$config['accept_language'];
+            // 检测替换包
+            if(isset($accept_lang[$locale])){
+                $lang=$accept_lang[$locale];
+            }else{
+                $lang=$config['default_lang'];
+            }
+            // 根据Accept-Language头设置语言
+            Lang::setLangSet($lang); // 例如 'zh-cn' 或 'en'
+        } else {
+            // 如果没有指定语言,可以设置默认语言
+            Lang::setLangSet($config['default_lang']); // 默认语言设置为中文
+        }
+        return $next($request);
+    }
+}

+ 43 - 0
app/common/middleware/ManageAuth.php

@@ -0,0 +1,43 @@
+<?php
+namespace app\common\middleware;
+//验证权限
+class ManageAuth
+{
+    public function handle($request, \Closure $next)
+    {
+        
+        // 设置演示模式,演示模式下无法修改配置
+        $request->demonMode=env('app.demon_mode',false);
+        if($request->userInfo['user_id']!=1){
+            if(!$request->demonMode){
+                if($request->userInfo['role']==0){
+                    shutdown(lang('system.notAuth'),-1);
+                }
+            }else{
+                $rules=[
+                    'user/add',
+                    'user/edit',
+                    'user/del',
+                    'user/setrole',
+                    'user/setstatus',
+                    'user/editpassword',
+                    'group/del',
+                    'group/changeowner',
+                    'group/delgroupuser',
+                    'task/starttask',
+                    'task/stoptask',
+                    'config/setconfig',
+                    'index/publishnotice',
+                    'index/delnotice',
+                    'message/dealmsg',
+                ];
+                // 获取pathinfo信息
+                $pathinfo = strtolower($request->pathinfo());
+                if(in_array($pathinfo,$rules)){
+                    return shutdown(lang('system.demoMode'),400);
+                }
+            }
+        }
+        return $next($request);
+    }
+}

+ 76 - 0
app/common/task/ClearMessage.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace app\common\task;
+
+use yunwuxin\cron\Task;
+use think\Exception;
+use think\facade\Db;
+use app\manage\model\{Config};
+use app\enterprise\model\Message;
+
+// 自动清理消息定时任务
+class ClearMessage extends Task
+{
+    
+    //    定时任务日志内容
+    protected $content='';
+    protected $path='';
+    protected $daytime=86400;
+
+    /**
+     * 自动写入定时任务日志
+     * @return \think\response\Json
+     */
+    protected function writeLog($text)
+    {
+        $this->path = root_path() . 'crontab.txt';
+
+        $content = '重置中!';
+        if (!file_exists($this->path)) {
+            fopen($this->path, 'w');
+        }
+        if (date('d') != 10) {
+            $content = file_get_contents($this->path);
+        }
+        file_put_contents($this->path, $content . date('Y-m-d H:i:s') . ':' . $text . PHP_EOL);
+    }
+
+    public function configure()
+    {
+        //设置每天2点执行
+        $this->dailyAt('02:00'); 
+    }
+
+    /**
+     * 执行任务
+     * @return mixed
+     */
+    protected function execute()
+    {
+        if(date('H:i')!='02:00'){
+            return false;
+        }
+        try {
+           $config=Config::getSystemInfo();
+           $status=$config['chatInfo']['msgClear'] ?? false;
+           $days=$config['chatInfo']['msgClearDay'] ?? 0;
+           if($status && $days){
+                $time=time() - ($days * $this->daytime);
+                $where[]=['create_time','<',$time];
+                $where[]=['chat_identify','<>','admin_notice']; //不删除公告
+                $where[]=['to_user','<>',-1]; //不删除收藏
+                $fileIds=Message::where($where)->where([['type','in',['image','video','file']],['file_id','>',0]])->column('file_id');
+                queuePush(['action'=>'clearFiles','fileIds'=>array_unique($fileIds)],10);
+                $list=Message::where($where)->where([['type','=','voice']])->column('content');
+                queuePush(['action'=>'clearVoice','list'=>$list],60);
+                Message::where($where)->delete();
+                // 整理数据碎片
+                $sql="ALTER TABLE `".config('database.connections.mysql.prefix')."message` ENGINE = InnoDB;";
+                Db::execute($sql);
+           }
+           print "****************消息清理成功******************\n";
+        } catch (Exception $e) {
+            print '消息清理失败:'.$e->getMessage()."\n";
+        }
+    }
+}

+ 52 - 0
app/common/task/SetAtRead.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace app\common\task;
+
+use yunwuxin\cron\Task;
+use think\Exception;
+use think\facade\Cache;
+use app\manage\model\{Config};
+use app\enterprise\model\Message;
+
+// 自动清理消息定时任务
+class SetAtRead extends Task
+{
+    
+    //    定时任务日志内容
+    protected $content='';
+    protected $path='';
+    protected $daytime=86400;
+
+    public function configure()
+    {
+        //设置每天8点执行
+        $this->everyMinute(); 
+    }
+
+    /**
+     * 执行任务
+     * @return mixed
+     */
+    protected function execute()
+    {
+        try {
+           $atListQueue=Cache::get('atListQueue');
+            if($atListQueue){
+                foreach ($atListQueue as $key=>$val){
+                    $message=Message::where('msg_id',$key)->value('at');
+                    $atList=($message ?? null) ? explode(',',$message): [];
+                    // 两个数组取差集
+                    $uniqueArr=array_unique($val);
+                    $newAtList = array_filter($atList, function ($value) use ($uniqueArr) {
+                        return !in_array($value, $uniqueArr);
+                    });
+                    Message::where('msg_id',$key)->update(['at'=>implode(',',$newAtList)]);
+                }
+                Cache::delete('atListQueue');
+            }
+            print "****************设置已读成功******************\n";
+        } catch (Exception $e) {
+            print '设置已读失败:'.$e->getMessage()."\n";
+        }
+    }
+}

+ 99 - 0
app/enterprise/controller/Emoji.php

@@ -0,0 +1,99 @@
+<?php
+
+namespace app\enterprise\controller;
+
+use app\BaseController;
+
+use app\enterprise\model\{Emoji as EmojiModel,File,Message};
+use think\facade\Filesystem;
+use think\facade\View;
+class Emoji extends BaseController
+{
+    // 表情列表
+    public function index()
+    {
+        $map=['status'=>1,'user_id'=>$this->uid,'type'=>2];
+        $list = EmojiModel::where($map)->field('id,name,src,file_id')->order('update_time desc')->select();
+        $data=[];
+        if($list){
+            $data=$list->toArray();
+            foreach ($data as $k => $v) {
+                $url=getFileUrl($v['src']);
+                $data[$k]['src'] =$url;
+                $data[$k]['title'] =$v['name'];
+            }
+        }
+        return success('', $data, count($data));
+    }
+
+    // 添加表情
+    public function add(){
+        $param = $this->request->param();
+        $file_id=$param['file_id'];
+        $fileInfo=File::find($file_id);
+        if(!$fileInfo){
+            return warning(lang('system.exits'));
+        }
+        $exist=EmojiModel::where(['user_id'=>$this->uid,'file_id'=>$file_id])->find();
+        // 判断是否已经有了当前表情,有了就更新
+        if($exist){
+            EmojiModel::where(['id'=>$exist['id']])->update(['update_time'=>time()]);
+        }else{
+            $info=[
+                'user_id'=>$this->uid,
+                'type'=>2,
+                'file_id'=>$file_id,
+                'name'=>$fileInfo->name,
+                'src'=>$fileInfo->src,
+            ];
+            EmojiModel::create($info);
+        }
+        return success(lang('system.addOk'));
+    }
+
+    // 删除表情
+    public function del(){
+        $ids = $this->request->param('ids',[]);
+        if(!is_array($ids) || $ids==[]){
+            return warning(lang('system.parameterError'));
+        }
+        foreach($ids as $id){
+            $emoji=EmojiModel::where(['id'=>$id,'user_id'=>$this->uid])->find();
+            if(!$emoji){
+                continue;
+            }
+            $res=EmojiModel::where(['id'=>$id])->delete();
+            if($res){
+                $exist=EmojiModel::where(['file_id'=>$emoji['file_id']])->find();
+                $exist2=Message::where(['file_id'=>$emoji['file_id']])->find();
+                // 如果文件没有引用了,就删除掉源文件
+                if(!$exist || !$exist2){
+                    $disk=env('filesystem.driver','local');
+                    $file=File::find($emoji['file_id']);
+                    Filesystem::disk($disk)->delete($file->src);
+                }
+            }
+        }
+        return success(lang('system.delOk'));
+        
+    }
+
+    // 移动表情
+    public function move(){
+        $ids = $this->request->param('ids',[]);
+        if(!is_array($ids) || $ids==[]){
+            return warning(lang('system.parameterError'));
+        }
+        foreach($ids as $id){
+            $emoji=EmojiModel::where(['id'=>$id,'user_id'=>$this->uid])->find();
+            if(!$emoji){
+                continue;
+            }
+            EmojiModel::where(['id'=>$id])->update(['update_time'=>time()]);
+        }
+        return success(lang('system.success'));
+        
+    }
+
+
+}

+ 92 - 0
app/enterprise/controller/Files.php

@@ -0,0 +1,92 @@
+<?php
+
+namespace app\enterprise\controller;
+
+use app\BaseController;
+
+use app\enterprise\model\{File,User,Message};
+use think\facade\View;
+class Files extends BaseController
+{
+    // 文件列表
+    public function index()
+    {
+        $param = $this->request->param();
+        $is_all = $param['is_all'] ?? 0;
+        $map = [];
+        $data=[];
+        // 如果是查询全部,就查询file表,否则查询message表
+        if ($is_all) {
+            if ($param['cate'] ?? 0) {
+                $map[] = ['cate', '=', $param['cate']];
+            }
+            $model = new File();
+            if ($param['keywords'] ?? '') {
+                $model = $model->where('name', 'like', '%' . $param['keywords'] . '%');
+            }
+            $list = $this->paginate($model->where($map)->order('file_id desc'));
+            
+            if ($list) {
+                $data = $list->toArray()['data'];
+                $userList = User::matchUser($data, true, 'user_id', 120);
+                foreach ($data as $k => $v) {
+                    $url=getFileUrl($v['src']);
+                    $data[$k]['src'] =$url;
+                    $data[$k]['preview'] = previewUrl($url);
+                    $data[$k]['extUrl'] = getExtUrl($v['src']);
+                    $data[$k]['name'] = $v['name'].'.'.$v['ext'];
+                    $data[$k]['msg_type'] = getFileType($v['ext'],true);
+                    $data[$k]['user_id_info'] = $userList[$v['user_id']] ?? [];
+                    $data[$k]['download'] = getMainHost().'/filedown/'.encryptIds($v['file_id']);
+                }
+                
+            }
+        } else {
+            $map = [
+               ['file_id', '>', 0],
+               ['type', '<>', 'voice'],
+               ['is_group', '=', 0],
+               ['is_undo', '=', 0],
+            ];
+            if ($param['cate'] ?? 0) {
+                $map[] = ['file_cate', '=', $param['cate']];
+            }
+            $user_id = $this->uid;
+            $model = new Message();
+            if ($param['keywords'] ?? '') {
+               $map[] = ['file_name', 'like', '%' . $param['keywords'] . '%'];
+            }
+            $role = $param['role'] ?? 0;
+            $where=[];
+            if($role==1){
+               $map[] = ['from_user', '=', $user_id];
+            }elseif($role==2){
+               $map[] = ['to_user', '=', $user_id];
+            }else{
+                $where='(from_user='.$user_id.' or to_user='.$user_id.')';
+            }
+
+            $list = $this->paginate($model->where($map)->where($where)->order('create_time desc'));
+            if ($list) {
+                $data = $list->toArray()['data'];
+                $userList = User::matchUser($data, true, 'from_user', 120);
+                foreach ($data as $k => $v) {
+                    $content=str_encipher($v['content'],false);
+                    $url=getFileUrl($content);
+                    $data[$k]['src'] = $url;
+                    $data[$k]['preview'] = previewUrl($url);
+                    $data[$k]['extUrl'] = getExtUrl($content);
+                    $data[$k]['cate'] = $v['file_cate'];
+                    $data[$k]['name'] = $v['file_name'];
+                    $data[$k]['size'] = $v['file_size'];
+                    $data[$k]['msg_type'] = $v['type'];
+                    $ext=explode('.',$content);
+                    $data[$k]['ext'] = end($ext);
+                    $data[$k]['user_id_info'] = $userList[$v['from_user']] ?? [];
+                    $data[$k]['download'] = getMainHost().'/filedown/'.encryptIds($v['file_id']);
+                }
+            }
+        }
+        return success('', $data, $list->total(), $list->currentPage());
+    }
+}

+ 200 - 0
app/enterprise/controller/Friend.php

@@ -0,0 +1,200 @@
+<?php
+
+namespace app\enterprise\controller;
+
+use app\BaseController;
+
+use app\enterprise\model\{ChatDelog, Friend as FriendModel,User,Message};
+
+class Friend extends BaseController
+{
+    // 好友申请列表
+    public function index()
+    {
+        $param = $this->request->param();
+        $map = [];
+        $map[]=['is_invite','=',1];
+        $isMine=$param['is_mine'] ?? 0;
+        if($isMine){
+            // 我发起的
+            $map[]=['create_user','=',$this->uid];
+        }else{
+            // 我收到的
+            $map[]=['friend_user_id','=',$this->uid];
+        }
+        $data=[];
+        $model = new FriendModel();
+        $list = $this->paginate($model->where($map)->order('friend_id desc'));
+        if ($list) {
+            $data = $list->toArray()['data'];
+            $userList = User::matchUser($data, true, ['create_user','friend_user_id'], 120);
+            foreach ($data as $k => $v) {
+                $data[$k]['create_user_info'] = $userList[$v['create_user']] ?? [];
+                $data[$k]['user_id_info'] = $userList[$v['friend_user_id']] ?? [];
+                $data[$k]['is_group'] = 0;
+            }
+        }
+        return success('', $data,$list->total(),$list->currentPage());
+    }
+
+    // 添加好友
+    public function add()
+    {
+        $param = $this->request->param();
+        $user_id=$param['user_id'] ?? 0;
+        if(!$user_id){
+            return warning(lang('system.notNull'));
+        }
+        if($user_id==$this->uid){
+            return warning(lang('friend.notAddOwn'));
+        }
+        // 查看是否限制了好友上限
+        if($this->userInfo['friend_limit']!=0 && $this->userInfo['role']==0){
+            $myFriend=FriendModel::where(['create_user'=>$this->userInfo['user_id']])->count();
+            // 好友已达上限
+            if($myFriend>$this->userInfo['friend_limit'] || $this->userInfo['friend_limit']<0){
+               return warning(lang('friend.limit'));
+            }
+         }
+        $friend=FriendModel::where(['friend_user_id'=>$user_id,'create_user'=>$this->uid])->find();
+        if($friend){
+            if($friend->status==1){
+                return warning(lang('friend.already'));
+            }elseif($friend->status==2){
+                return warning(lang('friend.repeatApply'));
+            }elseif($friend->status==3){
+                return warning(lang('friend.refuse'));
+            }
+        }
+        $status=2;
+        $otherFriend=FriendModel::where(['friend_user_id'=>$this->uid,'create_user'=>$user_id])->find();
+        if($otherFriend){
+            if($otherFriend->status==3){
+                return warning(lang('friend.refuse'));
+            }
+            if($otherFriend->status>0){
+                $status=1;
+            }
+        }
+        $model = new FriendModel();
+        $data=[
+            'friend_user_id'=>$user_id,
+            'status'=>$status,
+            'create_user'=>$this->uid,
+            'remark'=>$param['remark'],
+            'is_invite'=>1 // 是否为发起方
+        ];
+        $model->save($data);
+        $msg=[
+            'fromUser'=>[
+                'id'=>'system',
+                'displayName'=>lang('friend.new'),
+                'avatar'=>'',
+            ],
+            'toContactId'=>'system',
+            'id'=>uniqid(),
+            'is_group'=>2,
+            'content'=>lang('friend.apply'),
+            'status'=>'succeed',
+            'sendTime'=>time()*1000,
+            'type'=>'event',
+            'fileSize'=>0,
+            'fileName'=>'',
+        ];
+        // 发送好友申请
+        wsSendMsg($user_id,'simple',$msg);
+        return success(lang('system.addOk'));
+    }
+
+    // 接受或者拒绝好友申请
+    public function update()
+    {
+        $param = $this->request->param();
+        $friend=FriendModel::find($param['friend_id']);
+        if(!$friend){
+            return warning(lang('friend.notApply'));
+        }
+        $map=[
+            'friend_id'=>$param['friend_id']
+        ];
+        FriendModel::where($map)->update(['status'=>$param['status']]);
+        // 如果是接收,就添加到好友列表
+        if($param['status']){
+            $data=[
+                'friend_user_id'=>$friend->create_user,
+                'create_user'=>$this->uid,
+            ];
+            $newFriend=FriendModel::where($data)->find();
+            if($newFriend){
+                FriendModel::where($data)->update(['status'=>1]);
+                return success(lang('friend.already'));
+            }else{
+                $data['status']=1;
+                FriendModel::create($data);
+            }
+            $content=lang('friend.newChat');
+            $userM=new User;
+            // 将对方的信息发送给我,把我的信息发送对方
+            $user=$userM->setContact($friend->create_user,0,'event',$content);
+            if($user){
+                wsSendMsg($this->uid,'appendContact',$user);
+            }
+            $myInfo=$userM->setContact($this->uid,0,'event',$content);
+            if($myInfo){
+                wsSendMsg($friend->create_user,'appendContact',$myInfo);
+            }
+            
+        }
+        return success(lang('system.success'));
+    }
+
+
+    // 删除好友
+    public function del()
+    {
+        $param = $this->request->param();
+        $map=['friend_user_id'=>$param['id'],'create_user'=>$this->uid];
+        $friend=FriendModel::where($map)->find();
+        if(!$friend){
+            return warning(lang('friend.not'));
+        }
+        $is_black=$param['is_black'] ?? 0;
+        // 如果是加入黑名单,则更新状态为3,禁止加好友
+        if($is_black==1){
+           FriendModel::where($map)->update(['status'=>3]);
+           FriendModel::where(['friend_user_id'=>$this->uid,'create_user'=>$param['id']])->update(['status'=>3]);
+        }else{
+            // 需要删除双方的好友关系
+            FriendModel::where($map)->delete();
+            FriendModel::where(['friend_user_id'=>$this->uid,'create_user'=>$param['id']])->delete();
+        }
+        // 删除双方的聊天记录
+        $chat_identify=chat_identify($this->uid,$param['id']);
+        Message::where(['chat_identify'=>$chat_identify])->delete();
+        ChatDelog::where(['to_user'=>$param['id'],'user_id'=>$this->uid,'is_group'=>0])->delete();
+        // 性质和删除群聊一样
+        wsSendMsg($param['id'],'removeGroup',['group_id'=>$this->uid]);
+        return success(lang('system.delOk'));
+    }
+
+    // 设置好友备注
+    public function setNickname()
+    {
+        $param = $this->request->param();
+        if(!$param['nickname']){
+            return warning(lang('system.notNull'));
+        }
+        FriendModel::update(['nickname'=>$param['nickname']],['friend_id'=>$param['friend_id']]);
+        return success(lang('system.editOk'));
+    }
+
+    // 获取最新的一条和申请的总数
+    public function getApplyMsg(){
+        $model = new FriendModel();
+        $map[]=['friend_user_id','=',$this->uid];
+        $map[]=['status','=',2];
+        $count=$model->where($map)->count();
+        return success('', $count);
+    }
+
+}

+ 524 - 0
app/enterprise/controller/Group.php

@@ -0,0 +1,524 @@
+<?php
+
+namespace app\enterprise\controller;
+
+use app\BaseController;
+use app\enterprise\model\{User,Group as GroupModel,GroupUser,Message};
+use think\Exception;
+use think\facade\Db;
+use app\common\controller\Upload;
+use GatewayClient\Gateway;
+use utils\Str;
+
+class Group extends BaseController
+{
+
+   protected $setting=['manage' => 0, 'invite' => 1, 'nospeak' => 0];
+      // 获取联系人列表
+   public function getAllUser(){
+      $param=$this->request->param();
+      $user_ids=isset($param['user_ids'])?$param['user_ids']:[];
+      $groupId=$param['group_id'] ?? '';
+      $group_id='';
+      if($groupId){
+         $group_id=explode('-',$groupId)[1];
+      }
+      $data=User::getAllUser([['status','=',1],['user_id','<>',$this->userInfo['user_id']]],$user_ids,$this->uid,$group_id);
+      return success('',$data);
+   }
+
+   // 获取群成员
+   public function groupUserList()
+   {
+      $param = $this->request->param();
+      try {
+         $group_id = explode('-', $param['group_id'])[1];
+         $listRows = $this->request->param('limit',0);
+         $pageSize = $this->request->param('page',1);
+         $map=['group_user.group_id' => $group_id,'group_user.status'=>1];
+          $field="user_id,realname,account,avatar,name_py";
+         if($listRows){
+            $list=GroupUser::where($map)->order('role asc')->withJoin(['userInfo' => explode(',',$field)], 'LEFT')->paginate(['list_rows'=>$listRows,'page'=>$pageSize]);
+            $data=$list->toArray()['data'];
+            $count=$list->total();
+         }else{
+            $data=GroupUser::where($map)->order('role asc')
+               ->withJoin(['userInfo' => explode(',',$field)], 'LEFT')
+               ->select()
+               ->toArray();
+            $count=count($data);
+         }
+         foreach($data as $k=>$v){
+            $user=$v['userInfo'];
+            $data[$k]['userInfo']['id']=$user['user_id'];
+            $data[$k]['userInfo']['displayName']=$user['realname'];
+            $data[$k]['userInfo']['avatar']=avatarUrl($user['avatar'], $user['realname'], $user['user_id']);
+            $data[$k]['realname']=$user['realname'];
+            $data[$k]['name_py']=$user['name_py'];
+         }
+         return success('', $data,$count);
+      } catch (Exception $e) {
+         return error($e->getMessage());
+      }
+   }
+
+   // 获取群基本信息
+   public function groupInfo()
+   {
+      $param = $this->request->param();
+      try {
+         $jm='qr';
+         $groupId=$param['group_id'] ?? '';
+         $groupInfo = explode('-', $groupId);
+         $group_id=$groupInfo[1];
+         $group=GroupModel::find($group_id)->toArray();
+         $userList=User::matchUser($group,false,'owner_id');
+         $userCount=GroupUser::where(['group_id'=>$group_id,'status'=>1])->count();
+         $userInfo=$userList[$group['owner_id']];
+         $expire=time()+7*86400;
+         $token=urlencode(authcode($this->uid.'-'.$group_id,'ENCODE', $jm,7*86400));
+         $qrUrl=getMainHost().'/scan/g/'.$token;
+         $group['id']=$groupId;
+         $group['qrUrl']=$qrUrl;
+         $group['qrExpire']=date('m-d',$expire);
+         $group['userInfo']=$userInfo;
+         $group['ownerName']=$userInfo['realname'];
+         $group['groupUserCount']=$userCount;
+         $group['displayName']=$group['name'];
+         $group['avatar']=avatarUrl($group['avatar'],$group['name'],$group['group_id'],120);
+         $group['setting']=$group['setting']?json_decode($group['setting'],true):['manage' => 0, 'invite' => 1, 'nospeak' => 0];
+         $group['isJoin']=GroupUser::where(['group_id'=>$group_id,'user_id'=>$this->uid])->value('role') ?: 0;
+         return success('', $group);
+      } catch (Exception $e) {
+         return error($e->getMessage());
+      }
+   }
+
+   // 修改团队名称
+   public function editGroupName()
+   {
+      $param = $this->request->param();
+      $group_id = explode('-', $param['id'])[1];
+      $role=GroupUser::where(['group_id'=>$group_id,'user_id'=>$this->userInfo['user_id']])->value('role');
+      if($role>2){
+         return warning(lang('group.notAuth'));
+      }
+      GroupModel::where(['group_id' => $group_id])->update(['name' => $param['displayName'],'name_py'=>pinyin_sentence($param['displayName'])]);
+      $param['editUserName'] = $this->userInfo['realname'];
+      $action='editGroupName';
+      event('GroupChange', ['action' => $action, 'group_id' => $group_id, 'param' => $param]);
+      wsSendMsg($group_id, $action, $param, 1);
+      return success(lang('system.editOk'));
+   }
+
+   // 添加群成员
+   public function addGroupUser(){
+      $param = $this->request->param();
+      $uid=$this->userInfo['user_id'];
+      $group_id = explode('-', $param['id'])[1] ?? 0;
+      if(!$group_id){
+         return warning(lang('system.exits'));
+      }
+      $groupInfo=GroupModel::where(['group_id'=>$group_id])->find();
+      if(!$groupInfo){
+         return warning(lang('system.exits'));
+      }
+      $user_ids=$param['user_ids'];
+      $groupUserCount=GroupUser::where(['group_id'=>$group_id,'status'=>1])->count();
+      if((count($user_ids) + $groupUserCount) > $this->chatSetting['groupUserMax'] && $this->chatSetting['groupUserMax']!=0){
+         return warning(lang('group.userLimit',['userMax'=>$this->chatSetting['groupUserMax']]));
+      }
+      if(count($user_ids)>20){
+         return warning(lang('group.inviteLimit',['limit'=>20]));
+      }
+      try{
+         foreach($user_ids as $k=>$v){
+            $item=[
+               'group_id'=>$group_id,
+               'user_id'=>$v,
+               'role'=>3,
+               'invite_id'=>$uid
+            ];
+            $hasUser=GroupUser::where(['group_id'=>$group_id,'user_id'=>$v])->find();
+            // 查询是否有人
+            if(!$hasUser){
+               GroupUser::create($item);
+            }
+            // 如果是黑名单用户,则更新状态为正常,只有群主才有重新拉回黑名单的人权限
+            if($hasUser && $hasUser['status']==3 && $groupInfo['owner_id']==$uid){
+               GroupUser::where(['group_id'=>$group_id,'user_id'=>$v])->update(['status'=>1,'invite_id'=>$uid]);
+            }
+         }
+         // 给新成员添加新群聊信息
+         $user=new User();
+         $data=$user->setContact($group_id,1,'text',lang('group.invite',['username'=>$this->userInfo['realname']]),$groupInfo);
+         queuePush(['action'=>'createAvatar','group_id'=>$group_id]);
+         wsSendMsg($user_ids, 'addGroup', $data);
+         return success(lang('system.addOk'));
+      }catch(Exception $e){
+         return error($e->getMessage());
+      }
+   }
+
+      // 设置管理员
+      public function setManager(){
+         $param = $this->request->param();
+         $uid=$this->userInfo['user_id'];
+         $group_id = explode('-', $param['id'])[1];
+         $user_id=$param['user_id'];
+         $role=$param['role'];
+         if(!GroupUser::checkAuth(['group_id'=>$group_id,'user_id'=>$uid])){
+            return warning(lang('system.notAuth'));
+         }
+         $groupUser=GroupUser::where(['group_id'=>$group_id,'user_id'=>$user_id])->find();
+         if($groupUser){
+            $groupUser->role=$role;
+            $groupUser->save();
+            $avatar=GroupModel::where(['group_id'=>$group_id])->value('avatar');
+            $url=avatarUrl($avatar);
+            wsSendMsg($group_id,"setManager",['group_id'=>$param['id'],'user_id'=>$user_id,'avatar'=>$url],1);
+            return success(lang('system.settingOk'));
+         }else{
+            return warning('');
+         }
+         
+      }
+
+      // 添加群聊
+      public function add(){
+         $param = $this->request->param();
+         $uid=$this->userInfo['user_id'];
+         $user_ids=$param['user_ids'] ?? [];
+         if($this->chatSetting['groupChat']==0){
+            return warning(lang('system.notAuth'));
+         }
+         // 查看是否限制了群聊创建的个数
+         if($this->userInfo['group_limit']!=0 && $this->userInfo['role']==0){
+            $myGroup=GroupModel::where(['owner_id'=>$uid])->count();
+            // 群聊已达上限
+            if($myGroup>$this->userInfo['group_limit'] || $this->userInfo['group_limit']<0){
+               return warning(lang('group.limit'));
+            }
+         }
+         if(count($user_ids)>$this->chatSetting['groupUserMax'] && $this->chatSetting['groupUserMax']!=0){
+            return warning(lang('group.userLimit',['userMax'=>$this->chatSetting['groupUserMax']]));
+         }
+         // 管理员可以单独创建一个人的群
+         if(count($user_ids)<=1 && $this->userInfo['role']>=2){
+            return warning(lang('group.atLeast'));
+         }
+         // 将自己也加入群聊
+         $user_ids[]=$uid;
+         Db::startTrans();
+         $setting=$this->setting;
+         try{
+            $create=[
+               'create_user'=>$uid,
+               'owner_id'=>$uid,
+               'name'=>lang('group.name'),
+               'name_py'=>"qunliao",
+               'setting'=>json_encode($setting),
+            ];
+            $name=$param['name'] ?? '';
+            if($name){
+               $create['name']=$name;
+               $create['name_py']=pinyin_sentence($name);
+            }
+            $group=new GroupModel();
+            $group->save($create);
+            $group_id=$group->group_id;
+            $data=[];
+            array_unique($user_ids);
+            sort($user_ids);
+            foreach($user_ids as $k=>$v){
+               $info=[
+                  'user_id'=>$v,
+                  'invite_id'=>$uid,
+                  'status'=>1,
+                  'role'=>3,
+                  'group_id'=>$group_id
+               ];
+               if($v==$uid){
+                  $info['invite_id']=0;
+                  $info['role']=1;
+               }
+               $data[]=$info;
+            }
+            $groupUser=new GroupUser();
+            $groupUser->saveAll($data);
+            $groupInfo=[
+               'displayName'=>$create['name'],
+               'owner_id'=>$create['owner_id'],
+               'role'=>3,
+               'name_py'=>$create['name_py'],
+               'id'=>'group-'.$group_id,
+               'avatar'=>avatarUrl('',$create['name'],$group_id,120,1),
+               'is_group'=>1,
+               'lastContent'=>lang('group.add',['username'=>$this->userInfo['realname']]),
+               'lastSendTime'=>time()*1000,
+               'index'=>"[2]".lang('group.name'),
+               'is_notice'=>1,
+               'is_top'=>0,
+               'setting'=>$setting,
+            ];
+            Message::create([
+               'from_user'=>$uid,
+               'to_user'=>$group_id,
+               'content'=>str_encipher(lang('group.add',['username'=>''])),
+               'type'=>'event',
+               'is_group'=>1,
+               'is_read'=>1,
+               'is_last'=>1,
+               'chat_identify'=>'group-'.$group_id
+            ]);
+            wsSendMsg($user_ids, 'addGroup', $groupInfo);
+            Db::commit();
+            $groupInfo['role']=1;
+            queuePush(['action'=>'createAvatar','group_id'=>$group_id]);
+            return success('',$groupInfo);
+         }catch(Exception $e){
+            Db::rollback();
+            return error($e->getMessage());
+         }
+      }
+
+      // 移除成员
+      public function removeUser(){
+         $param = $this->request->param();
+         $uid=$this->userInfo['user_id'];
+         $group_id = explode('-', $param['id'])[1];
+         $groupInfo=GroupModel::where(['group_id'=>$group_id])->find();
+         if(!$groupInfo){
+            return warning(lang('group.exist'));
+         }
+         $user_id=$param['user_id'];
+         $role=GroupUser::where(['group_id'=>$group_id,'user_id'=>$uid])->value('role');
+         if($role>2 && $user_id!=$uid){
+            return warning(lang('system.notAuth'));
+         }
+         $groupUser=GroupUser::where(['group_id'=>$group_id,'user_id'=>$user_id])->find();
+         if(($groupUser && $groupUser['role']>$role) || $user_id==$uid){
+             $is_black=$param['is_black'] ?? 0;
+            // 如果是加入黑名单,则更新状态为3,禁止加入群聊
+            if($is_black==1){
+               GroupUser::where(['group_id'=>$group_id,'user_id'=>$user_id])->update(['status'=>3]);
+            }else{
+               GroupUser::destroy($groupUser->id);
+            }
+            Gateway::$registerAddress = config('gateway.registerAddress');
+            wsSendMsg($group_id,"removeUser",['group_id'=>$param['id'],'avatar'=>avatarUrl($groupInfo['avatar'],$groupInfo['name']),'user_id'=>$user_id],1);
+            $clientIds=Gateway::getClientIdByUid($user_id);
+            // 解绑群组
+            if($clientIds){
+               foreach($clientIds as $k=>$v){
+                  Gateway::leaveGroup($v, $group_id);
+               }
+            }
+            // 更新群聊头像
+            queuePush(['action'=>'createAvatar','group_id'=>$group_id]);
+         }else{
+            return warning(lang('system.notAuth'));
+         }
+         return success(lang('system.delOk'));
+      }
+
+      // 设置群成员禁言
+      public function setNoSpeak(){
+         $param = $this->request->param();
+         $uid=$this->userInfo['user_id'];
+         $group_id = explode('-', $param['id'])[1];
+         $user_id=$param['user_id'];
+         $role=GroupUser::where(['group_id'=>$group_id,'user_id'=>$uid])->value('role');
+         if($role>2 && $user_id!=$uid){
+            return warning(lang('system.notAuth'));
+         }
+         $groupUser=GroupUser::where(['group_id'=>$group_id,'user_id'=>$user_id])->find();
+         if(!$groupUser){
+            return warning(lang('system.notAuth'));
+         }
+         $noSpeakTimer=$param['noSpeakTimer'] ?? 0;
+         $noSpeakList=[600,3600,10800,86400];
+         $noSpeakDay=$param['noSpeakDay'] ?? 1;
+         $noSpeakTime=$noSpeakDay*86400;
+         if($noSpeakTimer>0){
+            $noSpeakTime=$noSpeakList[$noSpeakTimer-1];
+         }
+         wsSendMsg($group_id,"setNoSpeak",['group_id'=>$param['id'],'user_id'=>$user_id],1);
+         GroupUser::where(['group_id'=>$group_id,'user_id'=>$user_id])->update(['no_speak_time'=>time()+$noSpeakTime]);
+         return success(lang('system.success'));
+      }
+
+      // 解散团队
+      public function removeGroup(){
+         $param = $this->request->param();
+         $uid=$this->userInfo['user_id'];
+         $group_id = explode('-', $param['id'])[1];
+         $groupInfo=GroupModel::where(['group_id'=>$group_id])->find();
+         if(!$groupInfo){
+            return warning(lang('group.exist'));
+         }
+         $role=GroupUser::where(['group_id'=>$group_id,'user_id'=>$uid])->value('role');
+         if($role>1){
+            return warning(lang('system.notAuth'));
+         }
+         Db::startTrans();
+         try{
+            // 删除团队成员
+            GroupUser::where(['group_id'=>$group_id])->delete();
+            // 删除团队
+            GroupModel::destroy($group_id);
+            wsSendMsg($group_id,"removeGroup",['group_id'=>$param['id']],1);
+            Db::commit();
+            return success('');
+         }catch(Exception $e){
+            Db::rollback();
+            return error($e->getMessage());
+         }
+      }
+
+      // 设置公告
+      public function setNotice(){
+         $param = $this->request->param();
+         $uid=$this->userInfo['user_id'];
+         // 公告内容检测服务
+         event('GreenText',['content'=>$param['notice'],'service'=>"comment_detection"]);
+         $group_id = explode('-', $param['id'])[1];
+         if($param['notice']==''){
+            return warning(lang('system.notNull'));
+         }
+         $role=GroupUser::where(['group_id'=>$group_id,'user_id'=>$uid])->value('role');
+         if($role>2){
+            return warning(lang('system.notAuth'));
+         }
+         GroupModel::update(['notice'=>$param['notice']],['group_id'=>$group_id]);
+         $msg=[
+            'id'=>\utils\Str::getUuid(),
+            'user_id'=>$uid,
+            'content'=>'<b>'.lang('group.notice').':</b>&nbsp;@'.lang('group.all').'<br/>'.$param['notice'].'<br/>',
+            'toContactId'=>$param['id'],
+            'sendTime'=>time()*1000,
+            'type'=>'text',
+            'is_group'=>1,
+            'status'=>'succeed',
+            'fromUser'=>$this->userInfo,
+            'at'=>[0]
+         ];
+         $message=new Message();
+         $data = $message->sendMessage($msg,$this->globalConfig);
+         if (!$data) {
+             return warning($message->getError());
+         }
+         return success('');
+      }
+
+      // 群聊设置
+      public function groupSetting(){
+         $param = $this->request->param();
+         $uid=$this->userInfo['user_id'];
+         $group_id = explode('-', $param['id'])[1];
+         $role=GroupUser::where(['group_id'=>$group_id,'user_id'=>$uid])->value('role');
+         if($role!=1){
+            return warning(lang('system.notAuth'));
+         }
+         $setting=json_encode($param['setting']);
+         GroupModel::update(['setting'=>$setting],['group_id'=>$group_id]);
+         wsSendMsg($group_id,"groupSetting",['group_id'=>$param['id'],'setting'=>$param['setting']],1);
+         return success('');
+      }
+
+      // 加入群
+      public function joinGroup(){
+         $param = $this->request->param();
+         $uid=$this->userInfo['user_id'];
+         try{
+            $group_id = explode('-', $param['group_id'])[1];
+            $inviteUid=$param['inviteUid'] ?? '';
+            $groupUserCount=GroupUser::where(['group_id'=>$group_id,'status'=>1])->count();
+            $groupUser=GroupUser::where(['group_id'=>$group_id,'user_id'=>$uid])->find();
+            $groupInfo=GroupModel::where(['group_id'=>$group_id])->find();
+            if(!$groupInfo){
+               return warning(lang('group.exist'));
+            }
+             if($groupUser && $groupUser['status']==3){
+               return success('你已被禁止加入该群');
+            }
+            if($groupUser){
+               return warning(lang('group.alreadyJoin'));
+            }
+            
+            if(($groupUserCount+1) > $this->chatSetting['groupUserMax'] && $this->chatSetting['groupUserMax']!=0){
+               return warning(lang('group.userLimit',['userMax'=>$this->chatSetting['groupUserMax']]));
+            }
+            // 加入者的名称
+            $groupInfo['joinerName']=$this->userInfo['realname'];
+            GroupUser::joinGroup($uid,$inviteUid,$groupInfo);
+            return success(lang('system.joinOk'));
+         }catch(Exception $e){
+            return error($e->getMessage());
+         }
+      }
+
+   // 更换群主
+    public function changeOwner()
+    {
+        $user_id = $this->request->param('user_id');
+        $id = $this->request->param('id');
+        $group_id = explode('-', $id)[1];
+        $uid=$this->userInfo['user_id'];
+        $group=GroupModel::where('group_id',$group_id)->find();
+        if(!$group){
+            return warning(lang('group.exist'));
+        }
+        $user=User::where('user_id',$user_id)->find();
+        if(!$user){
+            return warning(lang('user.exist'));
+        }
+        $role=GroupUser::where(['group_id'=>$group_id,'user_id'=>$uid])->value('role');
+        if($role>1){
+           return warning(lang('system.notAuth'));
+        }
+        Db::startTrans();
+        try{
+            GroupUser::where('group_id',$group_id)->where('user_id',$user_id)->update(['role'=>1]);
+            GroupUser::where('group_id',$group_id)->where('user_id',$group->owner_id)->update(['role'=>3]);
+            $group->owner_id=$user_id;
+            $group->save();
+            wsSendMsg($group_id,"changeOwner",['group_id'=>'group-'.$group_id,'user_id'=>$user_id],1);
+            Db::commit();
+            return success('');
+        }catch (\Exception $e){
+            Db::rollback();
+            return warning('');
+        }
+    }
+
+      // 清理群消息
+      public function clearMessage()
+      {
+         $id = $this->request->param('id');
+         $group_id = explode('-', $id)[1];
+         $uid=$this->userInfo['user_id'];
+         $group=GroupModel::where('group_id',$group_id)->find();
+         if(!$group){
+            return warning(lang('group.exist'));
+         }
+         $role=GroupUser::where(['group_id'=>$group_id,'user_id'=>$uid])->value('role');
+         // 如果是群主或者后台管理员才有权限
+         if($role>1 && $this->userInfo['role']==0){
+            return warning(lang('system.notAuth'));
+         }
+         Db::startTrans();
+         try{
+            // 删除所有消息
+            Message::where(['chat_identify'=>$id])->delete();
+            // 该群聊的所有未读置为0
+            GroupUser::where('group_id',$group_id)->update(['unread'=>0]);
+            wsSendMsg($group_id,"clearMessage",['group_id'=>'group-'.$group_id],1);
+            Db::commit();
+            return success('');
+         }catch (\Exception $e){
+            Db::rollback();
+            return warning('');
+         }
+      }
+}

+ 985 - 0
app/enterprise/controller/Im.php

@@ -0,0 +1,985 @@
+<?php
+
+namespace app\enterprise\controller;
+
+use app\BaseController;
+use think\facade\Session;
+use think\facade\Db;
+use app\enterprise\model\{User, Message, GroupUser, Friend,Group,ChatDelog};
+use GatewayClient\Gateway;
+use Exception;
+use League\Flysystem\Util;
+use think\facade\Cache;
+
+class Im extends BaseController
+{
+    protected $fileType = ['file', 'image','video','voice','emoji'];
+    // 获取联系人列表
+    public function getContacts()
+    {
+        $data = User::getUserList([['status', '=', 1], ['user_id', '<>', $this->userInfo['user_id']]], $this->userInfo['user_id']);
+        $count=Friend::where(['status'=>2,'friend_user_id'=>$this->uid])->count();
+        $time=Friend::where(['friend_user_id'=>$this->uid,'is_invite'=>1])->order('create_time desc')->value('create_time');
+        return success('', $data,$count,$time*1000);
+    }
+
+    // 获取聊天列表
+    public function getChatList()
+    {
+        $data = User::getChatList($this->userInfo['user_id']);
+        $count=Friend::where(['status'=>2,'friend_user_id'=>$this->uid])->count();
+        $time=Friend::where(['friend_user_id'=>$this->uid,'is_invite'=>1])->order('create_time desc')->value('create_time');
+        return success('', $data,$count,$time*1000);
+    }
+
+    // 获取好友列表
+    public function getFriendList()
+    {
+        $data = User::getFriendList([['status', '=', 1], ['user_id', '<>', $this->userInfo['user_id']]],$this->userInfo['user_id']);
+        return success('', $data);
+    }
+
+    // 获取群聊列表
+    public function getGroupList()
+    {
+        $data = User::getGroupList($this->userInfo['user_id']);
+        return success('', $data);
+    }
+
+    // 获取单个人员的信息
+    public function getContactInfo(){
+        $id = $this->request->param('id');
+        $is_group = is_string($id) ? 1 : 0;
+        $user=new User;
+        $data=$user->setContact($id,$is_group);
+        if(!$data){
+            return warning($user->getError());
+        }
+        return success('',$data);
+    }
+
+
+    //发送消息
+    public function sendMessage()
+    {
+        $param = $this->request->param();
+        $param['user_id'] = $this->userInfo['user_id'];
+        $message=new Message();
+        $data = $message->sendMessage($param,$this->globalConfig);
+        if ($data) {
+            return success('', $data);
+        } else {
+            return warning($message->getError());
+        }
+    }
+
+    //转发消息
+    public function forwardMessage()
+    {
+        $param = $this->request->param();
+        $userIds=$param['user_ids'] ?? [];
+        if(!$userIds || count($userIds)>5){
+            return warning(lang('im.forwardLimit',['count'=>5]));
+        }
+        $msg_id=$param['msg_id'] ?? 0;
+        $message=Message::find($msg_id);
+        if(!$message){
+            return warning(lang('im.exist'));
+        }
+        $msg=$message->toArray();
+        $userInfo=$this->userInfo;
+        queuePush([
+            'action'=>'forwardMessage',
+            'message'=>$msg,
+            'user_ids'=>$userIds,
+            'config'=>$this->globalConfig,
+            'userInfo'=>$userInfo
+        ]);
+        return success(lang('im.forwardOk'));
+    }
+
+        // 获取用户信息
+    public function getUserInfo()
+    {
+        $user_id = $this->request->param('user_id');
+        $group_id = $this->request->param('group_id');
+        $groupInfo=[];
+        if($group_id){
+            $group_id = explode('-', $group_id)[1];
+            $groupInfo=Group::where(['group_id'=>$group_id])->find();
+            if($groupInfo){
+                // 查询操作对象的角色
+                $groupInfo['userInfo']=GroupUser::where(['user_id'=>$user_id,'group_id'=>$group_id])->find() ?: [];
+                // 查询我的角色
+                $groupInfo['manageRole']=GroupUser::where(['user_id'=>$this->userInfo['user_id'],'group_id'=>$group_id])->value('role') ?: 3;
+            }
+        }
+        $user=User::find($user_id);
+        if(!$user){
+            return error(lang('user.exist'));
+        }
+        $user->avatar=avatarUrl($user->avatar,$user->realname,$user->user_id,120);
+        // 账号前面截取3位,后面截取两位,中间星号展示
+        $user->account=substr($user->account, 0, 3).'******'.substr($user->account, -2, 2);
+        // 查询好友关系
+        $friend=Friend::where(['friend_user_id'=>$user_id,'create_user'=>$this->userInfo['user_id'],'status'=>1])->find();
+        $user->friend=$friend ? : '';
+        $location='';
+        if($user->last_login_ip){
+            $location=implode(" ", \Ip::find($user->last_login_ip));
+        }
+        $user->location=$location;
+        $user->groupInfo=$groupInfo ? : [];
+        $user->password='';
+        $userModel=new User;
+        $user->contactInfo=$userModel->setContact($user_id,0) ?? [];
+        return success('', $user);
+    }
+
+    // 扫码登录,向客户端推送数据
+    public function tokenLogin(){
+        $param = $this->request->param();
+        $token=$param['token'] ?? '';
+        $client_id='';
+        if($token){
+            $client_id=authcode(urldecode($token),"DECODE", config('app.app_id'));
+            if(!$client_id){
+                 return warning(lang('user.loginError'));
+            }
+        }else{
+            return warning(lang('user.loginError'));
+        }
+        $userInfo=$this->userInfo;
+        if(!$userInfo){
+            return warning(lang('user.loginError'));
+        }
+        //    如果用户已经有设置
+        $setting=$userInfo['setting'] ?: '';
+        if($setting){
+            $setting['hideMessageName']= $setting['hideMessageName']=='true' ? true : false;
+            $setting['hideMessageTime']= $setting['hideMessageTime']=='true' ? true : false;
+            $setting['avatarCricle']= $setting['avatarCricle']=='true' ? true : false;
+            $setting['isVoice']= $setting['isVoice']=='true' ? true : false;
+            $setting['sendKey']=(int)$setting['sendKey'];
+        }
+        $userInfo['setting']=$setting;
+         Gateway::$registerAddress = config('gateway.registerAddress');
+        //如果登录信息中含有client——id则自动进行绑定
+        if($client_id){
+            $user_id=$userInfo['user_id'];
+           
+            // 如果当前ID在线,将其他地方登陆挤兑下线
+            if(Gateway::isUidOnline($user_id)){
+                wsSendMsg($user_id,'offline',['id'=>$user_id,'client_id'=>$client_id,'isMobile'=>false]);
+            }
+            Gateway::bindUid($client_id, $user_id);
+            // 查询团队,如果有团队则加入团队
+            $group=Group::getMyGroup(['gu.user_id'=>$user_id,'gu.status'=>1]);
+            if($group){
+                $group=$group->toArray();
+                $group_ids=arrayToString($group,'group_id',false);
+                foreach($group_ids as $v){
+                    Gateway::joinGroup($client_id, $v); 
+                }
+            }
+        }
+        $update=[
+            'last_login_time'=>time(),
+            'last_login_ip'=>$this->request->ip(),
+            'login_count'=>Db::raw('login_count+1')
+        ];
+        User::where('user_id',$userInfo['user_id'])->update($update);
+        $authToken=User::refreshToken($userInfo,'web');
+        $data=[
+            'sessionId'=>Session::getId(),
+            'authToken'=>$authToken,
+            'userInfo'=>$userInfo
+        ];
+        Gateway::sendToClient($client_id, json_encode(array(
+            'type' => 'tokenLogin',
+            'data' => $data,
+        )));
+        return success(lang('user.loginOk'),$data);
+    }
+
+    // 搜索用户
+    public function searchUser(){
+        $keywords=$this->request->param('keywords','');
+        if(!$keywords){
+            return success('',[]);
+        }
+        $map=['status'=>1,'account'=>$keywords];
+        $list=User::where($map)->field(User::$defaultField)->where([['account','<>',$this->userInfo['account']]])->select()->toArray();
+        if($list){
+            $ids=array_column($list,'user_id');
+            $friendList=Friend::getFriend([['create_user','=',$this->uid],['friend_user_id','in',$ids]]);
+            foreach($list as $k=>$v){
+                $list[$k]['avatar']=avatarUrl($v['avatar'],$v['realname'],$v['user_id'],120);
+                $list[$k]['friend']=$friendList[$v['user_id']] ?? '';
+            }
+        }
+        return success('', $list);
+    }
+
+    // 获取系统所有人员加搜索
+    public function userList(){
+        $keywords=$this->request->param('keywords','');
+        $listRows=$this->request->param('limit',20);
+        $page=$this->request->param('page',1);
+        $map=['status'=>1];
+        $field="user_id,realname,avatar";
+        if(!$keywords){
+            $list=User::where($map)->field($field)->order('user_id asc')->limit(20)->paginate(['list_rows'=>$listRows,'page'=>$page]);;
+            if($list){
+                $list=$list->toArray()['data'];
+            }
+        }else{
+            $list=User::where($map)->field($field)->where([['account','<>',$this->userInfo['account']]])->whereLike('account|realname|name_py','%'.$keywords.'%')->select()->toArray();
+        }
+        if($list){
+            foreach($list as $k=>$v){
+                $list[$k]['avatar']=avatarUrl($v['avatar'],$v['realname'],$v['user_id'],120);
+                $list[$k]['id']=$v['user_id'];
+            }
+        }
+        return success('', $list);
+    }
+
+    // 获取聊天记录
+    public function getMessageList()
+    {
+        $param = $this->request->param();
+        $is_group = isset($param['is_group']) ? $param['is_group'] : 0;
+        // 如果toContactId是数字,绝对是单聊
+        $is_group = is_numeric($param['toContactId']) ? 0 : $is_group;
+        // 设置当前聊天消息为已读
+        $chat_identify = $this->setIsRead($is_group, $param['toContactId']);
+        $type = isset($param['type']) ? $param['type'] : '';
+        $is_at = isset($param['is_at']) ? $param['is_at'] : '';
+        $map = ['chat_identify' => $chat_identify, 'status' => 1];
+        $where = [];
+        if ($type && $type != "all") {
+            $map['type'] = $type;
+        } else {
+            if (isset($param['type'])) {
+                $where[] = ['type', '<>', 'event'];
+            }
+        }
+        $groupManage=[];
+        // 群聊查询入群时间以后的消息
+        if($is_group==1){
+            $group_id = explode('-', $param['toContactId'])[1];
+            $group=Group::where(['group_id'=> $group_id])->find();
+            $groupManage=GroupUser::getGroupManage($group_id);
+            if($group && $group['setting']){
+                $groupSetting=json_decode($group['setting'],true);
+                $history=$groupSetting['history'] ?? false;
+                // 如果开启了历史记录才可以查看所有记录,否者根据进群时间查询记录
+                if(!$history){
+                    $createTime=GroupUser::where(['group_id'=> $group_id,'user_id'=>$this->userInfo['user_id']])->value('create_time');
+                    $where[] = ['create_time', '>=', $createTime ? : 0];
+                }
+            }
+        }
+        $keywords = isset($param['keywords']) ? $param['keywords'] : '';
+        if ($keywords && in_array($type, ['text', 'all'])) {
+            $where[] = ['content', 'like', '%' . $keywords . '%'];
+        }
+        // 如果是查询@数据
+        if($is_at){
+            $atList=Db::name('message')->where($map)->where($where)->whereFindInSet('at',$this->userInfo['user_id'])->order('msg_id desc')->select()->toArray();
+            if($atList){
+                $data = $this->recombileMsg($atList,false);
+                Message::setAtread($data,$this->userInfo['user_id']);
+                return success('', $data, count($data));
+            }else{
+                return success('', [], 0);
+            }
+        }
+        $listRows = $param['limit'] ?: 20;
+        $pageSize = $param['page'] ?: 1;
+        $last_id = $param['last_id'] ?? 0;
+        if($last_id){
+            $where[]=['msg_id','<',$last_id];
+            $pageSize=1;
+        }
+        $list = Message::getList($map, $where, 'msg_id desc', $listRows, $pageSize);
+        $data = $this->recombileMsg($list,true,$groupManage);
+        // 如果是群聊并且是第一页消息,需要推送@数据给用户
+        if($param['is_group']==1 && $param['page']==1){
+            $isPush=Cache::get('atMsgPush'.$chat_identify) ?? '';
+            $atList=Db::name('message')->where(['chat_identify'=>$chat_identify,'is_group'=>1])->whereFindInSet('at',$this->userInfo['user_id'])->order('msg_id desc')->select()->toArray();
+            $msgIda=array_column($atList,'msg_id');
+            // 如果两次推送at数据的列表不一样,则推送
+            if($isPush!=json_encode($msgIda)){
+                $atData=$this->recombileMsg($atList,false);
+                wsSendMsg($this->userInfo['user_id'],'atMsgList',[
+                    'list'=>$atData,
+                    'count'=>count($atData),
+                    'toContactId'=>$param['toContactId']
+                ]);
+                Cache::set('atMsgPush'.$chat_identify,json_encode($msgIda),60);
+            }
+        }
+        // 如果是消息管理器则不用倒序
+        if (!isset($param['type'])) {
+            $data = array_reverse($data);
+        }
+        return success('', $data, $list->total());
+    }
+
+    // 获取单条消息详情
+    public function getMessageInfo()
+    {
+        $param = $this->request->param();
+        $id = $param['msg_id'] ?? 0;
+        $message = Message::where(['msg_id' => $id])->find();
+        if ($message) {
+            $data = $this->recombileMsg([$message], false);
+            return success('', $data);
+        } else {
+            return warning(lang('im.exist'));
+        }
+    }
+
+
+    // 获取单条消息上下文
+    public function getMessageContext()
+    {
+        $param = $this->request->param();
+        $is_group = isset($param['is_group']) ? $param['is_group'] : 0;
+        $id = $param['msg_id'] ?? 0;
+        $direction = $param['direction'] ?? 0;
+        $message = Message::where(['msg_id' => $id])->find();
+        if (!$message) {
+            return warning(lang('im.exist'));
+        }
+        $groupManage=[];
+        $where = [];
+        $map = ['chat_identify' => $message['chat_identify'], 'status' => 1];
+        if($is_group==1 && $direction<2){
+            $group_id = $message['to_user'];
+            $group=Group::where(['group_id'=> $group_id])->find();
+            $groupManage=GroupUser::getGroupManage($group_id);
+            if($group && $group['setting']){
+                $groupSetting=json_decode($group['setting'],true);
+                $history=$groupSetting['history'] ?? false;
+                // 如果开启了历史记录才可以查看所有记录,否者根据进群时间查询记录
+                if(!$history){
+                    $createTime=GroupUser::where(['group_id'=> $group_id,'user_id'=>$this->userInfo['user_id']])->value('create_time');
+                    $where[] = ['create_time', '>=', $createTime ? : 0];
+                }
+            }
+        }
+        if($direction==0){
+            $where[] = ['msg_id', '<', $id];
+            $beforeList = Message::where($map)->where($where)->order('msg_id desc')->limit(5)->select()->toArray();
+            $beforeList = array_reverse($beforeList);
+            $where2 = [];
+            $where2[] = ['msg_id', '>=', $id];
+            $afterList = Message::where($map)->where($where2)->order('msg_id asc')->limit(5)->select()->toArray();
+            $data = array_merge($beforeList, $afterList);
+        }elseif($direction==1){
+            $where[] = ['msg_id', '<', $id];
+            $data = Message::where($map)->where($where)->order('msg_id desc')->limit(5)->select()->toArray();
+            $data = array_reverse($data);
+        }else{
+            $where[] = ['msg_id', '>', $id];
+            $data = Message::where($map)->where($where)->order('msg_id asc')->limit(5)->select()->toArray();
+        }
+        if($data){
+            $data = $this->recombileMsg($data, false,$groupManage);  
+        }
+        return success('', $data);
+    }
+
+    protected function recombileMsg($list,$isPagination=true,$manage=[])
+    {
+        $data = [];
+        $userInfo = $this->userInfo;
+        if ($list) {
+            $listData = $isPagination ? $list->toArray()['data'] : $list;
+            $userList = User::matchUser($listData, true, 'from_user', 120);
+            
+            foreach ($listData as $k => $v) {
+                // 屏蔽已删除的消息
+                if ($v['del_user']) {
+                    $delUser = explode(',', $v['del_user']);
+                    if (in_array($userInfo['user_id'], $delUser)) {
+                        unset($list[$k]);
+                        continue;
+                        // $v['type']="event";
+                        // $v['content']="删除了一条消息";
+                    }
+                }
+                $content = str_encipher($v['content'],false);
+                $preview = '';
+                $ext='';
+                if (in_array($v['type'], $this->fileType)) {
+                    $content = getFileUrl($content);
+                    $preview = previewUrl($content);
+                    $ext=getExtUrl($content);
+                }
+                
+                $fromUser = $userList[$v['from_user']];
+                // 处理撤回的消息
+                if ($v['type'] == "event" && $v['is_undo']==1) {
+                    if ($v['from_user'] == $userInfo['user_id']) {
+                        $content = lang('im.you'). $content;
+                    } elseif ($v['is_group'] == 1) {
+                        $content = $fromUser['realname'] . $content;
+                    } else {
+                        $content = lang('im.other') . $content;
+                    }
+                }
+                $toContactId=$v['is_group'] ==1 ?  'group-'.$v['to_user'] : $v['to_user'];
+                $atList=($v['at'] ?? null) ? explode(',',$v['at']): [];
+                $role=$manage[$v['from_user']] ?? 3;
+                $data[] = [
+                    'msg_id' => $v['msg_id'],
+                    'id' => $v['id'],
+                    'status' => "succeed",
+                    'type' => $v['type'],
+                    'sendTime' => (is_string($v['create_time']) ? strtotime($v['create_time']) : $v['create_time'])* 1000,
+                    'content' => $content,
+                    'preview' => $preview,
+                    'download' => $v['file_id'] ? getMainHost().'/filedown/'.encryptIds($v['file_id']) : '',
+                    'is_read' => $v['is_read'],
+                    'is_group' => $v['is_group'],
+                    'at' => $atList,
+                    'toContactId' => $toContactId,
+                    'from_user' => $v['from_user'],
+                    'file_id' => $v['file_id'],
+                    'file_cate' => $v['file_cate'],
+                    'fileName' => $v['file_name'],
+                    'fileSize' => $v['file_size'],
+                    'fromUser' => $fromUser,
+                    'extUrl'=>$ext,
+                    'role'=>$role,
+                    'extends'=>is_string($v['extends'])?json_decode($v['extends'],true) : $v['extends']
+                ];
+            }
+        }
+        return $data;
+    }
+
+    // 设置当前窗口的消息默认为已读
+    public function setMsgIsRead()
+    {
+        $param = $this->request->param();
+        
+        // 判断是否是一个二维数组
+        if (is_array($param['messages'][0] ?? '')) {
+           $messages=$param['messages'];
+        } else {
+            $messages=[$param['messages']];
+        }
+        $this->setIsRead($param['is_group'], $param['toContactId'],$messages);
+        if (!$param['is_group']) {
+            wsSendMsg($param['fromUser'], 'isRead', $messages, 0);
+        }
+        return success('');
+    }
+
+    // 设置全部为已读
+    public function readAllMsg()
+    {
+        // 阅读所有单聊
+        $map = ['to_user' => $this->userInfo['user_id'], 'is_read' => 0, 'is_group' => 0];
+        Message::where($map)->update(['is_read' => 1]);
+        // 阅读所有群聊
+        GroupUser::where(['user_id' => $this->userInfo['user_id'], 'status' => 1])->update(['unread'=>0]);
+        return success('');
+    }
+
+    // 设置消息已读
+    protected function setIsRead($is_group, $to_user,$messages=[])
+    {
+        if ($is_group==1) {
+            $chat_identify = $to_user;
+        } else if($is_group==0) {
+            $chat_identify = chat_identify($this->userInfo['user_id'], $to_user);
+        } else if($is_group==2){
+            $chat_identify = $to_user; 
+        }
+        $data=[
+            'action'=>'setIsRead',
+            'is_group'=>$is_group,
+            'to_user'=>$to_user,
+            'messages'=>$messages,
+            'user_id'=>$this->userInfo['user_id']
+        ];
+        queuePush($data,3);
+        return $chat_identify;
+    }
+
+    // 聊天设置
+    public function setting()
+    {
+        $param = $this->request->param();
+        if ($param) {
+            User::where(['user_id' => $this->userInfo['user_id']])->update(['setting' => $param]);
+            return success('');
+        }
+        return warning('');
+    }
+
+    // 撤回消息
+    public function undoMessage()
+    {
+        $param = $this->request->param();
+        $id = $param['id'];
+        $message = Message::where(['id' => $id])->find();
+        if ($message) {
+            // 如果时间超过了2分钟也不能撤回
+            $createTime=is_string($message['create_time']) ? strtotime($message['create_time']) : $message['create_time'];
+            $redoTime=$this->globalConfig['chatInfo']['redoTime'] ?? 120;
+            if(time()-$createTime>$redoTime && $message['is_group']==0){
+                return warning(lang('im.redoLimitTime',['time'=>floor($redoTime/60)]));
+            }
+            $text = lang('im.redo');
+            $fromUserName = lang('im.other');
+            $toContactId = $message['to_user'];
+            if ($message['is_group'] == 1) {
+                $fromUserName = $this->userInfo['realname'];
+                $toContactId = explode('-', $message['chat_identify'])[1];
+                // 如果是群聊消息撤回,需要判断是否是群主或者管理员,如果是则可以撤回
+                if($message['from_user']!=$this->userInfo['user_id']){
+                    $groupUser=GroupUser::where(['user_id'=>$this->userInfo['user_id'],'group_id'=>$toContactId])->find();
+                    if(!$groupUser || !in_array($groupUser['role'],[1,2])){
+                        return warning(lang('system.notAuth'));
+                    }
+                    $text=lang('im.manageRedo');
+                }
+            }
+            $message->content = str_encipher($text);
+            $message->type = 'event';
+            $message->is_undo = 1;
+            //@的数据清空
+            $message->at = ''; 
+            $message->save();
+            $info = $message->toArray();
+            // $data = $info;
+            $data['content'] = $fromUserName . $text;
+            $data['sendTime'] = $createTime * 1000;
+            $data['id'] = $info['id'];
+            $data['from_user'] = $info['from_user'];
+            $data['msg_id'] = $info['msg_id'];
+            $data['status'] = $info['status'];
+            $data['type'] = 'event';
+            $data['is_last'] = $info['is_last'];
+            $data['toContactId'] = $message['is_group'] == 1 ? $info['chat_identify'] : $toContactId;
+            $data['isMobile'] = $this->request->isMobile() ? 1 : 0;
+            wsSendMsg($toContactId, 'undoMessage', $data, $info['is_group']); 
+            if($info['is_group']==0){
+               // 给自己也发一份推送,多端同步
+                $data['content'] =lang('im.you'). $text;
+                wsSendMsg($this->userInfo['user_id'], 'undoMessage', $data, $info['is_group']); 
+            }
+            return success('');
+        } else {
+            return warning();
+        }
+    }
+
+    // 删除消息
+    public function removeMessage()
+    {
+        $param = $this->request->param();
+        $id = $param['id'];
+        $map = ['id' => $id];
+        $message = Message::where($map)->find();
+        if ($message) {
+            $message->del_user = $this->userInfo['user_id'];
+            if ($message['is_group'] == 1) {
+                if ($message['del_user']) {
+                    $message->del_user .= ',' . $this->userInfo['user_id'];
+                }
+            } else {
+                if ($message['del_user'] > 0) {
+                    $message->where($map)->delete();
+                    return success(lang('system.delOk'));
+                }
+            }
+            $message->save();
+            return success('');
+        } else {
+            return warning('');
+        }
+    }
+
+    // 消息免打扰
+    public function isNotice()
+    {
+        $param = $this->request->param();
+        $user_id = $this->userInfo['user_id'];
+        $id = $param['id'];
+        if ($param['is_group'] == 1) {
+            $group_id = explode('-', $param['id'])[1];
+            GroupUser::update(['is_notice' => $param['is_notice']], ['user_id' => $user_id, 'group_id' => $group_id]);
+        } else {
+            $map = ['create_user' => $user_id, 'friend_user_id' => $id];
+            $friend = Friend::where($map)->find();
+            try {
+                if ($friend) {
+                    $friend->is_notice = $param['is_notice'];
+                    $friend->save();
+                } else {
+                    $info = [
+                        'create_user' => $user_id,
+                        'friend_user_id' => $id,
+                        'is_notice' => $param['is_notice']
+                    ];
+                    Friend::create($info);
+                }
+                return success('');
+            } catch (Exception $e) {
+                return error($e->getMessage());
+            }
+        }
+        wsSendMsg($user_id,"setIsNotice",['id'=>$id,'is_notice'=>$param['is_notice'],'is_group'=>$param['is_group']]);
+        return success('');
+    }
+
+    // 设置聊天置顶
+    public function setChatTop()
+    {
+        $param = $this->request->param();
+        $user_id = $this->userInfo['user_id'];
+        $is_group = $param['is_group'] ?: 0;
+        $id = $param['id'];
+        
+        try {
+            if ($is_group == 1) {
+                $group_id = explode('-', $param['id'])[1];
+                GroupUser::update(['is_top' => $param['is_top']], ['user_id' => $user_id, 'group_id' => $group_id]);
+            } else {
+                $map = ['create_user' => $user_id, 'friend_user_id' => $id];
+                $friend = Friend::where($map)->find();
+                if ($friend) {
+                    $friend->is_top = $param['is_top'];
+                    $friend->save();
+                } else {
+                    $info = [
+                        'create_user' => $user_id,
+                        'friend_user_id' => $id,
+                        'is_top' => $param['is_top']
+                    ];
+                    Friend::create($info);
+                }
+            }
+            wsSendMsg($user_id,"setChatTop",['id'=>$id,'is_top'=>$param['is_top'],'is_group'=>$is_group]);
+            return success('');
+        } catch (Exception $e) {
+            return error($e->getMessage());
+        }
+    }
+    
+    // 删除聊天
+    public function delChat()
+    {
+        $param = $this->request->param();
+        $user_id = $this->userInfo['user_id'];
+        $is_group = $param['is_group'] ?: 0;
+        $id = $param['id'];
+        $data=[
+            'user_id'=>$user_id,
+            'is_group'=>$is_group,
+            'to_user'=>$id
+        ];
+        ChatDelog::create($data);
+        ChatDelog::updateCache($user_id);
+        return success('');
+    }
+
+    // 向用户发送消息
+    public function sendToMsg(){
+        $param=$this->request->param();
+        $toContactId=$param['toContactId'];
+        
+        $type=$param['type'];
+        $status=$param['status'];
+        $event=$param['event'] ?? 'calling';
+        if($event=='calling'){
+            $status=3;
+        }
+        $sdp=$param['sdp'] ?? '';
+        $iceCandidate=$param['iceCandidate'] ?? '';
+        $callTime=$param['callTime'] ?? '';
+        $msg_id=$param['msg_id'] ?? '';
+        $id=$param['id'] ?? '';
+        $code=($param['code'] ?? '') ?: 901;
+        // 如果该用户不在线,则发送忙线
+        Gateway::$registerAddress = config('gateway.registerAddress');
+        if(!Gateway::isUidOnline($toContactId)){
+            $toContactId=$this->userInfo['user_id'];
+            $code=907;
+            $event='busy';
+            sleep(1);
+        }
+        switch($code){
+            case 902:
+                $content=lang('webRtc.cancel');
+                break;
+            case 903:
+                $content=lang('webRtc.refuse');
+                break;
+            case 905:
+                $content=lang('webRtc.notConnected');
+                break;
+            case 906:
+                $content=lang('webRtc.duration',['time'=>date("i:s",$callTime)]);
+                break;
+            case 907:
+                $content=lang('webRtc.busy');
+                break;
+            case 908:
+                $content=lang('webRtc.other');
+                break;
+            default:
+                $content=$type==1 ?lang('webRtc.video') : lang('webRtc.audio');
+                break;
+        }
+        switch($event){
+            case 'calling':
+                $content=$type==1 ?lang('webRtc.video'): lang('webRtc.audio');
+                break;
+            case 'acceptRtc':
+                $content=lang('webRtc.answer');
+                break;
+            case 'iceCandidate':
+                $content=lang('webRtc.exchange');
+                break;
+        }
+        $userInfo=$this->userInfo;
+        $userInfo['id']=$userInfo['user_id'];
+        $user = new User();
+        $data=[
+            'id'=>$id,
+            'msg_id'=>$msg_id,
+            'sendTime'=>time()*1000,
+            'toContactId'=>$toContactId,
+            'content'=>$content,
+            'type'=>'webrtc',
+            'status'=>'succeed',
+            'is_group'=>0,
+            'is_read'=>0,
+            'fromUser'=>$userInfo,
+            'at'=>[],
+            'extends'=>[
+                'type'=>$type,    //通话类型,1视频,0语音。
+                'status'=>$status, //,1拨打方,2接听方
+                'event'=>$event,
+                'callTime'=>$callTime,
+                'sdp'=>$sdp,
+                'code'=>$code,  //通话状态:呼叫901,取消902,拒绝903,接听904,未接通905,接通后挂断906,忙线907,其他端操作908
+                'iceCandidate'=>$iceCandidate,
+                'isMobile'=>$this->request->isMobile() ? 1 : 0,
+            ]
+        ];
+        if($event=='calling'){
+            $chat_identify=chat_identify($userInfo['id'],$toContactId);
+            $msg=[
+                'from_user'=>$userInfo['id'],
+                'to_user'=>$toContactId,
+                'id'=>$id,
+                'content'=>str_encipher($content),
+                'chat_identify'=>$chat_identify,
+                'create_time'=>time(),
+                'type'=>$data['type'],
+                'is_group'=>0,
+                'is_read'=>0,
+                'extends'=>$data['extends'],
+            ];
+            $message=new Message();
+            $message->update(['is_last'=>0],['chat_identify'=>$chat_identify]);
+            $message->save($msg);
+            $msg_id=$message->msg_id;
+            $data['msg_id']=$msg_id;
+            // 将接收人设置为发送人才能定位到该消息
+            $data['toContactId']=$userInfo['id'];
+            $data['toUser']=$toContactId;
+        }elseif($event=='hangup'){
+            $message=Message::where(['id'=>$id])->find();
+            if(!$message){
+                return error(lang('webRtc.fail'));
+            }
+            if($message){
+                $message->content=str_encipher($content);
+                $extends=$message->extends;
+                $extends['code']=$code;
+                $extends['callTime']=$callTime;
+                $message->extends=$extends;
+                $message->save();
+            }
+        }
+        wsSendMsg($toContactId,'webrtc',$data);
+        $wsData=$data;
+        if(in_array($event,['calling','acceptRtc','hangup'])){
+            if(in_array($event,['acceptRtc','hangup'])){
+                $data['extends']['event']='otherOpt'; //其他端操作
+            }
+            $data['toContactId']=$toContactId;
+            $data['contactInfo']=$user->setContact($toContactId,0,'webrtc',$content) ? : [];
+            wsSendMsg($userInfo['id'],'webrtc',$data);
+        }
+        return success('',$wsData);
+    }
+
+    // 修改密码
+    public function editPassword()
+    {
+        if(env('app.demon_mode',false)){
+            return warning(lang('system.demoMode'));
+        }
+        
+        $user_id = $this->userInfo['user_id'];
+        $user=User::find($user_id);
+        if(!$user){
+            return warning(lang('user.exist'));
+        }
+        $account=$user->account;
+        $code=$this->request->param('code','');
+        $originalPassword = $this->request->param('originalPassword', '');
+        if($code){
+            if(Cache::get($account)!=$code){
+                return warning(lang('user.codeErr'));
+            }
+        }elseif($originalPassword){
+            if(password_hash_tp($originalPassword,$user->salt)!= $user->password){
+                return warning(lang('user.passErr'));
+            }
+        }else{
+            return warning(lang('system.parameterError'));
+        }
+        try{
+            $password = $this->request->param('password','');
+            if($password){
+                $salt=$user->salt;
+                $user->password= password_hash_tp($password,$salt);
+            }
+            $user->save();
+            return success(lang('system.editOk'));
+        }catch (\Exception $e){
+            return error(lang('system.editFail'));
+        }
+    }
+
+    // 修改用户信息
+    public function updateUserInfo(){
+        try{
+            $data = $this->request->param();
+            $user=User::find($this->uid);
+            if(!$user){
+                return warning(lang('user.exist'));
+            }
+            // 接入用户名检测服务
+            event('GreenText',['content'=>$data['realname'],'service'=>"nickname_detection"]);
+            // 个性签名检测服务
+            event('GreenText',['content'=>$data['motto'],'service'=>"comment_detection"]);
+            $user->realname =$data['realname'];
+            $user->email =$data['email'];
+            $user->motto=$data['motto'];
+            $user->sex =$data['sex'];
+            $user->name_py= pinyin_sentence($data['realname']);
+            $user->save();
+            return success(lang('system.editOk'), $data);
+        }catch (\Exception $e){
+            return error($e->getMessage());
+        }
+    }
+
+    // 修改账户
+    public function editAccount(){
+        if(env('app.demon_mode',false)){
+            return warning(lang('system.demoMode'));
+        }
+        $code=$this->request->param('code','');
+        $newCode=$this->request->param('newCode','');
+        $account=$this->request->param('account','');
+        $isUser=User::where('account',$account)->find();
+        if($isUser){
+            return warning(lang('user.already'));
+        }
+        $user=User::find($this->uid);
+        if(!$user){
+            return warning(lang('user.exist'));
+        }
+        // 如果已经认证过了,则需要验证验证码
+        if($user->is_auth){
+            if(Cache::get($user->account)!=$code){
+                return warning(lang('user.codeErr'));
+            }
+        }
+        if(Cache::get($account)!=$newCode){
+            return warning(lang('user.newCodeErr'));
+        }
+        try{
+            $user->account=$account;
+            $user->is_auth=1;
+            $user->save();
+            return success(lang('system.editOk'));
+        }catch (\Exception $e){
+            return error(lang('system.editFail'));
+        }
+    }
+
+    // 阅读@消息
+    public function readAtMsg(){
+        $param = $this->request->param();
+        $atList=Db::name('message')->where(['chat_identify'=>$param['toContactId'],'is_group'=>1])->whereFindInSet('at',$this->userInfo['user_id'])->order('msg_id desc')->select();
+        $atData=$this->recombileMsg($atList,false);
+        Message::setAtRead($atData,$this->userInfo['user_id']);
+        // $message=Message::where('msg_id',$param['msg_id'])->select();
+        // $atList=($message ?? null) ? explode(',',$message): [];
+        // // 两个数组取差集
+        // $newAtList = array_diff($atList, [$this->userInfo['user_id']]);
+        // Message::where('msg_id',$param['msg_id'])->update(['at'=>implode(',',$newAtList)]);
+        return success('');
+    }
+
+    // 获取系统公告
+    public function getAdminNotice(){
+        $data=Message::where(['chat_identify'=>'admin_notice'])->order('msg_id desc')->find();
+        $extends=$data['extends'] ?? [];
+        if(!$extends){
+            $extends['title']='';
+        }
+        $createTime=$data['create_time'] ?? 0;
+        if(!$createTime){
+            $extends['create_time']=$createTime;
+        }else{
+            $extends['create_time']=is_string($data['create_time']) ? strtotime($data['create_time']) : $data['create_time'];
+        }
+       
+        return success('',$extends);
+    }
+
+    // 双向删除消息
+    public function delMessage(){
+        $param = $this->request->param();
+        $id = $param['id'];
+        if(!$this->globalConfig['chatInfo']['dbDelMsg']){
+            return warning(lang('system.notAuth'));
+        }
+        $message = Message::where(['id' => $id])->find();
+        if ($message) {
+            if($message['from_user']!=$this->userInfo['user_id']){
+                return warning(lang('system.notAuth'));
+            }
+            Message::where(['id' => $id])->delete();
+            // 如果是最后一条消息,需要将上一条设置为最后一条
+            if($message['is_last']){
+                Message::where(['chat_identify'=>$message['chat_identify']])->order('msg_id desc')->limit(1)->update(['is_last'=>1]);
+            }
+            $toContactId = $message['to_user'];
+            if ($message['is_group'] == 1) {
+                $toContactId = explode('-', $message['chat_identify'])[1];
+            }
+            wsSendMsg($toContactId, 'delMessage', $message, $message['is_group']); 
+            return success('');
+        } else {
+            return warning(lang('im.exist'));
+        }
+    }
+}

+ 49 - 0
app/enterprise/listener/GroupChange.php

@@ -0,0 +1,49 @@
+<?php
+namespace app\enterprise\listener;
+
+use app\enterprise\model\{Group,User,Message};
+use app\manage\model\Config;
+use GatewayClient\Gateway;
+
+// 监听群聊变更事件
+class GroupChange
+{
+    public function handle(Group $group,User $user,$data){
+        Gateway::$registerAddress = config('gateway.registerAddress');
+        $groupInfo=$data['param'];
+        // 如果是扫码进群,表示手动操作,手动操作需要触发客户端ID推送
+        if($data['action'] == 'joinGroup'){
+            Gateway::joinGroup(request()->header('clientId'),$data['group_id']);
+        }elseif($data['action'] == 'autoCreateGroup'){
+            // 自动创建的群通知群主,如果在线则推送
+            if(Gateway::isUidOnline($groupInfo['owner_uid'])){
+                wsSendMsg([$groupInfo['owner_uid']], 'addGroup', $groupInfo);
+            }
+        }elseif($data['action'] == 'editGroupName'){
+            return;
+        }
+        $uid=$groupInfo['owner_uid'] ?? 1;
+        $userInfo=$user->field('user_id,realname,avatar')->where(['user_id'=>$uid])->find();
+        if($userInfo){
+            $userInfo=$userInfo->toArray();
+            $userInfo['id']=$userInfo['user_id'];
+            $userInfo['avatar']=avatarUrl($userInfo['avatar'],$userInfo['realname'],$userInfo['user_id']);
+            // 发送入群事件
+            $msg=[
+                'id'=>\utils\Str::getUuid(),
+                'user_id'=>$uid,
+                'content'=>lang('group.join',['username'=>$groupInfo['joinerName'] ?? 'xxx ']),
+                'toContactId'=>'group-'.$data['group_id'],
+                'sendTime'=>time()*1000,
+                'type'=>'event',
+                'is_group'=>1,
+                'status'=>'succeed',
+                'fromUser'=>$userInfo,
+                'at'=>[],
+                'action'=>$data['action'],
+            ];
+            Message::sendMsg($msg,1);
+        }
+        return true;
+    }
+}

+ 5 - 0
app/enterprise/middleware.php

@@ -0,0 +1,5 @@
+<?php
+return [
+    "locale",
+    "checkAuth"
+];

+ 29 - 0
app/enterprise/model/ChatDelog.php

@@ -0,0 +1,29 @@
+<?php
+/**
+ * raingad IM [ThinkPHP6]
+ * @author xiekunyu <raingad@foxmail.com>
+ */
+namespace app\enterprise\model;
+
+use app\BaseModel;
+class ChatDelog extends BaseModel
+{
+    public static function updateCache($user_id)
+    {
+        $userList=self::where(['user_id'=>$user_id,'is_group'=>0])->column('to_user');
+        $groupList=self::where(['user_id'=>$user_id,'is_group'=>1])->column('to_user');
+        $data=['userList'=>$userList,'groupList'=>$groupList];
+        cache('chat_delog_'.$user_id,$data,86400);
+    }
+
+    // 获取缓存
+    public static function getCache($user_id)
+    {
+        $data=cache('chat_delog_'.$user_id);
+        if(!$data){
+            self::updateCache($user_id);
+            $data=cache('chat_delog_'.$user_id);
+        }
+        return $data;
+    }
+}

+ 12 - 0
app/enterprise/model/Emoji.php

@@ -0,0 +1,12 @@
+<?php
+/**
+ * raingad IM [ThinkPHP6]
+ * @author xiekunyu <raingad@foxmail.com>
+ */
+namespace app\enterprise\model;
+
+use app\BaseModel;
+class Emoji extends BaseModel
+{
+
+}

+ 14 - 0
app/enterprise/model/File.php

@@ -0,0 +1,14 @@
+<?php
+/**
+ * raingad IM [ThinkPHP6]
+ * @author xiekunyu <raingad@foxmail.com>
+ */
+namespace app\enterprise\model;
+
+use app\BaseModel;
+class File extends BaseModel
+{
+    protected $pk="file_id";
+
+
+}

+ 28 - 0
app/enterprise/model/Friend.php

@@ -0,0 +1,28 @@
+<?php
+/**
+ * raingad IM [ThinkPHP6]
+ * @author xiekunyu <raingad@foxmail.com>
+ */
+namespace app\enterprise\model;
+
+use app\BaseModel;
+use think\facade\Db;
+
+class Friend extends BaseModel
+{
+    protected $pk="friend_id";
+    
+
+    public static function getFriend($map){
+       $list=self::where($map)->select();
+       $data=[];
+       if($list){
+          $list=$list->toArray();
+          foreach($list as $k=>$v){
+             $data[$v['friend_user_id']]=$v;
+          }
+       }
+       return $data;
+    }
+   
+}

+ 25 - 0
app/enterprise/model/Group.php

@@ -0,0 +1,25 @@
+<?php
+/**
+ * raingad IM [ThinkPHP6]
+ * @author xiekunyu <raingad@foxmail.com>
+ */
+namespace app\enterprise\model;
+
+use app\BaseModel;
+use think\facade\Db;
+use app\common\controller\Upload;
+class Group extends BaseModel
+{
+    protected $pk="group_id";
+
+   // 获取我的团队
+   public static function getMyGroup($map){
+      return Db::name('group_user')
+      ->alias('gu')
+      ->field('gr.group_id,gr.avatar,gr.name as displayName,gu.unread,gr.name_py,gr.owner_id,gr.notice,gu.role,gu.is_notice,gu.is_top,gr.setting')
+      ->join('group gr','gu.group_id=gr.group_id','left')
+      ->where($map)
+      ->select();
+   }
+
+}

+ 74 - 0
app/enterprise/model/GroupUser.php

@@ -0,0 +1,74 @@
+<?php
+/**
+ * raingad IM [ThinkPHP6]
+ * @author xiekunyu <raingad@foxmail.com>
+ */
+namespace app\enterprise\model;
+
+use app\BaseModel;
+use think\facade\Db;
+
+class GroupUser extends BaseModel
+{
+    protected $pk="id";
+
+   // 编辑团队信息
+   public static function editGroupUser($map,$data){
+      return self::where($map)->update($data);
+   }
+
+   // 获取团队成员列表
+   public static function getGroupUser($map,$listRows,$pageSize=1){
+      if($listRows){
+         $list=self::where($map)->order('role asc')->paginate(['list_rows'=>$listRows,'page'=>$pageSize]);
+         $data=$list->toArray()['data'];
+      }else{
+         $data=self::where($map)->order('role asc')->select();
+      }
+      return User::matchAllUser($data,true,'user_id');
+   }
+
+   // 验证权限
+   public static function checkAuth($map,$role=1){
+      $info=self::where($map)->find()->toArray();
+      if($info['role']<=$role){
+         return true;
+      }else{
+         return false;
+      }
+   }
+
+   // 加入群聊,发送加入消息
+   public static function joinGroup($uid,$inviteId,$groupInfo,$action='joinGroup'){
+      $group_id=$groupInfo['group_id'];
+      GroupUser::create([
+         'user_id'=>$uid,
+         'invite_id'=>$inviteId,
+         'status'=>1,
+         'role'=>$action=='autoCreateGroup' ? 1 : 3,
+         'group_id'=>$group_id,
+      ]);
+      event('GroupChange', ['action' => $action, 'group_id' => $group_id, 'param' => $groupInfo]);
+      queuePush(['action'=>'createAvatar','group_id'=>$group_id]);
+      return true;
+   }
+
+   // 获取群管理
+   public static function getGroupManage($group_id){
+      $list=self::where([['group_id','=',$group_id],['role','<',3],['status','=',1]])->select()->toArray();
+      $data=[];
+      foreach($list as $k=>$v){
+         $data[$v['user_id']]=$v['role'];
+      }
+      return $data;
+   }
+
+   /**
+    * 群成员
+    * @return \think\model\relation\HasOne
+    */
+    public function userInfo()
+    {
+        return $this->hasOne(User::class, 'user_id', 'user_id');
+    }
+}

+ 277 - 0
app/enterprise/model/Message.php

@@ -0,0 +1,277 @@
+<?php
+/**
+ * raingad IM [ThinkPHP6]
+ * @author xiekunyu <raingad@foxmail.com>
+ */
+namespace app\enterprise\model;
+
+use app\BaseModel;
+use think\facade\Db;
+use think\facade\Cache;
+class Message extends BaseModel
+{
+    protected $pk="msg_id";
+    protected $json      = ["extends"];
+    protected $jsonAssoc = true;
+    protected static $fileType=['file','image','video','voice','emoji'];
+
+    // 添加聊天记录
+    public static function addData($data){
+       return Db::name('message')->insert($data);
+    }
+
+    // 更新消息状态
+    public static function editData($update,$map){
+        return Db::name('message')->where($map)->update($update);
+    }
+
+    // 查询聊天记录
+    public static function getList($map,$where,$sort,$listRows,$pageSize){
+        $list= Db::name('message')
+        ->where($map)
+        ->where($where)
+        ->order($sort)
+        ->paginate(['list_rows'=>$listRows,'page'=>$pageSize]);
+        return $list;
+     }
+
+         //    发送消息
+    public function sendMessage($param,$globalConfig=false){
+        $is_group = $param['is_group'] ?? 0;
+        $uid=self::$uid ? : ($param['user_id'] ?? 1);
+        if($param['toContactId']==-1){
+            $is_group=0;
+        }
+        // 如果是系统账号,直接禁言
+        if($is_group>1){
+            $this->error=lang('im.forbidChat');
+            return false;
+        }
+        $isForward=$param['is_forward'] ?? 0;
+        $sendInterval = $globalConfig['chatInfo']['sendInterval'] ?? 0;
+        // 如果设置了消息频率则验证,转发不收限制
+        if ($sendInterval && !$isForward) {
+            if (Cache::has('send_' . $uid)) {
+                $this->error=lang('im.sendTimeLimit',['time'=>$sendInterval]);
+                return false;
+            }
+        }
+        if($param['type']=='text'){
+            // 限制文字内容长度
+            $text = strip_tags($param['content']);
+            $textLen = mb_strlen($text);
+            if ($textLen > 2048) {
+                $this->error=lang('im.msgContentLimit') . $textLen;
+                return false;
+            }
+            $param['content'] = preg_link($param['content']);
+            // 接入聊天内容检测服务
+            event('GreenText',['content'=>$param['content'],'service'=>"chat_detection"]);
+        }
+        $chatSetting = $globalConfig['chatInfo'];
+        if($param['toContactId']!=-1){
+            if ($is_group == 0) {
+                $kefuUser=$chatSetting['autoAddUser']['user_ids'] ?? [];
+                $manageUser=User::where([['status','=',1],['role','>',0]])->column('user_id');
+                $kefu=array_unique(array_merge($kefuUser,$manageUser));
+                $csUid = self::$userInfo['cs_uid'] ?? 0;
+                $manage=false;
+                // 发送者和接受者是客服或者管理员也可以发送消息
+                if(in_array($uid,$kefu) || in_array($param['toContactId'],$kefu)){
+                    $manage=true;
+                }
+                if($chatSetting['simpleChat'] == 0 && !$manage){
+                    $this->error=lang('im.forbidChat');
+                    return false;
+                }
+                // 如果是单聊,并且是社区模式和不是自己的客服、需要判断是否是好友
+                if ($globalConfig['sysInfo']['runMode'] == 2 && $csUid != $param['toContactId'] && !$manage) {
+                    // 判断我是不是对方的客服
+                    $cus = User::where(['user_id' => $param['toContactId']])->value('cs_uid');
+                    if ($cus != $uid) {
+                        $friend = Friend::where(['friend_user_id' => $uid, 'create_user' => $param['toContactId']])->find();
+                        if (!$friend) {
+                            $this->error=lang('im.notFriend');
+                            return false;
+                        }
+                        $otherFriend = Friend::where(['friend_user_id' => $param['toContactId'], 'create_user' => $uid])->find();
+                        if (!$otherFriend) {
+                            $this->error=lang('im.friendNot');
+                            return false;
+                        }
+                    }
+                }
+            }else{
+                // 群聊必须群成员才能发送消息
+                $group_id = explode('-', $param['toContactId'])[1] ?? '';
+                if(!$group_id){
+                    $this->error=lang('system.parameterError');
+                    return false;
+                }
+                if(!self::nospeak($group_id,$uid)){
+                    if($isForward){
+                        return false;
+                    }
+                    return shutdown(lang('group.notSpeak'));
+                }
+                // 群聊必须群成员才能发送消息
+                $groupUser=GroupUser::where(['user_id'=>$uid,'status'=>1,'group_id'=>$group_id,'delete_time'=>0])->find();
+                if(!$groupUser){
+                    $this->error = lang('group.notCustom');
+                    return false;
+                }
+                if($groupUser['no_speak_time']>time()){
+                    $this->error = lang('group.notSpeak',['time'=>date('Y-m-d H:i:s',$groupUser['no_speak_time'])]);
+                    return false;
+                }
+            }
+        }
+        
+        if ($sendInterval) {
+            Cache::set('send_' . $uid, time(), $sendInterval);
+        }
+        return self::sendMsg($param,$is_group);
+    }
+
+    //实际发送消息
+    public static function sendMsg($param,$is_group=0,$is_sys=0){
+        $uid=self::$uid ?: ($param['user_id'] ?? 1);
+        $toContactId=$param['toContactId'];
+        $manage=[];
+        // 重新建立会话,更新会话删除记录
+        $isDelChat=ChatDelog::where(['user_id'=>$uid,'to_user'=>$toContactId])->find();
+        if($isDelChat){
+            ChatDelog::where(['user_id'=>$uid,'to_user'=>$toContactId])->delete();
+            ChatDelog::updateCache($uid);
+        }
+        if($is_group==1){
+            $group_id = explode('-', $param['toContactId'])[1] ?? '';
+            $chat_identify=$toContactId;
+            $toContactId=$group_id;
+            $manage=GroupUser::getGroupManage($group_id);
+        }else{
+            $chat_identify=chat_identify($param['user_id'],$toContactId);
+        }
+        $fileSzie=isset($param['file_size'])?$param['file_size']:'';
+        $fileName=isset($param['file_name'])?$param['file_name']:'';
+        $ossUrl=getDiskUrl();
+        // 如果是转发图片文件的消息,必须把域名去除掉
+        $content=$param['content'];
+        if(in_array($param['type'],self::$fileType)){
+            if(strpos($param['content'],$ossUrl)!==false){
+                $content=str_replace($ossUrl,'',$param['content']);
+            }
+        }
+        $param['content']=$content;
+        $atList=($param['at'] ?? null) ? array_map('intval', $param['at']): [];
+        // 如果at里面有0,代表@所有人
+        if($atList && in_array(0,$atList)){
+            $atList=GroupUser::where([['group_id','=',$toContactId],['status','=',1],['user_id','<>',$param['user_id']]])->column('user_id');
+        }
+        $at=$atList ? implode(',',$atList) : null;
+        $data=[
+            'from_user'=>$param['user_id'],
+            'to_user'=>$toContactId,
+            'id'=>$param['id'],
+            'content'=>str_encipher($param['content'],true),
+            'chat_identify'=>$chat_identify,
+            'create_time'=>time(),
+            'type'=>$param['type'],
+            'is_group'=>$toContactId==-1 ? 3 : $is_group,
+            'is_read'=>$is_group ? 1 : 0,
+            'file_id'=>$param['file_id'] ?? 0,
+            "file_cate"=>$param['file_cate'] ?? 0,
+            'file_size'=>$fileSzie,
+            'file_name'=>$fileName,
+            'at'=>$at,
+            'pid'=>$param['pid'] ?? 0,
+            'extends'=>($param['extends'] ?? null) ? $param['extends'] : null,
+        ];
+        $message=new self();
+        $message->update(['is_last'=>0],['chat_identify'=>$chat_identify]);
+        $message->save($data);
+        
+        // 拼接消息推送
+        $type=$is_group?'group':'simple';
+        $sendData=$param;
+        $sendData['status']='succeed';
+        $sendData['at']=$atList;
+        $sendData['msg_id']=$message->msg_id;
+        $sendData['is_read']=0;
+        $sendData['to_user']=$toContactId;
+        $sendData['role']=$manage[self::$uid] ?? 3;
+        $sendData['sendTime']=(int)$sendData['sendTime'];
+        //这里单聊中发送对方的消息,对方是接受状态,自己是对方的联系人,要把发送对象设置为发送者的ID。
+        if($is_group){
+            $sendData['toContactId']=$param['toContactId'];
+            // 将团队所有成员的未读状态+1
+            GroupUser::editGroupUser([['group_id','=',$toContactId],['user_id','<>',$uid]],['unread'=>Db::raw('unread+1')]);
+        }else{
+            $sendData['toContactId']=$uid;
+        }
+        $sendData['fromUser']['id']=(int)$sendData['fromUser']['id'];
+        $sendData['fileSize']=$fileSzie;
+        $sendData['fileName']=$fileName;
+        if(in_array($sendData['type'],self::$fileType)){
+            $sendData['content']=getFileUrl($sendData['content']);
+            if($sendData['type']=='image'){
+                $pre=1;
+            }else{
+                $pre=2;
+            }
+            $sendData['preview']=previewUrl($sendData['content'],$pre);
+            $sendData['extUrl']=getExtUrl($sendData['content']);
+            $sendData['download']= $sendData['file_id'] ? getMainHost().'/filedown/'.encryptIds($sendData['file_id']) : '';
+        }
+        $forContactId=$sendData['toContactId'];
+        if($is_sys){
+            $forContactId=$toContactId;
+        }
+        if($is_group==0){
+            $toContactId=[$toContactId,$param['user_id']];
+        }
+        $sendData['toUser']=$param['toContactId'];
+        $user=new User();
+        // 将聊天窗口的联系人信息带上,方便临时会话
+        
+        $sendData['contactInfo']=$user->setContact($forContactId,$is_group,$sendData['type'],$sendData['content']);
+        // 向发送方发送消息
+        wsSendMsg($toContactId,$type,$sendData,$is_group);
+        $sendData['toContactId']=$param['toContactId'];
+        return $sendData;
+}
+
+    // 群禁言
+    public static function nospeak($group_id,$user_id){
+        $group=Group::find($group_id);
+        if($group->owner_id==$user_id){
+            return true;
+        }
+        if($group->setting){
+            $setting=json_decode($group->setting,true);
+            $nospeak=isset($setting['nospeak'])?$setting['nospeak']:0;
+            $role=GroupUser::where(['group_id'=>$group_id,'user_id'=>$user_id])->value('role');
+            if($nospeak==1 && $role>2){
+                return false;
+            }elseif($nospeak==2 && $role!=1){
+                return false;
+            }
+        }
+        return true;
+    }
+
+    // 将消息中的@用户加入到atListQueue中
+    public static function setAtread($messages,$user_id){
+        foreach($messages as $k=>$v){
+            if(!isset($v['at'])){
+                continue;
+            }
+            if($v['at'] && in_array($user_id,$v['at'])){
+               $atListQueue=Cache::get("atListQueue");
+               $atListQueue[$v['msg_id']][]=$user_id;
+               Cache::set("atListQueue",$atListQueue);
+            }
+        }
+    }
+
+}

+ 765 - 0
app/enterprise/model/User.php

@@ -0,0 +1,765 @@
+<?php
+
+/**
+ * raingad IM [ThinkPHP6]
+ * @author xiekunyu <raingad@foxmail.com>
+ */
+
+namespace app\enterprise\model;
+
+use GatewayClient\Gateway;
+use app\BaseModel;
+use think\facade\Db;
+use think\facade\Request;
+use think\model\concern\SoftDelete;
+use app\manage\model\Config;
+use thans\jwt\facade\JWTAuth;
+
+class User extends BaseModel
+{
+   use SoftDelete;
+
+   protected $pk = "user_id";
+   
+   public static $defaultField = 'user_id,realname,realname as displayName,account,avatar,name_py,email,last_login_ip';
+
+   protected $json = ['setting'];
+   protected $jsonAssoc = true;
+
+   // 系统人员或者其他静态人员
+   public static function staticUser(){
+      return [
+         'adminNotice'=>[
+            'id'=>'admin_notice',
+            'displayName'=>lang('system.notice'),
+            'avatar'=>getMainHost().'/static/common/img/notice.png',
+            'name_py'=>'xitongtongzhi',
+         ],
+         'fileTransfer'=>[
+            'id'=>-1,
+            'displayName'=>lang('system.favor'),
+            'avatar'=>getMainHost().'/static/common/img/file_transfer.png',
+            'name_py'=>'wodeshoucang',
+        ]
+      ];
+   }
+
+   public function getUid()
+   {
+      return self::$uid;
+   }
+
+   //查询用户信息
+   public static function getUserInfo($map=[])
+   {
+      if(!$map){
+         return self::$userInfo;
+      }
+      $data = self::where($map)->find();
+      if ($data) {
+         $data = $data->toArray();
+      }
+      return $data;
+   }
+   
+   /**
+     * 刷新用户token 之前token将被拉黑
+     * 修改用户数据后 调用该方法 并返回前台更新token
+     * @param array $info 用户信息
+     * @param string $terminal 客户端标识
+     * @return string
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public static function refreshToken($info,$terminal)
+    {
+        $info      = str_encipher(json_encode($info),true, config('app.aes_token_key'));
+        $authToken = 'bearer '.JWTAuth::builder(['info' => $info, 'terminal' => $terminal]);
+        return $authToken;
+    }
+
+   //   获取所有用户列表
+   public static function getAllUser($map, $user_ids = [],$user_id,$group_id = 0)
+   {
+      $field = self::$defaultField;
+      $list=[];
+      if($group_id){
+         $groupUser=GroupUser::where([['group_id','=',$group_id],['role','<>',1],['status','=',1]])->column('user_id');
+         if($groupUser){
+            $list=User::where([['user_id','in',$groupUser]])->field($field)->select()->toArray();
+         }
+      }else{
+         $config=Config::getSystemInfo();
+         // 如果是社区模式,就只查询自己的好友,如果是企业模式,就查询所有用户
+         if($config['sysInfo']['runMode']==1){
+            $list = self::where($map)->field($field)->select()->toArray();
+         }else{
+            $friendList = Friend::getFriend(['create_user' => $user_id,'status'=>1]);
+            $userList = array_keys($friendList);
+            $list = self::where($map)->where('user_id', 'in', $userList)->field($field)->select()->toArray();
+         }
+      }
+      foreach ($list as $k => $v) {
+         $list[$k]['disabled'] = false;
+         $list[$k]['avatar'] = avatarUrl($v['avatar'], $v['realname'], $v['user_id']);
+         if ($user_ids) {
+            if (in_array($v['user_id'], $user_ids)) {
+               $list[$k]['disabled'] = true;
+            }
+         }
+      }
+      return $list;
+   }
+
+   //查询用户列表
+   public static function getUserList($map, $user_id, $field = "")
+   {
+      if (!$field) {
+         $field = self::$defaultField;
+      }
+      
+      $config=Config::getSystemInfo();
+      // 如果是社区模式,就只查询自己的好友,如果是企业模式,就查询所有用户
+      if($config['sysInfo']['runMode']==1){
+         $friendList = Friend::getFriend(['create_user' => $user_id]);
+         $list = self::where($map)->field($field)->select();
+      }else{
+         $friendList = Friend::getFriend(['create_user' => $user_id,'status'=>1]);
+         $userList = array_keys($friendList);
+         // 将专属客服设置为好友
+         $csUid=request()->userInfo['cs_uid'] ?? 0;
+         if($csUid){
+            $userList[]=$csUid;
+         }
+         // 如果我有客服权限,就查询客服的好友
+         $cus=self::where(['cs_uid'=>$user_id])->column('user_id');
+         if($cus){
+            $userList=array_merge($userList,$cus);
+         }
+         $userList = array_unique($userList);
+         $list = self::where($map)->where('user_id', 'in', $userList)->field($field)->select();
+      }
+      $list_chart = chartSort($list, 'realname', false, 'index');
+      // 查询未读消息
+      $unread = Db::name('message')
+         ->field('from_user,count(msg_id) as unread')
+         ->where([['to_user', '=', $user_id], ['is_read', '=', 0], ['is_group', '=', 0]])
+         ->group('from_user')
+         ->select();
+      // 查询最近的联系人
+      $map1 = [['to_user', '=', $user_id], ['is_last', '=', 1], ['is_group', '=', 0]];
+      $map2 = [['from_user', '=', $user_id], ['is_last', '=', 1], ['is_group', '=', 0]];
+      $msgField = 'from_user,to_user,content as lastContent,create_time as lastSendTime,chat_identify,type,del_user';
+      $lasMsgList = Db::name('message')
+         ->field($msgField)
+         ->whereOr([$map1, $map2])
+         ->order('create_time desc')
+         ->select();
+      // 查询群聊
+      $group = Group::getMyGroup(['gu.user_id' => $user_id, 'gu.status' => 1]);
+      if ($group) {
+         $group = $group->toArray();
+         $group_ids = arrayToString($group, 'group_id');
+         $getGroupLastMsg = Db::name('message')->field($msgField)->where([['to_user', 'in', $group_ids], ['is_group', '=', 1], ['is_last', '=', 1]])->select();
+         $getAtMsg=Db::name('message')->field($msgField)->where([['to_user', 'in', $group_ids], ['is_group', '=', 1]])->whereFindInSet('at',$user_id)->select();
+
+         // halt($getAtMsg);
+         foreach ($group as $k => $v) {
+            $setting = $v['setting'] ? json_decode($v['setting'], true) : ['manage' => 0, 'invite' => 1, 'nospeak' => 0];
+            $group_id = 'group-' . $v['group_id'];
+            $group[$k]['id'] = $group_id;
+            $group[$k]['account'] = $group_id;
+            $group[$k]['avatar'] = avatarUrl($v['avatar'], $v['displayName'], $v['group_id'], 120,1);
+            $group[$k]['name_py'] = $v['name_py'];
+            $group[$k]['owner_id'] = $v['owner_id'];
+            $group[$k]['role'] = $v['role'];
+            $group[$k]['is_group'] = 1;
+            $group[$k]['setting'] = $setting;
+            $group[$k]['index'] = "[2]群聊";
+            $group[$k]['realname'] = $v['displayName'] . " [群聊]";
+            $group[$k]['is_notice'] = $v['is_notice'];
+            $group[$k]['is_top'] = $v['is_top'];
+            $group[$k]['is_online'] = 1;
+            $group[$k]['is_at'] = 0;
+            if ($getGroupLastMsg) {
+               foreach ($getGroupLastMsg as $key=>$val) {
+                  if ($val['to_user'] == $v['group_id']) {
+                     $group[$k]['type'] =$val['type'];
+                     $group[$k]['lastContent'] = str_encipher($val['lastContent'],false);
+                     $group[$k]['lastSendTime'] = $val['lastSendTime'] * 1000;
+                     // 已经赋值了删除掉提升下次循环的性能
+                     unset($getGroupLastMsg[$key]);
+                     break;
+                  }
+               }
+            }
+            if($getAtMsg){
+               foreach ($getAtMsg as $key=> $val) {
+                  if ($val['to_user'] == $v['group_id']) {
+                     ++$group[$k]['is_at'];
+                     // 已经赋值了删除掉提升下次循环的性能
+                     unset($getAtMsg[$key]);
+                  }
+               }
+            }
+         }
+      }
+      try{
+         Gateway::$registerAddress = config('gateway.registerAddress');
+         $onlineList=Gateway::getAllUidList();
+      }catch(\Exception $e){
+         $onlineList=[];
+      }
+      $isRegion=$config['sysInfo']['ipregion'] ?? 0;
+      foreach ($list_chart as $k => $v) {
+         // 是否有消息通知或者置顶聊天
+         $friend = isset($friendList[$v['user_id']]) ? $friendList[$v['user_id']] : [];
+         $list_chart[$k]['id'] = $v['user_id'];
+         $list_chart[$k]['displayName'] = ($friend['nickname'] ?? '') ? : $v['realname'];
+         $list_chart[$k]['name_py'] = $v['name_py'];
+         $list_chart[$k]['avatar'] = avatarUrl($v['avatar'], $v['realname'], $v['user_id'], 120);
+         $list_chart[$k]['lastContent'] = '';
+         $list_chart[$k]['unread'] = 0;
+         $list_chart[$k]['lastSendTime'] = time() * 1000;
+         $list_chart[$k]['is_group'] = 0;
+         $list_chart[$k]['setting'] = [];
+         $list_chart[$k]['is_at'] = 0;
+         $list_chart[$k]['last_login_ip'] = '';
+         $list_chart[$k]['location'] ="";
+         if($isRegion){
+            $list_chart[$k]['last_login_ip'] = $v['last_login_ip'];
+            $list_chart[$k]['location'] =$v['last_login_ip'] ? implode(" ", \Ip::find($v['last_login_ip'])) : "未知";
+         }
+         $is_online=0;
+         if(isset($onlineList[$v['user_id']])){
+            $is_online=1;
+         }
+         $list_chart[$k]['is_online'] = $is_online;
+         
+         $is_top = 0;
+         $is_notice = 1;
+         if ($friend) {
+            $is_top = $friend['is_top'];
+            $is_notice = $friend['is_notice'];
+         }
+         $list_chart[$k]['is_top'] = $is_top;
+         $list_chart[$k]['is_notice'] = $is_notice;
+         if ($unread) {
+            foreach ($unread as $val) {
+               if ($val['from_user'] == $v['user_id']) {
+                  $list_chart[$k]['unread'] = $val['unread'];
+                  break;
+               }
+            }
+         }
+         if ($lasMsgList) {
+            foreach ($lasMsgList as $val) {
+               if ($val['from_user'] == $v['user_id'] || $val['to_user'] == $v['user_id']) {
+                  $content = str_encipher($val['lastContent'],false);
+                  // 屏蔽已删除的消息
+                  if ($val['del_user']) {
+                     $delUser = explode(',', $val['del_user']);
+                     if (in_array($user_id, $delUser)) {
+                        $content = "";
+                     }
+                  }
+                  $list_chart[$k]['type'] = $val['type'];
+                  $list_chart[$k]['lastContent'] = $content;
+                  $list_chart[$k]['lastSendTime'] = $val['lastSendTime'] * 1000;
+
+                  break;
+               }
+            }
+         }
+      }
+      // 合并群聊和联系人
+      $data = array_merge($list_chart, $group);
+      // 合并助手消息和聊天消息
+      $helper=self::otherChat($user_id);
+      $data=array_merge($data,$helper);
+      return $data;
+   }
+
+      //查询用户列表
+   public static function getChatList($user_id, $field = "")
+   {
+      if (!$field) {
+         $field = self::$defaultField;
+      }
+      $list_chart=[];
+      $config=Config::getSystemInfo();
+      // 查询未读消息
+      $unread = Db::name('message')
+         ->field('from_user,count(msg_id) as unread')
+         ->where([['to_user', '=', $user_id], ['is_read', '=', 0], ['is_group', '=', 0]])
+         ->group('from_user')
+         ->select();
+      $unread = self::matchListKey($unread,'from_user');
+      // 查询最近的联系人
+      $map1 = [['to_user', '=', $user_id], ['is_last', '=', 1], ['is_group', '=', 0]];
+      $map2 = [['from_user', '=', $user_id], ['is_last', '=', 1], ['is_group', '=', 0]];
+      $msgField = 'from_user,to_user,content as lastContent,create_time as lastSendTime,chat_identify,type,del_user';
+      $lasMsgList = Db::name('message')
+         ->field($msgField)
+         ->whereOr([$map1, $map2])
+         ->order('create_time desc')
+         ->select();
+      if($lasMsgList){
+         $list=self::matchChatUser($lasMsgList, $user_id);
+         $list_chart=$list['list']; 
+         $lasMsgList=$list['lastMsg'];
+      }
+      // 查询群聊
+      $group = Group::getMyGroup(['gu.user_id' => $user_id, 'gu.status' => 1]);
+      $groupList=[];
+      if ($group) {
+         $group = $group->toArray();
+         $group_ids = arrayToString($group, 'group_id');
+         $getGroupLastMsg = Db::name('message')->field($msgField)->where([['to_user', 'in', $group_ids], ['is_group', '=', 1], ['is_last', '=', 1]])->select();
+         $getAtMsg=Db::name('message')->field('to_user,count(msg_id) as count')->where([['to_user', 'in', $group_ids], ['is_group', '=', 1]])->whereFindInSet('at',$user_id)->group('to_user')->select();
+         $getGroupLastMsg=self::matchListKey($getGroupLastMsg,'to_user');
+         $getAtMsg=self::matchListKey($getAtMsg,'to_user');
+         $groupList=self::recombileGroupList($group,$getGroupLastMsg,$getAtMsg,false,$user_id);
+      }
+      try{
+         Gateway::$registerAddress = config('gateway.registerAddress');
+         $onlineList=Gateway::getAllUidList();
+      }catch(\Exception $e){
+         $onlineList=[];
+      }
+      $isRegion=$config['sysInfo']['ipregion'] ?? 0;
+      $friendList = Friend::getFriend(['create_user' => $user_id,'status'=>1]);
+      $list_chart=self::recombineUserList($list_chart,$isRegion,$friendList,$unread,$lasMsgList,$onlineList,$user_id);
+      // 合并群聊和联系人
+      $data = array_merge($list_chart, $groupList);
+      // 合并助手消息和聊天消息
+      $helper=self::otherChat($user_id);
+      $data=array_merge($data,$helper);
+      return $data;
+   }
+
+   // 获取好友列表
+   public static function getFriendList($map,$user_id,$field='')
+   {
+      if (!$field) {
+         $field = self::$defaultField;
+      }
+      $config=Config::getSystemInfo();
+      // 如果是社区模式,就只查询自己的好友,如果是企业模式,就查询所有用户
+      if($config['sysInfo']['runMode']==1){
+         $friendList = Friend::getFriend(['create_user' => $user_id]);
+         $list = self::where($map)->field($field)->select();
+      }else{
+         $friendList = Friend::getFriend(['create_user' => $user_id,'status'=>1]);
+         $userList = array_keys($friendList);
+         // 将专属客服设置为好友,6.2+已废弃,客服改为双向好友关系
+         // $csUid=request()->userInfo['cs_uid'] ?? 0;
+         // if($csUid){
+         //    $userList[]=$csUid;
+         // }
+         // 如果我有客服权限,就查询客服的好友
+         // $cus=self::where(['cs_uid'=>$user_id])->column('user_id');
+         // if($cus){
+         //    $userList=array_merge($userList,$cus);
+         // }
+         // $userList = array_unique($userList);
+         $list = self::where($map)->where('user_id', 'in', $userList)->field($field)->select();
+      }
+      $list_chart=self::recombineUserList($list,$config['sysInfo']['ipregion'] ?? 0,$friendList);
+      $list=chartSort($list_chart, 'displayName', false, 'index');
+      // 合并助手消息和聊天消息
+      $helper=self::otherChat($user_id);
+      return array_merge($list,$helper);
+   }
+
+      // 获取好友列表
+   public static function getGroupList($user_id)
+   {
+      // 查询群聊
+      $group = Group::getMyGroup(['gu.user_id' => $user_id, 'gu.status' => 1]);
+      $groupList=[];
+      if ($group) {
+         $group = $group->toArray();
+         $groupList=self::recombileGroupList($group,[],[],true);
+      }
+      return $groupList;
+   }
+
+   //重新组成标准的群聊数据
+   protected static function recombileGroupList($group,$getGroupLastMsg=[],$getAtMsg=[],$is_all=false,$user_id=0){
+      $groupList=[];
+      foreach ($group as $k => $v) {
+         $val=$v;
+         $group_id = 'group-' . $v['group_id'];
+         if($user_id){
+            $delChat=ChatDelog::getCache($user_id);
+            $delChat = $delChat['groupList'] ?? [];
+            if(in_array($group_id,$delChat)){
+               continue;
+            }
+         }
+         $groupVal=$getGroupLastMsg[$v['group_id']] ?? [];
+         if($groupVal){
+            $val['type'] =$groupVal['type'];
+            $val['lastContent'] = str_encipher($groupVal['lastContent'],false);
+            $val['lastSendTime'] = $groupVal['lastSendTime'] * 1000;
+         }else{
+            if(!$is_all){
+               continue;
+            }
+         }
+         $setting = $v['setting'] ? json_decode($v['setting'], true) : ['manage' => 0, 'invite' => 1, 'nospeak' => 0, 'profile' => 0, 'history' => 0];
+         
+         $val['id'] = $group_id;
+         $val['account'] = $group_id;
+         $val['avatar'] = avatarUrl($v['avatar'], $v['displayName'], $v['group_id'], 120,1);
+         $val['name_py'] = $v['name_py'];
+         $val['owner_id'] = $v['owner_id'];
+         $val['role'] = $v['role'];
+         $val['is_group'] = 1;
+         $val['setting'] = $setting;
+         $val['index'] = "[2]群聊";
+         $val['realname'] = $v['displayName'] . " [群聊]";
+         $val['is_notice'] = $v['is_notice'];
+         $val['is_top'] = $v['is_top'];
+         $val['is_online'] = 1;
+         $val['is_at'] = 0;
+         $atMsgVal=$getAtMsg[$v['group_id']] ?? [];
+         if($atMsgVal){
+            $val['is_at'] =$atMsgVal['count'];
+         }
+         $groupList[]=$val;
+      }
+      return $groupList;
+   }
+
+   // 重新组成标准的好友数据
+   protected static function recombineUserList($list_chart,$isRegion,$friendList,$unread=[],$lasMsgList=[],$onlineList=[],$user_id=0){
+      $isRegion=$config['sysInfo']['ipregion'] ?? 0;
+      if($list_chart){
+         foreach ($list_chart as $k => $v) {
+            if($user_id){
+               // 过滤已删除的聊天
+               $delChat=ChatDelog::getCache($user_id);
+               $delChat = $delChat['userList'] ?? [];
+               if(in_array($v['user_id'],$delChat)){
+                  continue;
+               }
+            }
+            // 是否有消息通知或者置顶聊天
+            $friend = $friendList[$v['user_id']] ?? [];
+            $list_chart[$k]['id'] = $v['user_id'];
+            $list_chart[$k]['displayName'] = ($friend['nickname'] ?? '') ? : $v['realname'];
+            $list_chart[$k]['name_py'] = $v['name_py'];
+            $list_chart[$k]['avatar'] = avatarUrl($v['avatar'], $v['realname'], $v['user_id'], 120);
+            $list_chart[$k]['lastContent'] = '';
+            $list_chart[$k]['unread'] = 0;
+            $list_chart[$k]['lastSendTime'] = time() * 1000;
+            $list_chart[$k]['is_group'] = 0;
+            $list_chart[$k]['setting'] = [];
+            $list_chart[$k]['is_at'] = 0;
+            $list_chart[$k]['last_login_ip'] = '';
+            $list_chart[$k]['location'] ="";
+            if($isRegion){
+               $list_chart[$k]['last_login_ip'] = $v['last_login_ip'];
+               $list_chart[$k]['location'] =$v['last_login_ip'] ? implode(" ", \Ip::find($v['last_login_ip'])) : "未知";
+            }
+            $list_chart[$k]['is_online'] = ($onlineList[$v['user_id']] ?? 0) ? 1 : 0;
+            $is_top = 0;
+            $is_notice = 1;
+            if ($friend) {
+               $is_top = $friend['is_top'];
+               $is_notice = $friend['is_notice'];
+            }
+            $list_chart[$k]['is_top'] = $is_top;
+            $list_chart[$k]['is_notice'] = $is_notice;
+            $unrreadVal=$unread[$v['user_id']] ?? [];
+            $list_chart[$k]['unread'] = $unrreadVal['unread'] ?? 0;
+            $list_chart[$k]['type'] = 'text';
+            $lastMsgVal=$lasMsgList[$v['user_id']] ?? [];
+            if($lastMsgVal){
+               $content = str_encipher($lastMsgVal['lastContent'],false);
+               $list_chart[$k]['type'] = $lastMsgVal['type'];
+               $list_chart[$k]['lastContent'] = $content;
+               $list_chart[$k]['lastSendTime'] = $lastMsgVal['lastSendTime'] * 1000;
+            }
+         }
+      }
+      return $list_chart;
+   }
+
+   // 获取机器人聊天消息
+   public static function otherChat($uid){
+      $staticList=self::staticUser();
+      $adminNotice=$staticList['adminNotice'];
+      $fileTransfer=$staticList['fileTransfer'];
+      $count=Message::where(['chat_identify'=>$adminNotice['id']])->count();
+      $createTime=Message::where(['chat_identify'=>$adminNotice['id']])->order('id desc')->value('create_time');
+      $sendTime=0;
+      if($createTime){
+         $sendTime=is_string($createTime) ? strtotime($createTime) : $createTime;
+      }
+      $chat_identify=chat_identify($uid,$fileTransfer['id']);
+      $fileLast=Message::where(['is_last'=>1,'chat_identify'=>$chat_identify])->find();
+      $fileSendTime=$fileLast['create_time'] ?? '';
+      $content =$fileLast['content'] ?? '';
+      $friend=Friend::where(['create_user'=>$uid,'friend_user_id'=>$fileTransfer['id']])->find();
+      $notice=[
+            [
+               'id'=>$adminNotice['id'],
+               'user_id'=>$adminNotice['id'],
+               'displayName'=>$adminNotice['displayName'],
+               'realname'=>$adminNotice['displayName'],
+               'name_py'=>$adminNotice['name_py'],
+               'avatar'=>$adminNotice['avatar'],
+               'lastContent'=>$sendTime ? lang('system.announce',['num'=>$count]) :'',
+               'unread'=>0,
+               'lastSendTime'=>$sendTime * 1000,
+               'is_group'=>2,
+               'setting'=>[],
+               'type'=>'text',
+               'is_top'=>0,
+               'is_notice'=>1,
+               'is_online'=>0,
+               'index'=>"",
+            ],
+            [
+               'id'=>$fileTransfer['id'],
+               'user_id'=>$fileTransfer['id'],
+               'displayName'=>$fileTransfer['displayName'],
+               'realname'=>$fileTransfer['displayName'],
+               'name_py'=>$fileTransfer['name_py'],
+               'avatar'=>$fileTransfer['avatar'],
+               'lastContent'=> str_encipher($content,false) ?: lang('system.transFile'),
+               'unread'=>0,
+               'lastSendTime'=>((is_string($fileSendTime) ? strtotime($fileSendTime) : $fileSendTime) * 1000) ?: time() * 1000,
+               'is_group'=>3,
+               'setting'=>[],
+               'type'=>$fileLast['type'] ?? 'text',
+               'is_top'=>$friend['is_top'] ?? 0,
+               'is_notice'=>$friend['is_notice'] ?? 1,
+               'is_online'=>0,
+               'index'=>"",
+            ],
+      ];
+
+      return $notice;
+
+   }
+
+   public static function getList($map)
+   {
+      return self::field(self::$defaultField)->where($map)->select();
+   }
+
+   // 匹配用户列表信息(返回用户信息)
+
+   public static function matchUser($data, $many = false, $field = 'user_id', $cs = 80)
+   {
+      if ($many) {
+         $idr = arrayToString($data, $field, false);
+      } else {
+         $idr = [];
+         if (is_array($field)) {
+            foreach ($field as $v) {
+               $idr[] = $data[$v];
+            }
+         } else {
+            $idr = [$data[$field]];
+         }
+      }
+      $key = array_search(0, $idr);
+      if ($key) {
+         array_splice($idr, $key, 1);
+      }
+      $userList = self::where([['user_id', 'in', $idr]])->field(self::$defaultField)->select()->toArray();
+      $friend = Friend::where([['friend_user_id', 'in', $idr],['create_user','=',self::$uid]])->field('friend_user_id,nickname')->select()->toArray();
+      $list = [];
+      foreach ($userList as $v) {
+         $v['avatar'] = avatarUrl($v['avatar'], $v['realname'], $v['user_id'], $cs);
+         $v['id'] = $v['user_id'];
+         if($friend){
+            foreach($friend as $key=>$val){
+               if($val['friend_user_id']==$v['user_id']){
+                  $v['realname']=$val['nickname'] ? : $v['displayName'];
+                  break;
+               }
+            }
+         }
+         $list[$v['user_id']] = $v;
+      }
+      return $list;
+   }
+
+   // 匹配联系人列表
+   public static function matchChatUser($data,$user_id)
+   {
+      $idr=[];
+      $lastMsg=[];
+      foreach($data as $k=>$v){
+         $idr[]=$v['from_user'];
+         $idr[]=$v['to_user'];
+         if($v['from_user']==$user_id){
+            $lastMsg[$v['to_user']]=$v;
+            continue;
+         }
+         if($v['to_user']==$user_id){
+            $lastMsg[$v['from_user']]=$v;
+            continue;
+         }
+      }
+      $key = array_search(0, $idr);
+      if ($key) {
+         array_splice($idr, $key, 1);
+      }
+      $idr = array_diff($idr, [$user_id]);
+      $list = self::where([['user_id', 'in', $idr]])->field(self::$defaultField)->select()->toArray();
+      return [
+         'list'=>$list,
+         'lastMsg'=>$lastMsg
+      ];
+   }
+
+   // 匹配用户列表信息(返回data)  
+   public static function matchAllUser($data, $many = false, $field = 'user_id', $key = "userInfo", $cs = 80)
+   {
+      if ($many) {
+         $idr = arrayToString($data, $field);
+         $userList = self::getList([['user_id', 'in', $idr]]);
+         $userList=self::matchListKey($userList,'user_id');
+         foreach ($data as $k => $v) {
+            $vv=$userList[$v[$field]] ?? [];
+            if($vv){
+               $data[$k][$key] = [
+                  'id' => $vv['user_id'],
+                  'displayName' => $vv['realname'],
+                  'account' => $vv['account'],
+                  'name_py' => $vv['name_py'],
+                  'avatar' => avatarUrl($vv['avatar'], $vv['realname'], $vv['user_id'], $cs),
+               ];
+            }else{
+               $data[$k][$key]=[];
+            }
+         }
+      } else {
+         $user = self::getUserInfo(['user_id' => $data[$field]]);
+         $data[$key] = [
+            'id' => $user['user_id'],
+            'displayName' => $user['realname'],
+            'account' => $user['account'],
+            'name_py' => $user['name_py'],
+            'avatar' => avatarUrl($user['avatar'], $user['realname'], $user['user_id']),
+         ];
+      }
+      return $data;
+   }
+
+
+   // 将id转换成联系人信息
+   public function setContact($id,$is_group=0,$type='text',$content='',$contactInfo=null){
+      $data=[
+         'id'=>$id,
+         'lastContent'=>$content,
+         'unread'=>0,
+         'lastSendTime'=> time() * 1000,
+         'is_group'=>$is_group,
+         'is_top'=>0,
+         'is_notice'=>1,
+         'is_top'=>0,
+         'is_at'=>0,
+         'setting'=>[],
+         'type'=>$type,
+         'location'=>'',
+      ];
+      if($is_group==0){
+         $user=$contactInfo ?: User::where('user_id',$id)->find();
+         if(!$user){
+            $this->error=lang('user.exist');
+            return false;
+         }
+         $user->avatar=avatarUrl($user->avatar,$user->realname,$user->user_id,120);
+         // 查询好友关系
+         $friend= self::$userInfo ? Friend::where(['friend_user_id'=>$id,'create_user'=>self::$userInfo['user_id']])->find() : [];
+         $data['displayName'] = ($friend['nickname'] ?? '') ? : $user['realname'];
+         $data['avatar'] = avatarUrl($user['avatar'], $user['realname'], $user['user_id'], 120);
+         $data['location'] =$user['last_login_ip'] ? implode(" ", \Ip::find($user['last_login_ip'])) : "未知";
+         $data['name_py'] = $user['name_py'];
+      }else{
+         $group_id=is_numeric($id) ? $id : (explode('-',$id)[1] ?? 0);
+         $group=$contactInfo ?: Group::where(['group_id'=>$group_id])->find();
+         if(!$group){
+            $this->error=lang('group.exist');
+            return false;
+         }
+         $data['id'] = 'group-'.$group_id;
+         $data['displayName'] = $group['name'];
+         $data['avatar'] = avatarUrl($group['avatar'], $group['name'], $group['group_id'], 120,1);
+         $data['name_py'] = $group['name_py'];
+         $data['setting'] = $group['setting'];
+         $data['role'] = 3;
+      }
+      $data['index'] =getFirstChart($data['displayName']);
+      return $data;
+   }
+
+   // 验证账号的合法性
+   public function checkAccount(&$data){
+      $user_id=$data['user_id'] ?? 0;
+      if($user_id){
+         $user=self::find($data['user_id']);
+         if(!$user){
+            $this->error='账户不存在';
+            return false;
+         }
+         if($user->user_id==1 && self::$uid!=1){
+            $this->error='超管账户只有自己才能修改';
+            return false;
+         }
+         $other=self::where([['account','=',$data['account']],['user_id','<>',$data['user_id']]])->find();
+         if($other){
+            $this->error='账户已存在';
+            return false;
+         }
+      }else{
+         $user=self::where('account',$data['account'])->find();
+         if($user){
+               $this->error='账户已存在';
+               return false;
+         }
+      }
+      $config=Config::getSystemInfo();
+      $regauth=$config['sysInfo']['regauth'] ?? 0;
+      $acType=\utils\Regular::check_account($data['account']);
+      switch($regauth){
+            case 1:
+               if($acType!=1){
+                  $this->error='当前系统只允许账号为手机号!';
+                  return false;
+               }
+               break;
+            case 2:
+               if($acType!=2){
+                  $this->error='当前系统只允许账号为邮箱!';
+                  return false;
+               }
+               break;
+            case 3:
+               // 验证账号是否为手机号或者邮箱
+               if(!$acType){
+                  $this->error='账户必须为手机号或者邮箱';
+                  return false;
+               }
+               break;
+            default:
+               break;
+      }
+
+      $data['is_auth'] =$regauth ? 1 : 0;
+      $email=$data['email'] ?? '';
+      if($data['is_auth'] && $acType==2 && !$email){
+            $data['email'] =$data['account'];
+      }
+      return true;
+   }
+}

+ 24 - 0
app/enterprise/validate/User.php

@@ -0,0 +1,24 @@
+<?php
+/**
+ * lvzhe [a web admin based ThinkPHP5]
+ */
+
+namespace app\enterprise\validate;
+
+use think\Validate;
+
+class User extends Validate
+{
+    protected $rule = [
+        'account|帐号'      => 'require',
+        'password|密码'     => 'require',
+        'captcha|验证码'     => 'require|captcha',
+        'oldpassword|旧密码' => 'require',
+        'repassword|重复密码' => 'require',
+    ];
+
+    protected $scene = [
+        'password' => ['password', 'oldpassword', 'repassword'],
+        'login'    => ['account', 'password'],
+    ];
+}

+ 20 - 0
app/event.php

@@ -0,0 +1,20 @@
+<?php
+// 事件定义文件
+return [
+    'bind'      => [
+    ],
+
+    'listen'    => [
+        'AppInit'  => [],
+        'HttpRun'  => [],
+        'HttpEnd'  => [],
+        'LogLevel' => [],
+        'LogWrite' => [],
+        'UserRegister'=>['app\common\listener\UserRegister'],
+        'GroupChange'=>['app\enterprise\listener\GroupChange'],
+        'GreenText'=>['app\common\listener\GreenText'],
+    ],
+
+    'subscribe' => [
+    ],
+];

+ 212 - 0
app/index/controller/Index.php

@@ -0,0 +1,212 @@
+<?php
+namespace app\index\controller;
+
+use app\cms\model\Message as ModelMessage;
+use app\enterprise\model\{File,Group,User,Message};
+use think\facade\View;
+use app\manage\model\Config;
+use app\Request;
+use think\facade\Filesystem;
+
+class Index
+{
+
+    public function index()
+    {
+        if (!file_exists(PACKAGE_PATH . "install.lock")) {
+            return redirect(url('index/install/index'));
+        }
+        $demon=env('app.demon_mode',false);
+        if(request()->isMobile() && strpos(request()->url(),'index.html')===false && !$demon){
+            return redirect("/h5");
+        }
+        return redirect("/index.html");
+    }
+
+    public function view()
+    {
+        $url=request()->param('src');
+        $suffix=explode('.',$url);
+        $ext=$suffix[count($suffix)-1];
+        return View::fetch('',[
+            'url'  => $url,
+            'ext'=>$ext,
+            'name'=>lang('file.preview')
+        ]);
+    }
+
+    //    头像生成
+    public function avatar()
+    {
+        circleAvatar(input('str'), input('s') ?: 80, input('uid'));die;
+    }
+
+    // 文件下载
+    public function download()
+    {
+        
+        if (strpos($_SERVER['HTTP_USER_AGENT'], 'MicroMessenger') !== false) {
+            throw new \think\Exception(lang('file.browserDown'),400);
+        }
+        $param = request()->param();
+        $file_id = $param['file_id'] ?? 0;
+        if (!$file_id) {
+            throw new \think\Exception(lang('system.parameterError'), 502);
+        }
+        try {
+            $file_id = decryptIds($file_id);
+        } catch (\Exception $e) {
+            throw new \think\Exception($e->getMessage(), 400);
+        }
+        $file = File::find($file_id);
+        if (!$file) {
+            throw new \think\Exception(lang('file.exist'),404);
+        }
+        $file = $file->toArray();
+        // 兼容本地文件下载
+        $fileUrl=getDiskUrl();
+        if($fileUrl==getMainHost()){
+            $url=rtrim(public_path(),'/').$file['src'];
+        }else{
+            $url= getFileUrl($file['src']);
+        }
+        return \utils\File::download($url, $file['name'] . '.' . $file['ext'], $file['size'], $file['ext']);
+    }
+
+    // 扫码获取信息
+    public function scanQr(){
+        $param=request()->param();
+        $action=$param['action'] ?? '';
+        $token=$param['token'] ?? '';
+        $realToken=$param['realToken'] ?? '';
+        if(request()->isPost() && $action && $token && $realToken){
+            $actions=[
+                'g'=>'group',
+                'u'=>'user',
+                'a'=>'qrLogin',
+            ];
+            $a=$actions[$action] ?? '';
+            if(!$a){
+                return warning(lang('scan.failure'));
+            }
+            return $this->$a($param);
+        }else{
+            return redirect('/downapp');
+        }
+    }
+
+    protected function group($param)
+    {
+        $token=authcode(urldecode($param['realToken']),"DECODE", 'qr');
+        if(!$token){
+            return warning(lang('scan.failure'));
+        }
+        $groupInfo=explode('-',$token);
+        $uid=$groupInfo[0];
+        $group_id=$groupInfo[1];
+        $group=Group::find($group_id);
+        if($group){
+            $group=$group->toArray();
+            $group['avatar']=avatarUrl($group['avatar'],$group['name'],$group_id,120,1);
+            $group['invite_id']=$uid;
+            $group['id']='group-'.$group_id;
+            $group['action']='groupInfo';
+            return success('',$group);
+        }else{
+            return warning(lang('scan.failure'));
+        }
+    }
+
+    protected function user($param)
+    {
+        $id=decryptIds($param['token']);
+        if(!$id){
+            return warning(lang('scan.failure'));
+        }
+        $user=User::where(['user_id'=>$id])->field(User::$defaultField)->find();
+        if($user){
+            $user=$user->toArray();
+            $user['avatar']=avatarUrl($user['avatar'],$user['realname'],$user['user_id'],120);
+            $user['id']=$user['user_id'];
+            $user['action']='userInfo';
+            return success('',$user);
+        }else{
+            return warning(lang('scan.failure'));
+        }
+    }
+
+    // 简易实现二维码扫描登录
+    protected function qrLogin($param){
+        $appid=config('app.app_id');
+        $client_id=authcode(urldecode($param['realToken']),"DECODE",$appid);
+        if(!$client_id){
+            return warning(lang('scan.failure'));
+        }
+        return success('',[
+            'loginToken'=>urlencode(authcode($client_id,'ENCODE',$appid,300)),
+            'action'=>'qrLogin'
+        ]);
+    }
+
+    // app下载页
+    public function downApp(){
+        // echo request()->domain(true);
+        $downAppUrl=env('app.downApp_url','');
+        if($downAppUrl){
+            return redirect($downAppUrl);
+        }
+        $config=Config::where('name','sysInfo')->value('value');
+        $android=getAppDowmUrl('android');
+        $winUrl=getAppDowmUrl('windows');
+        $macUrl=getAppDowmUrl('mac');
+        $client=[
+            'android_appid'=>env('app.android_appid',''),
+            'android_webclip'=>env('app.android_webclip','') ? : $android,
+            'ios_appid'=>env('app.ios_appid',''),
+            'ios_webclip'=>env('app.ios_webclip',''),
+            'win_webclip'=>env('app.win_webclip','') ? : $winUrl,
+            'mac_webclip'=>env('app.mac_webclip','') ? : $macUrl
+        ];
+        $noUrl=false;
+        if(!$client['android_appid'] && !$client['android_webclip']  && !$client['ios_appid'] && !$client['ios_webclip']){
+           $noUrl=true;
+        }
+        View::assign('noUrl',$noUrl);
+        View::assign('client',$client);
+        View::assign('config',$config);
+        return View::fetch();
+    }
+
+    // 下载APP
+    public function downloadApp(){
+        
+        $platform=request()->param('platform','windows');
+        $config=config('version.'.$platform);
+        $name=config('version.app_name');
+        if($platform=='android'){
+            $packageName=$name."_Setup_".$config['version'].".apk";
+        }elseif($platform=='mac'){
+            $packageName=$name."_Setup_".$config['version'].".dmg";
+        }else{
+            $packageName=$name."_Setup_".$config['version'].".exe";
+        }
+        $file=PACKAGE_PATH . $packageName;
+        if(is_file($file)){
+            return \utils\File::download($file, $packageName);
+        }else{
+            return shutdown(lang('file.exist'));
+        }
+    }
+
+    public function test(){
+        echo time()-(30*86400);
+        // $m=new Message();
+        // $sql="ALTER TABLE `".config('database.connections.mysql.prefix')."message` ENGINE = InnoDB;";
+        // \think\facade\Db::execute($sql);
+        // $list=Message::where('create_time','<',time()-(30*86400))->where(['type'=>'image'])->select()->toArray();
+        // foreach($list as $k=> $v){
+        //     $list[$k]['src']=str_encipher($v['content'],false);
+        // }
+        // halt($list);
+    }
+}

Fișier diff suprimat deoarece este prea mare
+ 570 - 0
app/index/controller/Install.php


+ 17 - 0
app/index/route/app.php

@@ -0,0 +1,17 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+use think\facade\Route;
+Route::rule('avatar/:str/:s/:uid','index/avatar');
+Route::rule('view','index/index/view');
+Route::rule('filedown/:file_id','index/download');
+Route::rule('scan/:action/:token','index/scanQr');
+Route::rule('downapp','index/index/downapp');
+Route::rule('downloadApp/:platform','index/index/downloadApp');

+ 207 - 0
app/job/Work.php

@@ -0,0 +1,207 @@
+<?php
+/**
+ * User raingad
+ * Date 2021/8/2 15:31
+ */
+
+namespace app\job;
+
+use app\common\controller\Upload;
+
+use app\enterprise\model\{Group,GroupUser,User,Message,File};
+
+use Exception;
+
+use think\queue\Job;
+
+use think\facade\Filesystem;
+
+class Work
+{
+    // 允许的队列
+    protected $actions=[
+        'createAvatar', //创建、更新头像
+        'clearFiles', //清理文件
+        'clearVoice', //清理音频
+        'setIsRead', //设置消息为已读
+        'forwardMessage'  //转发消息
+    ];
+
+    // 执行队列
+    public function fire(Job $job, $data)
+    {
+        $action = $data['action'] ?? '';
+        //如果重试超过3次,或者没有action或者没有type,直接删除队列
+        if ($job->attempts() > 3 || !in_array($action,$this->actions)) {
+            $job->delete();
+            return;
+        }
+        try {
+            // 根据不同的方法执行处理数据
+            $res=$this->$action($data);
+            if($res){
+                print("<info>".$action." is success</info> \n");
+            }else{
+                print("<info>".$action." is error</info> \n");
+            }
+        } catch (\Exception $e) {
+            print("<info>".$action." error: ".$e->getMessage()."</info> \n");
+            
+        }
+        $job->delete();
+    }
+
+    // 创建头像
+    public function createAvatar($data)
+    {
+        $group_id=$data['group_id'] ?? 0;
+        if(!$group_id){ return false;}
+        $userList = GroupUser::where(['group_id' => $group_id,'status'=>1])->limit(9)->column('user_id');
+        $userList = User::where('user_id', 'in', $userList)->select()->toArray();
+        $imgList  = [];
+        $dirPath  = app()->getRootPath() . 'public/temp';
+        foreach ($userList as $k => $v) {
+            if ($v['avatar']) {
+                $imgList[] = avatarUrl($v['avatar'], $v['realname'], $v['user_id']);
+            } else {
+                $imgList[] = circleAvatar($v['realname'], 80, $v['user_id'], 1, $dirPath);
+            }
+        }
+        $groupId = 'group_' . $group_id;
+        $path    = $dirPath . '/' . $groupId . '.jpg';
+        $a       = getGroupAvatar($imgList, 1, $path);
+        $url     = '';
+        if ($a) {
+            $upload  = new Upload();
+            $newPath = $upload->uploadLocalAvatar($path, [], $groupId);
+            if ($newPath) {
+                Group::where('group_id', $group_id)->update(['avatar' => $newPath]);
+                $url = avatarUrl($newPath);
+            }
+        }
+        $files = glob($dirPath . '/*'); // 获取目录下所有文件路径
+        foreach ($files as $file) {
+            if (is_file($file)) { // 如果是文件则删除
+                unlink($file);
+            }
+        }
+        wsSendMsg($group_id,"setManager",['group_id'=>'group-'.$group_id,'avatar'=>$url],1);
+        return true;
+    }
+
+    // 清理除了音频相关的文件
+    public function clearFiles($data){
+        $fileIds = $data['fileIds'];
+        $count=0;
+        foreach($fileIds as $fileId){
+            $message=Message::where(['file_id'=>$fileId])->count();
+            // 如果还有消息,就不删除文件
+            if($message>0){
+                continue;
+            }
+            $file=File::find($fileId);
+            if($file){
+                $MD5=$file->md5;
+                $src=$file->src;
+                $file->delete();
+                // 查询相同文件
+                $sameFile=File::where(['md5'=>$MD5])->count();
+                // 如果有相同的文件,则不删除原件
+                if($sameFile){
+                    continue;
+                }
+                $count++;
+                // 删除源文件
+                $disk=env('filesystem.driver','local');
+                Filesystem::disk($disk)->delete($src);
+            }
+            print("<info>成功删除".$count."个文件!</info> \n");
+            return true;
+        }
+    }
+
+    // 清理除了音频相关的文件
+    public function clearVoice($data){
+        $list = $data['list'];
+        foreach($list as $content){
+            $src = str_encipher($content,false);
+            // 解密文件路径,删除源文件
+            $disk=env('filesystem.driver','local');
+            Filesystem::disk($disk)->delete($src);
+        }
+        print("<info>成功删除".count($list)."个音频文件!</info> \n");
+        return true;
+    }
+
+    // 设置已读
+    public function setIsRead($data){
+        $is_group=$data['is_group'];
+        $to_user= $data['to_user'];
+        $messages=$data['messages'];
+        $user_id=$data['user_id'];
+        if ($is_group==1) {
+            $toContactId = explode('-', $to_user)[1];
+            // 将@消息放到定时任务中逐步清理
+            if($messages){
+                Message::setAtRead($messages,$user_id);
+            }
+            // 更新群里面我的所有未读消息为0
+            GroupUser::editGroupUser(['user_id' => $user_id, 'group_id' => $toContactId], ['unread' => 0]);
+        } else if($is_group==0) {
+            $chat_identify = chat_identify($user_id, $to_user);
+            // 更新我的未读消息为0
+            Message::update(['is_read' => 1], [['chat_identify', '=', $chat_identify], ['to_user', '=', $user_id]]);
+            // 告诉对方我阅读了消息
+            wsSendMsg($to_user, 'readAll', ['toContactId' => $user_id]);
+        } 
+        return true;
+    }
+    
+    // 转发消息
+    public function forwardMessage($data){
+        try{
+            $is_group=0;
+            $error=0;
+            $simpleChat=$data['config']['chatInfo']['simpleChat'] ?? 1;
+            $userInfo=$data['userInfo'];
+            $userIds=$data['user_ids'];
+            foreach($userIds as $k=>$v){
+                $msgInfo=$data['message'];
+                if(strpos($v,'group')!==false){
+                    $is_group=1;
+                }else{
+                    $is_group=0;
+                }
+                if($is_group==0 && $simpleChat==0){
+                    $error++;
+                    continue;
+                }
+                $msgInfo['id']=\utils\Str::getUuid();
+                $msgInfo['status']='successd';
+                $msgInfo['user_id']=$userInfo['user_id'];
+                $msgInfo['sendTime']=time()*1000;
+                $msgInfo['toContactId']=$v;
+                $msgInfo['content']=str_encipher($msgInfo['content'],false);
+                $msgInfo['fromUser']=[
+                    'id'=>$userInfo['user_id'],
+                    'avatar'=>avatarUrl($userInfo['avatar'],$userInfo['realname'],$userInfo['user_id'],120),
+                    'displayName'=>$userInfo['realname']
+                ];
+                $msgInfo['is_group']=$is_group;
+                $message=new Message();
+                $msgInfo['is_forward']=1;
+                $isSend=$message->sendMessage($msgInfo,$data['config']);
+                if(!$isSend){
+                    $error++;
+                }
+            }
+            $count=count($userIds);
+            print("<info>成功转发".($count - $error)."条消息</info> \n");
+            return true;
+        }catch(\Exception $e){
+            print("<info>转发失败!</info> \n".$e->getMessage().$e->getLine());
+        }
+        
+    }
+
+}

+ 62 - 0
app/lang/en_us copy.php

@@ -0,0 +1,62 @@
+<?php
+return [  
+    // Friend  
+    'cannot_add_self_as_friend' => 'Cannot add yourself as a friend',  
+    'you_are_already_friends' => 'You are already friends',  
+    'you_have_already_applied_please_wait' => 'You have already applied, please wait for their approval',  
+    'new_friend' => 'New Friend',  
+    'has_added_you_as_a_friend' => 'has added you as a friend',  
+    'added_successfully' => 'Added successfully',  
+    'request_does_not_exist' => 'Request does not exist',  
+    'operation_successful' => 'Operation successful',  
+    'friend_does_not_exist' => 'Friend does not exist',  
+    'deleted_successfully' => 'Deleted successfully',  
+    'note_cannot_be_empty' => 'Note cannot be empty',  
+    'settings_updated_successfully' => 'Settings updated successfully',  
+      
+    // Group  
+    'you_do_not_have_permission_only_owners_and_admins_can_modify' => 'You do not have permission, only group owners and administrators can modify!',  
+    'modified_successfully' => 'Modified successfully',  
+    'member_limit_exceeded' => 'Member limit exceeded',  
+    'you_do_not_have_permission' => 'You do not have permission!',  
+    'settings_failed' => 'Settings failed!',  
+    'you_do_not_have_permission_to_create_group' => 'You do not have permission to create a group!',  
+    'please_select_at_least_two_members' => 'Please select at least two members!',  
+    'group_created' => 'Group created',  
+    'your_permission_is_not_sufficient' => 'Your permission is not sufficient!',  
+    'deleted_successfully_group' => 'Deleted successfully',  
+    'please_enter_content' => 'Please enter content!',  
+    'you_are_already_in_this_group' => 'You are already in this group!',  
+    'joined_successfully' => 'Joined successfully',  
+    'group_does_not_exist' => 'Group does not exist',  
+    'user_does_not_exist' => 'User does not exist',  
+    'transferred_successfully' => 'Transferred successfully',  
+    'change_failed' => 'Change failed',  
+      
+    // IM  
+    'private_chat_is_currently_disabled' => 'Private chat is currently disabled!',  
+    'you_are_not_on_their_friend_list_cannot_send_message' => 'You are not on their friend list, cannot send a message!',  
+    'they_are_not_your_friend_cannot_send_message' => 'They are not your friend, cannot send a message!',  
+    'send_failed' => 'Send failed',  
+    'please_select_users_or_quantity_not_exceeding_10' => 'Please select users or quantity not exceeding 10!',  
+    'message_does_not_exist' => 'Message does not exist',  
+    'forwarding_failed_due_to_rules' => 'Forwarding failed due to rules',  
+    'forwarded_successfully' => 'Forwarded successfully',  
+    'you' => 'You',  
+    'other' => 'Other',  
+    'cannot_withdraw_after_2_minutes' => 'Cannot withdraw after 2 minutes!',  
+    'withdrawn_a_message' => 'Withdrawn a message',  
+    'you_do_not_have_permission_to_withdraw_this_message' => 'You do not have permission to withdraw this message',  
+    'a_message_was_withdrawn_by_an_admin' => 'A message was withdrawn by an admin',  
+    'deleted_successfully_im' => 'Deleted successfully',  
+    'call_cancelled' => 'Call cancelled',  
+    'declined' => 'Declined',  
+    'not_connected' => 'Not connected',  
+    'call_duration' => 'Call duration',  
+    'busy' => 'Busy',  
+    'operation_performed_on_another_device' => 'Operation performed on another device',  
+    'video_call' => 'Video call',  
+    'audio_call' => 'Audio call',  
+    'answer_call_request' => 'Answer call request',  
+    'data_exchange_in_progress' => 'Data exchange in progress'
+];

+ 162 - 0
app/lang/en_us.php

@@ -0,0 +1,162 @@
+<?php
+return [
+    'system' => [  
+        'success' => 'Operation successful',  
+        'fail' => 'Operation failed',  
+        'error' => 'System error',  
+        'forbidden' => 'Access forbidden',  
+        'exist' => 'The record does not exist',  
+        'sendOK' => 'Sent successfully',  
+        'sendFail' => 'Sending failed',  
+        'delOk' => 'Deletion successful',  
+        'settingOk' => 'Settings updated successfully',  
+        'notNull' => 'Cannot be empty',  
+        'editOk' => 'Edit successful',  
+        'editFail' => 'Edit failed',  
+        'addOk' => 'Addition successful',  
+        // The original 'addFail' seems to be a typo, assuming it should be '添加失败' which translates to 'Addition failed'  
+        'addFail' => 'Addition failed',  
+        'joinOk' => 'Joining successful',  
+        'notAuth' => 'You do not have the permission to perform this operation!',  
+        'demoMode' => 'Modifications are not supported in demo mode',  
+        'parameterError' => 'Parameter error',  
+        'longTime' => 'Request timeout',  
+        'apiClose' => 'API is closed',  
+        'appIdError' => 'appId error',  
+        'signError' => 'Signature error',  
+        'tooFast'=>"You visited too fast!",
+        'notice'=>"System Notice",
+        'favor'=>"My Collection",
+        'message'=>"System Message",
+        'announce'=>"{:num} announcements",
+        'transFile'=>"Transfer your files",
+    ],
+    'messageType' => [  
+        'other' => "[Unsupported message type]",  
+        'image' => "[Image]",  
+        'emoji' => "[DIYemoji]",  
+        'voice' => "[Voice]",  
+        'video' => "[Video]",  
+        'file' => "[File]",  
+        'webrtcAudio' => "[Audio call request with you]",  
+        'webrtcVideo' => "[Video call request with you]",  
+    ],
+    'friend' => [  
+        'notAddOwn' => "You cannot add yourself as a friend",  
+        'already' => "You are already friends",  
+        'repeatApply' => "You have already sent a request, please wait for the other person to accept",  
+        'new' => "New friend",  
+        'apply' => "Has added you as a friend",  
+        'notApply' => "The request does not exist",  
+        'not' => "Friend does not exist",  
+        'newChat' => "You have been successfully added as friends, let's start chatting now! ",  
+        'limit' => "Your friends have reached the limit! ",  
+        'refuse' => "The other party has refused to add you as a friend! ",
+    ],  
+    'group' => [  
+        'name' => "Group chat",  
+        'notAuth' => "You do not have permission to perform this action. Only the group owner and administrators can make changes!",  
+        'userLimit' => "The number of members cannot exceed {:userMax} people!",  
+        'inviteLimit'=>'The number of people invited at a single time cannot exceed {:limit}! ',
+        'invite' => "{:username} has invited you to join the group chat",  
+        'removeUser'=>"You have been removed from Group chats! ",
+        'notCustom'=>"You are not a member of this group and have no right to send messages!",
+        'add' => "{:username} has created a group chat",  
+        'join'=>"{:username} join the group chat",
+        'atLeast' => "Please select at least two people!",  
+        'alreadyJoin' => "You are already in this group!",  
+        'exist' => "The group chat does not exist",
+        'notice'=>"Announcement",
+        'all'=>"All",
+        'noSpeak'=>"You have been banned from speaking, recovery time is {:time}",
+        'notSpeak'=>"Group chat has been banned!",
+        'limit'=>"You have been restricted from creating Group chats! ",
+    ],
+    'user' => [  
+        'exist' => "User does not exist",  
+        'codeErr' => "Verification code is incorrect!",  
+        'newCodeErr' => "New verification code is incorrect!",  
+        'passErr' => "Original password is incorrect!",  
+        'already' => "Account already exists",  
+        'registerOk' => "Registration successful",  
+        'loginOk' => "Login successful",  
+        'tokenFailure' => "TOKEN has expired!",  
+        'forbid' => "Your account has been disabled",  
+        'passError' => "Password is incorrect",  
+        'logoutOk' => "Logout successful!",  
+        'closeRegister' => "The system has disabled registration!",  
+        'inviteCode' => "Invite code has expired!",  
+        'accountVerify' => "Account must be a phone number or email",  
+        'waitMinute' => "Please try again after one minute!",  
+        "loginAccount" => "Login account",  
+        "registerAccount" => "Register account",  
+        "editPass" => "Change password",  
+        "editAccount" => "Edit account",  
+        'loginError' => 'Login information is incorrect. Please log in again.',  
+        'mustToken' => 'Please log in to the system first',  
+        'blacklist' => 'Login has expired. Please log in again',  
+        'expired' => 'Login has expired. Please log in again',
+        'notOwn' =>"Customer service can't be for him",
+        'loginLimit' =>"Your password has been wrong too many times. Please try again later! ",
+        'registerLimit'=>"Please register again in {:time} minutes"
+    ],  
+    'im' => [  
+        'forbidChat' => "Private chatting is currently prohibited!",  
+        'notFriend' => "You are not on their friend list, cannot send messages!",  
+        'friendNot' => "They are not your friend, cannot send messages!",  
+        'forwardLimit' => "Please select fewer than {:count} recipients for forwarding!",  
+        'exist' => "Message does not exist",  
+        'forwardRule' => "Forwarding failed for {:count} messages due to rule restrictions!",  
+        'forwardOk' => 'Message forwarded successfully',  
+        'you' => 'You',  
+        'other' => 'Recipient',  
+        'redoLimitTime' => "Cannot recall messages after {:time} minutes!",  
+        'redo' => " A message has been recalled",  
+        'manageRedo' => " A message has been recalled by (an admin)",
+        'msgContentLimit' => "Message content too long",
+        'sendTimeLimit'=>'The message sending interval is {:time} seconds!',
+        'forbidMsg'=>'[The message is suspected of violation and has been blocked]',
+    ],
+    'webRtc' => [  
+        'cancel' => 'Call has been canceled',  
+        'refuse' => 'Call has been rejected',  
+        'notConnected' => 'Call not connected',  
+        'duration' => 'Call duration: {:time}',  
+        'busy' => 'Busy',  
+        'other' => 'Operation performed on another device',  
+        'video' => 'Video call',  
+        'audio' => 'Audio call',  
+        'answer' => 'Answer call request',  
+        'exchange' => 'Data exchange in progress',  
+        'fail' => 'Call failed',  
+    ],  
+    'email' => [  
+        'input' => 'Please enter a valid email address',  
+        'testTitle' => "Test Email",  
+        'testContent' => "This is a test email. If you receive it, it means all your configurations are correct!",  
+    ],  
+    'task' => [  
+        'schedule' => 'Scheduled Task',  
+        'queue' => 'Message Queue',  
+        'worker' => 'Message Push',  
+        'clearStd' => 'Clear Logs',  
+        'null' => "Unknown task",  
+        'winRun' => "To start on Windows, please run the 'start_for_win.bat' file in the root directory",  
+        'alreadyRun' => "Process is already running",  
+        'startOk' => "Started successfully",  
+        'startFail' => "Failed to start",  
+        'notRun' => "Process is not running",  
+        'logExist' => "Log does not exist",  
+    ],  
+    'file' => [  
+        'preview' => "Preview file",  
+        'browserDown' => "Please use the browser to download",  
+        'exist' => "The file does not exist", // Note: This might be a duplicate of 'preview' and could be replaced with a more specific message  
+        'uploadLimit' => "File size cannot exceed {:size}MB",  
+        'typeNotSupport' => "File format is not supported",  
+        'uploadOk' => "Upload successful",  
+    ],  
+    'scan' => [  
+        'failure' => 'QR code has expired'  
+    ]
+];

+ 161 - 0
app/lang/zh_cn.php

@@ -0,0 +1,161 @@
+<?php
+return [
+    'system'=>[
+        'success'=>'操作成功',
+        'fail'=>'操作失败',
+        'error'=>'系统错误',
+        'forbidden'=>"禁止访问",
+        'exist'=>"记录不存在",
+        'sendOK'=>"发送成功",
+        'sendFail'=>"发送失败",
+        'delOk'=>"删除成功",
+        'settingOk'=>"设置成功",
+        'notNull'=>"不能为空",
+        'editOk'=>'修改成功',
+        'editFail'=>'修改失败',
+        'addOk'=>'添加成功',
+        'addFail'=>'添加成功',
+        'joinOk'=>'加入成功',
+        'notAuth'=>"您没有操作权限!",
+        'demoMode'=>"演示模式不支持修改",
+        'parameterError'=>"参数错误",
+        'longTime'=>'请求超时',
+        'apiClose'=>"接口已关闭",
+        'appIdError'=>'appId错误',
+        'signError'=>'签名错误',
+        'toofast'=>"您访问的太快了!",
+        'notice'=>"系统通知",
+        'favor'=>"我的收藏",
+        'message'=>"系统消息",
+        'announce'=>"{:num}条公告",
+        'transFile'=>"传输你的文件"
+    ],
+    'messageType'=>[
+        'other'=>"[暂不支持的消息类型]",
+        'image'=>'[图片]',
+        'voice'=>'[语音]',
+        'emoji'=>'[自定义表情]',
+        'video'=>'[视频]',
+        'file'=>'[文件]',
+        'webrtcAudio'=>'[正在请求与您语音通话]',
+        'webrtcVideo'=>'[正在请求与您视频通话]',
+    ],
+    'friend'=>[
+        'notAddOwn'=>"不能添加自己为好友",
+        'already'=>"你们已经是好友了",
+        'repeatApply'=>"你已经申请过了,请等待对方同意",
+        'new'=>"新朋友",
+        "apply"=>"添加您为好友",
+        'notApply'=>"申请不存在",
+        'not'=>"好友不存在",
+        'newChat'=>"你们已经成功添加为好友,现在开始聊天吧!",
+        'limit'=>"您的好友已达上限!",
+        'refuse'=>"对方拒绝添加您为好友!",
+    ],
+    'group'=>[
+        'name'=>"群聊",
+        'notAuth'=>'你没有操作权限,只有群主和群管理员才可以修改!',
+        'userLimit'=>'人数不能超过{:userMax}人!',
+        'inviteLimit'=>'单次邀请人数不能超过{:limit}人!',
+        'invite'=>"{:username}邀请你加入群聊",
+        'removeUser'=>"您已被移出群聊!",
+        'notCustom'=>"您不是本群成员,无权发送消息!",
+        'add'=>"{:username}创建了群聊",
+        'join'=>"{:username}加入了群聊",
+        'atLeast'=>"请至少选择两人!",
+        'alreadyJoin'=>'您已经加入该群!',
+        'exist'=>"群聊不存在",
+        'notice'=>"群公告",
+        'all'=>"所有人",
+        'noSpeak'=>"您已被禁言,恢复时间为:{:time}",
+        'notSpeak'=>"群聊已禁言!",
+        'limit'=>"您已被限制创建群聊!",
+    ],
+    'user'=>[
+        'exist'=>"用户不存在",
+        'codeErr'=>'验证码不正确!',
+        'newCodeErr'=>'新验证码不正确!',
+        'passErr'=>"原密码不正确!",
+        'already'=>"账户已存在",
+        'registerOk'=>"注册成功",
+        'loginOk'=>"登陆成功",
+        'tokenFailure'=>"TOKEN已失效!",
+        'forbid'=>'您的账号已被禁用',
+        'passError'=>'密码错误',
+        'logoutOk'=>'退出成功!',
+        'closeRegister'=>'当前系统已关闭注册功能!',
+        'inviteCode'=>'邀请码已失效!',
+        'accountVerify'=>'账户必须为手机号或者邮箱',
+        'waitMinute'=>"请一分钟后再试!",
+        "loginAccount"=>"登录账户",
+        "registerAccount"=>"注册账户",
+        "editPass"=>"修改密码",
+        "editAccount"=>"修改账户",
+        'loginError' => '登陆信息有误 请重新登录',
+        'mustToken' => '请先登陆系统',
+        'blacklist' => '登陆已失效 请重新登陆',
+        'expired' => '登陆已过期 请重新登陆',
+        'notOwn' =>"客服不能为他本人",
+        'loginLimit' =>"您的密码错误次数过多,请稍后再试!",
+        'registerLimit'=>"请{:time}分钟后再注册!",
+    ],
+    'im'=>[
+        'forbidChat'=>"目前禁止用户私聊!",
+        'notFriend'=>"您不在TA的好友列表,不能发消息!",
+        'friendNot'=>"TA还不是您的好友,不能发消息!",
+        'forwardLimit'=>"请选择转发的用户或者数量不操作{:count}个!",
+        'exist'=>"消息不存在",
+        'forwardRule'=>"由于规则限制,转发失败{:count}条!",
+        'forwardOk'=>'转发成功',
+        'you'=>'你',
+        'other'=>'对方',
+        'redoLimitTime'=>"超过{:time}分钟不能撤回!",
+        'redo'=>"撤回了一条消息",
+        'manageRedo'=>'被(管理员)撤回了一条消息',
+        'msgContentLimit'=>'你发送的消息长度太长了!',
+        'sendTimeLimit'=>'消息发送时间间隔为 {:time} 秒!',
+        'forbidMsg'=>'[该消息涉嫌违规,已被屏蔽]',
+    ],
+    'webRtc'=>[
+        'cancel'=>'已取消通话',
+        'refuse'=>'已拒绝',
+        'notConnected'=>'未接通',
+        'duration'=>'通话时长:{:time}',
+        'busy'=>'忙线中',
+        'other'=>'其他端已操作',
+        'video'=>'视频通话',
+        'audio'=>'语音通话',
+        'answer'=>'接听通话请求',
+        'exchange'=>'数据交换中',
+        'fail'=>'通话失败',
+    ],
+    'email'=>[
+        'input'=>'请输入正确的邮箱',
+        'testTitle'=>"测试邮件",
+        'testContent'=>'这是一封测试邮件,当您收到之后表明您的所有配置都是正确的!',
+    ],
+    'task'=>[
+        'schedule' => '计划任务',
+        'queue' => '消息队列',
+        'worker' => '消息推送',
+        'clearStd' => '清理日志',
+        'null'=>"未知任务",
+        'winRun'=>"windows启动请运行根目录下的:start_for_win.bat",
+        'alreadyRun'=>"进程已启动",
+        'startOk'=>"启动成功",
+        'startFail'=>"启动失败",
+        'notRun'=>"进程未启动",
+        'logExist'=>"日志不存在",
+    ],
+    'file'=>[
+        'preview'=>"预览文件",
+        'browserDown'=>"请使用浏览器下载",
+        'exist'=>"文件不存在",
+        'uploadLimit'=>"文件大小不能超过{:size}MB",
+        'typeNotSupport'=>"文件格式不支持",
+        'uploadOk'=>"上传成功"
+    ],
+    'scan'=>[
+        'failure'=>'二维码已失效'
+    ]
+];

+ 104 - 0
app/manage/controller/Config.php

@@ -0,0 +1,104 @@
+<?php
+/**
+ * Created by PhpStorm
+ * User Julyssn
+ * Date 2022/12/14 17:24
+ */
+
+
+namespace app\manage\controller;
+
+
+use app\BaseController;
+use app\manage\model\{Config as Conf};
+use think\facade\Cache;
+class Config extends BaseController
+{
+    /**
+     * 获取单个配置
+     * @return \think\response\Json
+     */
+    public function getInfo()
+    {
+        $name=$this->request->param('name');
+        $data = Conf::where(['name'=>$name])->value('value');
+        return success('', $data);
+    }
+
+    /**
+     * 获取配置
+     * @return \think\response\Json
+     */
+    public function getAllConfig()
+    {
+        $name=['sysInfo','chatInfo','smtp','fileUpload','compass'];
+        $list = Conf::where(['name'=>$name])->select();
+        return success('', $list);
+    }
+
+    /**
+     * 修改配置
+     * @return \think\response\Json
+     */
+    public function setConfig()
+    {
+        $name = $this->request->param('name');
+        $value = $this->request->param('value');
+        if(Conf::where(['name'=>$name])->find()){
+            Conf::where(['name'=>$name])->update(['value'=>$value]);
+        }else{
+            Conf::create(['name'=>$name,'value'=>$value]);
+        }
+        if($name=='fileUpload'){
+            updateEnv('driver',$value['disk']);
+            updateEnv('own',$value['preview']);
+            foreach ($value['aliyun'] as $k=>$v){
+                if($v){
+                    updateEnv('aliyun_'.$k,$v);
+                }
+            }
+            foreach ($value['qiniu'] as $k=>$v){
+                if($v){
+                    updateEnv('qiniu_'.$k,$v);
+                }
+            }
+            foreach ($value['qcloud'] as $k=>$v){
+                if($v){
+                    updateEnv('qcloud_'.$k,$v);
+                }
+            }
+        }else{
+            // 更新系统缓存
+            $systemInfo=Conf::getSystemInfo(true);
+            // 向所有人推送新的设置
+            wsSendMsg(0,'updateConfig',$systemInfo);
+        }
+        return success(lang('system.editOk'));
+    }
+
+    /**
+     * 获取邀请链接
+     * @return \think\response\Json
+     */
+    public function getInviteLink(){
+        $uid=$this->userInfo['user_id'];
+        // 邀请码仅两天有效
+        $code=\utils\Str::random(8);
+        Cache::set($code,$uid,172800);
+        $url=getMainHost().'/index.html/#/register?inviteCode='.$code;
+        return success('',$url);
+    }
+
+    // 发送测试邮件
+    public function sendTestEmail(){
+        $email=$this->request->param('email');
+        if(!$email || !(\utils\Regular::is_email($email))){
+            return warning(lang('email.input'));
+        }
+        $conf=Conf::where(['name'=>'smtp'])->value('value');
+        $mail=new \mail\Mail($conf);
+        $mail->sendEmail([$email],lang('email.testTitle'),lang('email.testContent'));
+        return success(lang('system.sendOk'));
+
+    } 
+}

+ 165 - 0
app/manage/controller/Group.php

@@ -0,0 +1,165 @@
+<?php
+/**
+ * Created by PhpStorm
+ * User raingad@foxmail.com
+ * Date 2022/12/14 17:24
+ */
+namespace app\manage\controller;
+use app\BaseController;
+use app\enterprise\model\{User as UserModel,GroupUser,Group as GroupModel};
+use think\facade\Db;
+
+class Group extends BaseController
+{
+    // 获取群聊列表
+    public function index()
+    {
+        $map = [];
+        $model=new GroupModel();
+        $param = $this->request->param();
+        //搜索关键词
+        if ($keyword = $this->request->param('keywords')) {
+            $model = $model->whereLike('name|name_py', '%' . $keyword . '%');
+        }
+        // 排序
+        $order='group_id DESC';
+        if ($param['order_field'] ?? '') {
+            $order = orderBy($param['order_field'],$param['order_type'] ?? 1);
+        }
+        $list = $this->paginate($model->where($map)->order($order));
+        if ($list) {
+            $data = $list->toArray()['data'];
+            $userList=UserModel::matchUser($data,true,'owner_id',120);
+            foreach($data as $k=>$v){
+                $data[$k]['avatar']=avatarUrl($v['avatar'],$v['name'],$v['group_id'],120,1);
+                $data[$k]['owner_id_info']=$userList[$v['owner_id']] ?? [];
+            }
+        }
+        return success('', $data, $list->total(), $list->currentPage());
+    }
+
+    // 更换群主
+    public function changeOwner()
+    {
+        $group_id = $this->request->param('group_id');
+        $user_id = $this->request->param('user_id');
+        $group=GroupModel::where('group_id',$group_id)->find();
+        if(!$group){
+            return warning(lang('group.exist'));
+        }
+        $user=UserModel::where('user_id',$user_id)->find();
+        if(!$user){
+            return warning(lang('user.exist'));
+        }
+        Db::startTrans();
+        try{
+            GroupUser::where('group_id',$group_id)->where('user_id',$user_id)->update(['role'=>1]);
+            GroupUser::where('group_id',$group_id)->where('user_id',$group->owner_id)->update(['role'=>3]);
+            $group->owner_id=$user_id;
+            $group->save();
+            wsSendMsg($group_id,"changeOwner",['group_id'=>'group-'.$group_id,'user_id'=>$user_id],1);
+            Db::commit();
+            return success('');
+        }catch (\Exception $e){
+            Db::rollback();
+            return warning('');
+        }
+    }
+
+    // 解散群聊
+    public function del()
+    {
+        $group_id = $this->request->param('group_id');
+        $group=GroupModel::where('group_id',$group_id)->find();
+        if(!$group){
+            return warning(lang('group.exist'));
+        }
+        Db::startTrans();
+        try{
+            // 删除团队成员
+            GroupUser::where('group_id',$group_id)->delete();
+            // 删除团队
+            GroupModel::destroy($group_id);
+            wsSendMsg($group_id,"removeGroup",['group_id'=>'group-'.$group_id],1);
+            Db::commit();
+            return success('');
+        }catch (\Exception $e){
+            Db::rollback();
+            return warning('');
+        }
+    }
+
+    // 添加群成员
+    public function addGroupUser(){
+        $param = $this->request->param();
+        $uid=$this->userInfo['user_id'];
+        $group_id = $param['group_id'];
+        $group=GroupModel::where('group_id',$group_id)->find();
+        if(!$group){
+            return warning(lang('group.exist'));
+        }
+        $user_ids=$param['user_ids'];
+        $data=[];
+        try{
+            foreach($user_ids as $k=>$v){
+                $data[]=[
+                    'group_id'=>$group_id,
+                    'user_id'=>$v,
+                    'role'=>3,
+                    'invite_id'=>$uid
+                ];
+            }
+            $groupUser=new GroupUser;
+            $groupUser->saveAll($data);
+            queuePush(['action'=>'createAvatar','group_id'=>$group_id]);
+            return success(lang('system.addOk'));
+        }catch(\Exception $e){
+                return error($e->getMessage());
+        }
+        
+    }
+
+    // 删除群成员
+    public function delGroupUser(){
+        $param = $this->request->param();
+        $group_id = $param['group_id'];
+        $group=GroupModel::where('group_id',$group_id)->find();
+        if(!$group){
+            return warning(lang('group.exist'));
+        }
+        $user_id=$param['user_id'];
+        $groupUser=GroupUser::where(['group_id'=>$group_id,'user_id'=>$user_id])->find();
+        if($groupUser){
+            $groupUser->delete();
+            wsSendMsg($group_id,"removeUser",['group_id'=>'group-'.$group_id],1);
+            return success('');
+        }else{
+            return warning('');
+        }
+        
+    }
+
+    // 设置管理员
+    public function setManager(){
+       $param = $this->request->param();
+       $group_id = $param['group_id'];
+        $group=GroupModel::where('group_id',$group_id)->find();
+        if(!$group){
+            return warning(lang('group.exist'));
+        }
+       $user_id=$param['user_id'];
+       $role=$param['role'];
+       $groupUser=GroupUser::where(['group_id'=>$group_id,'user_id'=>$user_id])->find();
+       if($groupUser){
+          $groupUser->role=$role;
+          $groupUser->save();
+          wsSendMsg($group_id,"setManager",['group_id'=>'group-'.$group_id],1);
+          return success('');
+       }else{
+          return warning('');
+       }
+       
+    }
+
+
+}

+ 126 - 0
app/manage/controller/Index.php

@@ -0,0 +1,126 @@
+<?php
+/**
+ * Created by PhpStorm
+ * User Julyssn
+ * Date 2022/12/14 17:24
+ */
+
+
+namespace app\manage\controller;
+
+
+use app\BaseController;
+use app\enterprise\model\{Message,User,Group,File};
+use GatewayClient\Gateway;
+class Index extends BaseController
+{
+    // 超级管理员专属功能
+    public function index(){
+        Gateway::$registerAddress = config('gateway.registerAddress');
+        $client_id=$this->request->param('client_id','');
+        $is_join=0;
+        if($client_id){
+            Gateway::joinGroup($client_id, 'admin-manage');
+             $is_join=1;
+        }
+        $data=[
+            'userCount'=>User::where(['status'=>1])->count(),
+            'groupCount'=>Group::where(['status'=>1])->count(),
+            'messageCount'=>Message::where(['status'=>1])->where([['type', 'not in', ['event','admin_notice','webrtc']]])->count(),
+            'fileCount'=>File::where(['status'=>1])->count(),
+            'onlineCount'=>Gateway::getAllUidCount() ?? 0,
+            'clientCount'=>Gateway::getAllClientCount() ?? 0,
+            'isJoin'=>$is_join,
+        ];
+        return success('', $data);
+    }
+
+    // 清理消息
+    public function clearMessage(){
+        if($this->userInfo['user_id']!=1){
+            return warning('system.notAuth');
+        }
+        Message::where(['status'=>1])->delete();
+        return success('system.clearOk');
+    }
+
+    // 公告列表
+    public function noticeList(){
+        $model=new Message();
+        // 排序
+        $order='msg_id DESC';
+        $map=['chat_identify'=>"admin_notice"];
+        $list = $this->paginate($model->where($map)->order($order));
+        if ($list) {
+            $data = $list->toArray()['data'];
+            foreach($data as $k=>$v){
+                $data[$k]['title']=$v['extends']['title'];
+            }
+        }
+        return success('', $data, $list->total(), $list->currentPage());
+    }
+
+    // 删除公告
+    public function delNotice(){
+        $param=$this->request->param();
+        $msgId=$param['id'] ?:0;
+        $map=['msg_id'=>$msgId];
+        $message=Message::where($map)->find();
+        if($message){
+            Message::where($map)->delete();
+        }
+        return success('');
+    }
+
+    // 发布公告
+    public function publishNotice(){
+        $userInfo=$this->userInfo;
+        if($userInfo['user_id']!=1){
+            return warning('system.notAuth');
+        }
+        $param=$this->request->param();
+        $msgId=$param['msgId'] ?? 0;
+        $content="<h4>".$param['title']."</h4><br><p>".$param['content']."</p>";
+        $data=[
+            'from_user'=>$userInfo['user_id'],
+            'to_user'=>0,
+            'content'=>str_encipher($content,true),
+            'chat_identify'=>'admin_notice',
+            'create_time'=>time(),
+            'type'=>'text',
+            'is_group'=>2,
+            'is_read'=>1,
+            'is_top'=>0,
+            'is_notice'=>1,
+            'at'=>[],
+            'pid'=>0,
+            'extends'=>['title'=>$param['title'],'notice'=>$param['content']],
+        ];
+        if($msgId){
+            Message::where(['msg_id'=>$msgId])->update([
+                'content'=>$data['content'],
+                'extends'=>$data['extends'],
+            ]);
+        }else{
+            $data['id']=\utils\Str::getUuid();
+            $message=new Message();
+            $message->save($data);
+            $msgId=$message->msg_id;
+        }
+        $msgInfo=$data;
+        $msgInfo['status']='successd';
+        $msgInfo['msg_id']=$msgId;
+        $msgInfo['user_id']=$userInfo['user_id'];
+        $msgInfo['sendTime']=time()*1000;
+        $msgInfo['toContactId']='admin_notice';
+        $msgInfo['to_user']='admin_notice';
+        $msgInfo['content']=$param['title'];
+        $msgInfo['fromUser']=[
+            'id'=>$userInfo['user_id'],
+            'avatar'=>avatarUrl($userInfo['avatar'],$userInfo['realname'],$userInfo['user_id'],120),
+            'displayName'=>$userInfo['realname']
+        ];
+        wsSendMsg(0,'simple',$msgInfo,1);
+        return success('');
+    }
+}

+ 210 - 0
app/manage/controller/Message.php

@@ -0,0 +1,210 @@
+<?php
+/**
+ * User raingad
+ * Date 2024/11/17 17:24
+ */
+
+
+namespace app\manage\controller;
+
+
+use app\BaseController;
+use app\enterprise\model\{Message as MessageModel,User,Friend,Group};
+use think\facade\Db;
+class Message extends BaseController
+{
+    protected $fileType = ['file', 'image','video','voice'];
+    // 获取聊天记录
+    public function index()
+    {
+        $param = $this->request->param();
+        $user_id=$param['user_id'] ?? 0;
+        $toContactId=$param['toContactId'] ?? 0;
+        $is_group=($param['is_group'] ?? 0) ? $param['is_group']-1 : -1;
+        $map = [ 'status' => 1];
+        if($user_id){
+            if(!$toContactId){
+                return warning(lang('system.parameterError'));
+            }
+            $chat_identify=chat_identify($param['user_id'],$param['toContactId']);
+            $map['chat_identify'] = $chat_identify;
+        }
+        if($is_group>=0){
+            $map['is_group']=$is_group;
+        }
+        $type = isset($param['type']) ? $param['type'] : '';
+        $where = [];
+        if ($type && $type != "all") {
+            $map['type'] = $type;
+        } else {
+            $where[] = ['type', 'not in', ['event','admin_notice','webrtc']];
+        }
+        $keywords = isset($param['keywords']) ? $param['keywords'] : '';
+        if ($keywords && in_array($type, ['text', 'all'])) {
+            $where[] = ['content', 'like', '%' . $keywords . '%'];
+            $where[] = ['type', '=', 'text'];
+        }
+        $listRows = $param['limit'] ?: 20;
+        $pageSize = $param['page'] ?: 1;
+        $last_id = $param['last_id'] ?? 0;
+        if($last_id){
+            $where[]=['msg_id','<',$last_id];
+        }
+        $list = MessageModel::getList($map, $where, 'msg_id desc', $listRows, $pageSize);
+        $data = $this->recombileMsg($list);
+        return success('', $data, $list->total(),$list->currentPage());
+    }
+
+    protected function recombileMsg($list,$isPagination=true)
+    {
+        $data = [];
+        if ($list) {
+            $listData = $isPagination ? $list->toArray()['data'] : $list;
+            $userList = User::matchUser($listData, true, 'from_user', 120);
+            foreach ($listData as $k => $v) {
+                $content = str_encipher($v['content'],false);
+                $preview = '';
+                $ext='';
+                if (in_array($v['type'], $this->fileType)) {
+                    $content = getFileUrl($content);
+                    $preview = previewUrl($content);
+                    $ext=getExtUrl($content);
+                }
+                $fromUser = $userList[$v['from_user']];
+                $toContactId=$v['is_group'] ==1 ?  'group-'.$v['to_user'] : $v['to_user'];
+                $atList=($v['at'] ?? null) ? explode(',',$v['at']): [];
+                if($v['is_group']==0){
+                    $toUser=User::where(['user_id'=>$v['to_user']])->field(User::$defaultField)->find() ?? [];
+                    if($toUser){
+                        $toUser=[
+                            'name'=>$toUser['realname']
+                        ];
+                    }
+                    
+                }else{
+                    $toUser=Group::where(['group_id'=>$v['to_user']])->find();
+                    if($toUser){
+                        $toUser=[
+                            'name'=>$toUser['name']
+                        ];
+                    }
+                }
+                $data[] = [
+                    'msg_id' => $v['msg_id'],
+                    'id' => $v['id'],
+                    'status' => "succeed",
+                    'type' => $v['type'],
+                    'sendTime' => $v['create_time'] * 1000,
+                    'create_time' => is_string($v['create_time']) ? $v['create_time'] : date('Y-m-d H:i:s',$v['create_time']),
+                    'content' => $content,
+                    'preview' => $preview,
+                    'download' => $v['file_id'] ? getMainHost().'/filedown/'.encryptIds($v['file_id']) : '',
+                    'is_read' => $v['is_read'],
+                    'is_group' => $v['is_group'],
+                    'at' => $atList,
+                    'toContactId' => $toContactId,
+                    'from_user' => $v['from_user'],
+                    'file_id' => $v['file_id'],
+                    'file_cate' => $v['file_cate'],
+                    'fileName' => $v['file_name'],
+                    'fileSize' => $v['file_size'],
+                    'fromUser' => $fromUser,
+                    'toUser' => $toUser,
+                    'extUrl'=>$ext,
+                    'extends'=>is_string($v['extends'])?json_decode($v['extends'],true) : $v['extends']
+                ];
+            }
+        }
+        return $data;
+    }
+
+    // 获取某个联系人的好友列表
+    public function getContacts(){
+        $param = $this->request->param();
+        $user_id=$param['user_id'] ?? 0;
+        if(!$user_id){
+            return warning(lang('system.parameterError'));
+        }
+        $config=$this->globalConfig;
+        $listRows = $param['limit'] ?: 20;
+        $pageSize = $param['page'] ?: 1;
+        $keywords = $param['keywords'] ?: '';
+        $where=[['status','=',1]];
+        if($keywords){
+            $where[] = ['realname', 'like', '%' . $keywords . '%'];
+        }
+        $hasConvo=$param['hasConvo'] ?? 0;
+        if($hasConvo){
+            // 查询最近的联系人
+            $map1 = [['to_user', '=', $user_id], ['is_last', '=', 1], ['is_group', '=', 0]];
+            $map2 = [['from_user', '=', $user_id], ['is_last', '=', 1], ['is_group', '=', 0]];
+            $msgField = 'from_user,to_user,content as lastContent,create_time as lastSendTime,chat_identify,type,del_user';
+            $lasMsgList = Db::name('message')
+                ->field($msgField)
+                ->whereOr([$map1, $map2])
+                ->order('create_time desc')
+                ->select();
+            $ids1=\utils\Arr::arrayToString($lasMsgList,'from_user',false);
+            $ids2=\utils\Arr::arrayToString($lasMsgList,'to_user',false);
+            $ids=array_merge($ids1,$ids2);
+            $userList = array_diff($ids, [$user_id]);
+            $where[]=['user_id','in',$userList];
+        }else{
+             // 如果是社区模式,就只查询的好友,如果是企业模式,就查询所有用户
+            if($config['sysInfo']['runMode']==1){
+                $where[]=['user_id','<>',$user_id];
+            }else{
+                $friendList = Friend::getFriend(['create_user' => $user_id,'status'=>1]);
+                $userList = array_keys($friendList);
+                $where[]=['user_id','in',$userList];
+            }
+        }
+        
+        $list = User::where($where)->field(User::$defaultField)->paginate(['list_rows'=>$listRows,'page'=>$pageSize]);
+        $data=[];
+        if($list){
+            $data=$list->toArray()['data'];
+            foreach ($data as $k => $v) {
+                $data[$k]['avatar'] = avatarUrl($v['avatar'], $v['realname'], $v['user_id'], 120);
+                $data[$k]['id'] = $v['user_id'];
+            }
+        }
+        return success('',$data,$list->total(),$list->currentPage());
+    }
+
+    // 消息处理
+    public function dealMsg(){
+        $param = $this->request->param();
+        $id = $param['id'];
+        $message = MessageModel::where(['id' => $id])->find();
+        if ($message) {
+            $dealType=$param['dealType'] ?? 0;
+            $content=$message['content'] ?? '';
+            if($dealType==1){
+                MessageModel::where(['id' => $id])->delete();
+                 // 如果是最后一条消息,需要将上一条设置为最后一条
+                if($message['is_last']){
+                    MessageModel::where(['chat_identify'=>$message['chat_identify']])->order('msg_id desc')->limit(1)->update(['is_last'=>1]);
+                }
+                $action='delMessage';
+            }else{
+                $content=str_encipher(lang('im.forbidMsg'),true);
+                MessageModel::where(['id' => $id])->update(['content'=>$content,'type'=>'text']);
+                $action='updateMessage';
+            }
+            $toContactId = $message['to_user'];
+            if ($message['is_group'] == 1) {
+                $toContactId = explode('-', $message['chat_identify'])[1];
+            }
+            $data=[
+                'id'=>$message['id'],
+                'type'=>"text",
+                'content'=>str_encipher($content,false),
+            ];
+            wsSendMsg($toContactId, $action, $data, $message['is_group']); 
+            return success('');
+        } else {
+            return warning(lang('im.exist'));
+        }
+    }
+}

+ 190 - 0
app/manage/controller/Task.php

@@ -0,0 +1,190 @@
+<?php
+/**
+ * Created by PhpStorm
+ * User Julyssn
+ * Date 2022/12/14 17:24
+ */
+
+
+namespace app\manage\controller;
+
+
+use app\BaseController;
+use easyTask\Terminal;
+use think\App;
+use think\facade\Console;
+use think\Response;
+
+class Task extends BaseController
+{
+    /**
+     * 项目根目录
+     * @var string
+     */
+    protected $rootPath;
+
+    protected $taskNames = [];
+
+    public function __construct(App $app)
+    {
+        parent::__construct($app);
+
+        $this->rootPath = root_path();
+        chdir($this->rootPath);
+        $this->taskNames = [
+            'schedule' => lang('task.schedule'),
+            'queue' => lang('task.queue'),
+            'worker' => lang('task.worker'),
+            'clearStd' => lang('task.clearStd'),
+        ];
+    }
+
+    /**
+     * 任务列表
+     * @return Response
+     */
+    public function getTaskList()
+    {
+        $data = $this->taskMsg();
+
+        if (!count($data)) {
+            return warning('');
+        }
+
+        foreach ($data as &$datum) {
+            $expName = explode('_', $datum['name']);
+
+            $datum['remark'] = $this->taskNames[$expName[count($expName) - 1]] ?? lang('task.null');
+        }
+        unset($datum);
+        return success('', $data);
+    }
+
+    /**
+     * 启动全部进程
+     * @return Response
+     */
+    public function startTask()
+    {
+        if(strpos(strtolower(PHP_OS), 'win') === 0)
+        {
+            return warning(lang('task.winRun'));
+        }
+
+        if (count($this->taskMsg())) {
+            return warning(lang('task.alreadyRun'));
+        }
+
+        // 启动
+        $out = Terminal::instance(2)->exec('php think task start');
+        if (!count($this->analysisMsg($out))) {
+            return warning(lang('task.startFail'));
+        }
+
+        return success(lang('task.startOk'));
+    }
+
+    /**
+     * 强制停止全部进程
+     * @return Response
+     */
+    public function stopTask()
+    {
+        if (!count($this->taskMsg())) {
+            return warning(lang('task.notRun'));
+        }
+
+        // 强制停止
+        Terminal::instance(2)->exec('php think task stop force');
+
+        return success('');
+    }
+
+    /**
+     * 获取单个任务日志
+     * @return Response
+     */
+    public function getTaskLog()
+    {
+        $name = $this->request->param('name');
+
+        $path = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'easy_task' . DIRECTORY_SEPARATOR . 'Std' . DIRECTORY_SEPARATOR;
+
+        if (!file_exists($path . 'exec_' . $name . '.std')) {
+            $expName = explode('_', $name);
+            $name    = $expName[count($expName) - 1];
+            if (!file_exists($path . 'exec_' . $name . '.std')) {
+                return warning(lang('task.logExist'));
+            }
+        }
+
+        return success('', file_get_contents($path . 'exec_' . $name . '.std'));
+    }
+
+    /**
+     * 清理单个任务日志
+     * @return Response
+     */
+    public function clearTaskLog()
+    {
+        $name = $this->request->param('name');
+
+        $path = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'easy_task' . DIRECTORY_SEPARATOR . 'Std' . DIRECTORY_SEPARATOR;
+
+        if (!file_exists($path . 'exec_' . $name . '.std')) {
+            $expName = explode('_', $name);
+            $name    = $expName[count($expName) - 1];
+            if (!file_exists($path . 'exec_' . $name . '.std')) {
+                return warning(lang('task.logExist'));
+            }
+        }
+
+        file_put_contents($path . 'exec_' . $name . '.std', '');
+        return success('');
+    }
+
+
+    /**
+     * 获取运行状态
+     * @return array
+     */
+    private function taskMsg()
+    {
+        $out = Terminal::instance(2)->exec('php think task status');
+        return $this->analysisMsg($out);
+    }
+
+    /**
+     * 解析数据
+     * @param string $out 带解析数据
+     * @return array
+     */
+    private function analysisMsg(string $out)
+    {
+        $re = '/│ *([\w+]+) *│ *([\w+]+)[ ]*│ *([\w+]+|[0-9- :]+) *│ *([\w+]+) *│ *([\w+]+) *│ *([\w+]+) *│/m';
+
+        preg_match_all($re, $out, $matches, PREG_SET_ORDER, 0);
+
+        if (!count($matches)) {
+            return [];
+        }
+
+        $data  = [];
+        $names = $matches[0];
+        unset($names[0]);
+        $names = array_values($names);
+        unset($matches[0]);
+
+        foreach ($matches as $match) {
+            $temp = [];
+            foreach ($match as $key => $item) {
+                if ($key !== 0) {
+                    $temp[$names[$key - 1]] = $item;
+                }
+            }
+            $data[] = $temp;
+        }
+
+        return $data;
+    }
+}

+ 204 - 0
app/manage/controller/User.php

@@ -0,0 +1,204 @@
+<?php
+/**
+ * Created by PhpStorm
+ * User raingad@foxmail.com
+ * Date 2022/12/14 17:24
+ */
+namespace app\manage\controller;
+use app\BaseController;
+use app\enterprise\model\{User as UserModel,GroupUser,Friend};
+use think\facade\Db;
+use think\facade\Cache;
+
+class User extends BaseController
+{
+    // 获取用户列表
+    public function index()
+    {
+        $map = [];
+        $model=new UserModel();
+        $param = $this->request->param();
+        //搜索关键词
+        if ($keyword = $this->request->param('keywords')) {
+            $model = $model->whereLike('realname|account|name_py|email', '%' . $keyword . '%');
+        }
+        // 排序
+        $order='user_id DESC';
+        if ($param['order_field'] ?? '') {
+            $order = orderBy($param['order_field'],$param['order_type'] ?? 1);
+        }
+        $list = $this->paginate($model->where($map)->order($order));
+        if ($list) {
+            $data = $list->toArray()['data'];
+            foreach($data as $k=>$v){
+                $data[$k]['avatar']=avatarUrl($v['avatar'],$v['realname'],$v['user_id'],120);
+                $data[$k]['location']=$v['last_login_ip'] ? implode(" ", \Ip::find($v['last_login_ip'])) : '--';
+                $data[$k]['reg_location']=$v['register_ip'] ? implode(" ", \Ip::find($v['register_ip'])) : '--';
+                $data[$k]['last_login_time']=$v['last_login_time'] ? date('Y-m-d H:i:s',$v['last_login_time']) : '--';
+                unset($data[$k]['password']);
+            }
+        }
+        return success('', $data, $list->total(), $list->currentPage());
+    }
+
+    // 添加用户
+    public function add()
+    {
+        try{
+            $data = $this->request->param();
+            $user=new UserModel();
+            $verify=$user->checkAccount($data);
+            if(!$verify){
+                return warning($user->getError());
+            }
+            $salt=\utils\Str::random(4);
+            $data['password'] = password_hash_tp($data['password'],$salt);
+            $data['salt'] =$salt;
+            $data['register_ip'] =$this->request->ip();
+            $data['name_py'] = pinyin_sentence($data['realname']);
+            $user->save($data);
+            $data['user_id']=$user->user_id;
+            return success(lang('system.addOk'), $data);
+        }catch (\Exception $e){
+            return error(lang('system.addFail'));
+        }
+    }
+
+    // 修改用户    
+    public function edit()
+    {
+        try{
+            $data = $this->request->param();
+            $user=new UserModel();
+            $verify=$user->checkAccount($data);
+            if(!$verify){
+                return warning($user->getError());
+            }
+            $user=UserModel::find($data['user_id']);
+            $user->account =$data['account'];
+            $user->realname =$data['realname'];
+            $user->email =$data['email'];
+            $user->remark=$data['remark'];
+            $user->sex =$data['sex'] ?? 0;
+            $user->friend_limit =$data['friend_limit'];
+            $user->group_limit =$data['group_limit'];
+            $csUid=$data['cs_uid'] ?? 0;
+            if($csUid && $csUid==$data['user_id']){
+                return warning(lang('user.notOwn'));
+            }
+            $user->cs_uid =$data['cs_uid'];
+            // 只有超管才能设置管理员
+            if($this->userInfo['user_id']==1){
+                $user->role =$data['role'];
+            }
+            $user->status =$data['status'];
+            $user->name_py= pinyin_sentence($data['realname']);
+            $user->save();
+            return success(lang('system.editOk'), $data);
+        }catch (\Exception $e){
+            return error(lang('system.editFail'));
+        }
+    }
+
+    // 删除用户
+    public function del()
+    {
+        $user_id = $this->request->param('user_id');
+        $user=UserModel::find($user_id);
+        if(!$user || $user->user_id==1){
+            return warning(lang('user.exist'));
+        }
+        Db::startTrans();
+        try{
+            // 删除其好友关系
+            Friend::where('create_user', $user_id)->whereOr(['friend_user_id'=>$user_id])->delete();
+            // 删除其群组关系
+            GroupUser::where('user_id', $user_id)->delete();
+            UserModel::destroy($user_id);
+            Db::commit();
+            return success(lang('system.delOk'));
+        }catch (\Exception $e){
+            Db::rollback();
+            return error($e->getMessage());
+        }
+    }
+
+    // 修改用户状态
+    public function setStatus()
+    {
+        $user_id = $this->request->param('user_id');
+        $user=UserModel::find($user_id);
+        if(!$user){
+            return warning(lang('user.exist'));
+        }
+        try{
+            $status = $this->request->param('status',0);
+            // 将禁用状态写入缓存
+            if(!$status){
+                Cache::set('forbidUser_'.$user_id,true,env('jwt.ttl',86400));
+            }
+            UserModel::where('user_id', $user_id)->update(['status'=>$status]);
+            return success(lang('system.editOk'));
+        }catch (\Exception $e){
+            return error(lang('system.editFail'));
+        }
+    }
+
+    // 获取用户信息
+    public function detail()
+    {
+        $user_id = $this->request->param('user_id');
+        $user=UserModel::find($user_id);
+        if(!$user){
+            return error(lang('user.exist'));
+        }
+        $user->avatar=avatarUrl($user->avatar,$user->realname,$user->user_id,120);
+        $location='';
+        if($user->last_login_ip){
+            $location=implode(" ", \Ip::find($user->last_login_ip));
+        }
+        $user->location=$location;
+        $user->password='';
+        return success('', $user);
+    }
+
+    // 设置用户角色
+    public function setRole()
+    {
+        $user_id = $this->request->param('user_id');
+        $user=UserModel::find($user_id);
+        if(!$user){
+            return warning(lang('user.exist'));
+        }
+        try{
+            $role = $this->request->param('role');
+            UserModel::where('user_id', $user_id)->update(['role'=>$role]);
+            return success('');
+        }catch (\Exception $e){
+            return error('');
+        }
+    }
+
+    // 修改密码
+    public function editPassword()
+    {
+        $user_id = $this->request->param('user_id');
+        $user=UserModel::find($user_id);
+        if(!$user){
+            return warning(lang('user.exist'));
+        }
+        try{
+            $password = $this->request->param('password','');
+            if($password){
+                $salt=$user->salt;
+                $user->password= password_hash_tp($password,$salt);
+                Cache::set('forbidUser_'.$user_id,true,env('jwt.ttl',86400));
+            }
+            $user->save();
+            return success('');
+        }catch (\Exception $e){
+            return error('');
+        }
+    }
+
+}

+ 6 - 0
app/manage/middleware.php

@@ -0,0 +1,6 @@
+<?php
+return [
+    "locale",
+"checkAuth",
+"manageAuth",
+];

+ 45 - 0
app/manage/model/Config.php

@@ -0,0 +1,45 @@
+<?php
+/**
+ * raingad IM [ThinkPHP6]
+ * @author xiekunyu <raingad@foxmail.com>
+ */
+namespace app\manage\model;
+
+use app\BaseModel;
+use think\facade\Cache;
+class Config extends BaseModel
+{
+    protected $json = ['value'];
+    protected $jsonAssoc = true;
+
+    // 获取系统配置信息
+    public static function getSystemInfo($update=false){
+        $name='systemInfo';
+        // $auth=request()->header('Authorization');
+        $nameFields=['sysInfo','fileUpload','chatInfo','compass'];
+        // 如果是登录状态才会返回chatINfo
+        // if($auth){
+        //     $name='all'.$name;
+        //     $nameFields[]="chatInfo";
+        // }
+        if(Cache::has($name) && !$update){
+            $systemInfo=Cache::get($name);
+        }else{
+            $systemInfo=[];
+            $conf=Config::where([['name','in',$nameFields]])->select()->toArray();
+            foreach($conf as $v){
+                $value=[];
+                if($v['name']=='fileUpload'){
+                    $value['size'] = $v['value']['size'];
+                    $value['preview'] = $v['value']['preview'];
+                    $value['fileExt'] = $v['value']['fileExt'];
+                }else{
+                    $value=$v['value'];
+                }
+                $systemInfo[$v['name']]=$value;
+            }
+            Cache::set($name,$systemInfo,7*86400);
+        }
+        return $systemInfo;
+    }
+}

+ 19 - 0
app/middleware.php

@@ -0,0 +1,19 @@
+<?php
+// 全局中间件定义文件
+return [
+      // 全局请求缓存
+
+    // 'think\middleware\CheckRequestCache',
+
+    // 多语言加载
+
+    'think\middleware\LoadLangPack',
+
+    // Session初始化
+
+    'think\middleware\SessionInit',
+
+    // 页面Trace调试
+
+    // 'think\middleware\TraceDebug',
+];

+ 9 - 0
app/provider.php

@@ -0,0 +1,9 @@
+<?php
+use app\ExceptionHandle;
+use app\Request;
+
+// 容器Provider定义文件
+return [
+    'think\Request'          => Request::class,
+    'think\exception\Handle' => ExceptionHandle::class,
+];

+ 9 - 0
app/service.php

@@ -0,0 +1,9 @@
+<?php
+
+use app\AppService;
+
+// 系统服务定义文件
+// 服务在完成全局初始化之后执行
+return [
+    AppService::class,
+];

+ 105 - 0
app/worker/Application.php

@@ -0,0 +1,105 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace app\worker;
+
+use think\App;
+use think\exception\Handle;
+use think\exception\HttpException;
+use Workerman\Connection\TcpConnection;
+use Workerman\Protocols\Http\Response;
+/**
+ * Worker应用对象
+ */
+class Application extends App
+{
+    /**
+     * 处理Worker请求
+     * @access public
+     * @param  \Workerman\Connection\TcpConnection   $connection
+     * @param  void
+     */
+    public function worker(TcpConnection $connection)
+    {
+        try {
+            $this->beginTime = microtime(true);
+            $this->beginMem  = memory_get_usage();
+            $this->db->clearQueryTimes();
+
+            $pathinfo = ltrim(strpos($_SERVER['REQUEST_URI'], '?') ? strstr($_SERVER['REQUEST_URI'], '?', true) : $_SERVER['REQUEST_URI'], '/');
+
+            $this->request
+                ->setPathinfo($pathinfo)
+                ->withInput($GLOBALS['HTTP_RAW_POST_DATA']);
+
+            while (ob_get_level() > 1) {
+                ob_end_clean();
+            }
+
+            ob_start();
+            $response = $this->http->run();
+            $content  = ob_get_clean();
+
+            ob_start();
+
+            $response->send();
+            $this->http->end($response);
+
+            $content .= ob_get_clean() ?: '';
+
+            $this->httpResponseCode($response->getCode());
+            $header=[];
+            foreach ($response->getHeader() as $name => $val) {
+                // 发送头部信息
+                $header[$name] =!is_null($val) ? $val : '';
+            }
+            if (strtolower($_SERVER['HTTP_CONNECTION']) === "keep-alive") {
+                $connection->send(new Response(200, $header, $content));
+            } else {
+                $connection->close(new Response(200, $header, $content));
+            }
+        } catch (HttpException | \Exception | \Throwable $e) {
+            $this->exception($connection, $e);
+        }
+    }
+
+    /**
+     * 是否运行在命令行下
+     * @return bool
+     */
+    public function runningInConsole(): bool
+    {
+        return false;
+    }
+
+    protected function httpResponseCode($code = 200)
+    {
+            new Response($code);
+    }
+
+    protected function exception($connection, $e)
+    {
+        if ($e instanceof \Exception) {
+            $handler = $this->make(Handle::class);
+            $handler->report($e);
+
+            $resp    = $handler->render($this->request, $e);
+            $content = $resp->getContent();
+            $code    = $resp->getCode();
+
+            $this->httpResponseCode(new Response($code, [], $content));
+            $connection->send($content);
+        } else {
+            $connection->send(new Response(500, [], $e->getMessage()));
+        }
+    }
+
+}

+ 181 - 0
app/worker/Events.php

@@ -0,0 +1,181 @@
+<?php
+/**
+ * This file is part of workerman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link http://www.workerman.net/
+ * @license http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+ namespace app\worker;
+/**
+ * 推送主逻辑
+ * 主要是处理 onMessage onClose 
+ */
+use GatewayWorker\Lib\Gateway;
+use app\worker\Application;
+use think\facade\Config;
+use Lcobucci\JWT\Builder;
+use Lcobucci\JWT\Parser;
+use thans\jwt\provider\JWT\Lcobucci;
+use utils\Aes;
+
+class Events
+{
+    // 使用TP框架
+    public static function onWorkerStart()
+    {
+        $app = new Application;
+        $app->initialize();
+    }
+
+    // 当有客户端连接时,将client_id返回,让mvc框架判断当前uid并执行绑定
+    public static function onConnect($client_id)
+    {
+        Gateway::sendToClient($client_id, json_encode(array(
+            'type'      => 'init',
+            'client_id' => $client_id
+        )));
+        self::onlineStatistics();
+    }
+    /**
+    * 有消息时
+    * @param int $client_id
+    * @param mixed $message
+    */
+   public static function onMessage($client_id, $message)
+   {
+        // 客户端传递的是json数据
+        $message_data = json_decode($message, true);
+        if(!$message_data)
+        {
+            return ;
+        }
+        
+        // 根据类型执行不同的业务
+        switch($message_data['type'])
+        {
+            // 客户端回应服务端的心跳
+            case 'pong':
+                // 三次心跳后如果没有登录则断开连接
+                $_SESSION['pong_times'] = ($_SESSION['pong_times'] ?? 0) + 1;
+                $user_id=$_SESSION['user_id'] ?? '';
+                $isQrLogin = $message_data['isQrLogin'] ?? 0;
+                // 二维码登录逻辑,每60秒
+                if($isQrLogin){
+                    // 生成新的二维码链接
+                    $domain=config('app.app_host');
+                    $appid=config('app.app_id');
+                    $token=urlencode(authcode($client_id,'ENCODE',$appid,100));
+                    $qrurl=rtrim($domain,'/') . '/scan/a/' . $token;
+                    Gateway::sendToClient($client_id, json_encode(array(
+                        'type' => 'codeLoginQr',
+                        'qrurl' => $qrurl,
+                    )));
+                }
+                // 如果是扫码登录则不做断开处理
+                if(!$user_id && $_SESSION['pong_times'] >= 3 && !$isQrLogin){
+                    $_SESSION['pong_times'] = 0;
+                    self::closeClient($client_id);
+                }
+                break;
+            case 'ping':
+                self::sendStatus($client_id);
+                break;
+            case 'bindUid':
+                self::auth($client_id,$message_data);
+                break;
+        }
+        return;
+   }
+
+   protected static function sendStatus($client_id){
+        $uid=$_SESSION['user_id'] ?? 0;
+        $multiport=false;
+        if($uid){
+            $arr=Gateway::getClientIdByUid($uid);
+            if(count($arr)>1){
+                $multiport=true;
+            }
+        }
+        Gateway::sendToClient($client_id, json_encode(array(
+            'type' => 'pong',
+            'multiport' => $multiport,
+        )));
+   }
+
+    //验证用户的真实性并绑定
+    protected static function auth($client_id, $msg){
+        $token=$msg['token'] ?? '';
+        $config   = Config::get('jwt');
+        $keys     = $config['secret'] ?: [
+            'public' => $config['public_key'],
+            'private' => $config['private_key'],
+            'password' => $config['password'],
+        ];
+        $provider = new Lcobucci(new Builder(), new Parser(), $config['algo'], $keys);
+        try {
+            $token=str_replace('bearer ','',$token);
+            $jwtData = $provider->decode((string)$token);
+        } catch (\Exception $exception) {
+            self::closeClient($client_id);
+        }
+
+        $userInfo = $jwtData['info']->getValue();
+        //解密token中的用户信息
+        $userInfo = Aes::decrypt($userInfo, config('app.aes_token_key'));
+        //解析json
+        $userInfo = (array)json_decode($userInfo, true);
+        if(!$userInfo){
+            self::closeClient($client_id);
+        }
+        $_SESSION['user_id']=$userInfo['user_id'];
+    
+        self::sendStatus($client_id);
+    }
+
+    //断开连接
+    protected static function closeClient($client_id){
+        self::onlineStatistics();
+        $_SESSION['user_id']=null;
+        Gateway::closeClient($client_id);
+    }
+
+    /**
+    * 当断开连接时
+    * @param int $client_id
+    */
+    public static function onClose($client_id)
+    {
+        $user_id=$_SESSION['user_id'] ?? '';
+        if($user_id){
+            Gateway::sendToAll(json_encode(array(
+            'type'      => 'isOnline',
+            'time' => time(),
+            'data' => ['id'=>$user_id,'is_online'=>0]
+        )));
+        }
+        self::onlineStatistics();
+    }
+
+    public static function onlineStatistics()
+    {
+       // 通知后台在线用户数和在线设备数
+        $data=[
+            'type'      => 'statistics',
+            'time' => time(),
+            'data' => [
+                'onlineCount'=>Gateway::getAllUidCount() ?? 0,
+                'clientCount'=>Gateway::getAllClientCount() ?? 0,
+            ] 
+        ];
+        Gateway::sendToGroup('admin-manage', json_encode($data));
+        Gateway::sendToUid(1, json_encode($data));
+    }
+  
+}

+ 201 - 0
app/worker/command/GatewayWorker.php

@@ -0,0 +1,201 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace app\worker\command;
+
+use GatewayWorker\BusinessWorker;
+use GatewayWorker\Gateway;
+use GatewayWorker\Register;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\input\Option;
+use think\console\Output;
+use think\facade\Config;
+use Workerman\Worker;
+
+/**
+ * Worker 命令行类
+ */
+class GatewayWorker extends Command
+{
+    public function configure()
+    {
+        $this->setName('worker:gateway')
+            ->addArgument('action', Argument::OPTIONAL, "start|stop|restart|reload|status|connections", 'start')
+            ->addOption('host', 'H', Option::VALUE_OPTIONAL, 'the host of workerman server.', null)
+            ->addOption('port', 'p', Option::VALUE_OPTIONAL, 'the port of workerman server.', null)
+            ->addOption('daemon', 'd', Option::VALUE_NONE, 'Run the workerman server in daemon mode.')
+            ->setDescription('GatewayWorker Server for ThinkPHP');
+    }
+
+    public function execute(Input $input, Output $output)
+    {
+        $action = $input->getArgument('action');
+
+        if (DIRECTORY_SEPARATOR !== '\\') {
+            if (!in_array($action, ['start', 'stop', 'reload', 'restart', 'status', 'connections'])) {
+                $output->writeln("Invalid argument action:{$action}, Expected start|stop|restart|reload|status|connections .");
+                exit(1);
+            }
+
+            global $argv;
+            array_shift($argv);
+            array_shift($argv);
+            array_unshift($argv, 'think', $action);
+        } else {
+            $output->writeln("GatewayWorker Not Support On Windows.");
+            exit(1);
+        }
+
+        if ('start' == $action) {
+            $output->writeln('Starting GatewayWorker server...');
+        }
+
+        $option = Config::get('gateway');
+
+        if ($input->hasOption('host')) {
+            $host = $input->getOption('host');
+        } else {
+            $host = !empty($option['host']) ? $option['host'] : '0.0.0.0';
+        }
+
+        if ($input->hasOption('port')) {
+            $port = $input->getOption('port');
+        } else {
+            $port = !empty($option['port']) ? $option['port'] : '2347';
+        }
+
+        $this->start($host, (int) $port, $option);
+    }
+
+    /**
+     * 启动
+     * @access public
+     * @param  string   $host 监听地址
+     * @param  integer  $port 监听端口
+     * @param  array    $option 参数
+     * @return void
+     */
+    public function start(string $host, int $port, array $option = [])
+    {
+        $registerAddress = !empty($option['registerAddress']) ? $option['registerAddress'] : '127.0.0.1:1236';
+
+        if (!empty($option['register_deploy'])) {
+            // 分布式部署的时候其它服务器可以关闭register服务
+            // 注意需要设置不同的lanIp
+            $this->register($registerAddress);
+        }
+
+        // 启动businessWorker
+        if (!empty($option['businessWorker_deploy'])) {
+            $this->businessWorker($registerAddress, $option['businessWorker'] ?? []);
+        }
+
+        // 启动gateway
+        if (!empty($option['gateway_deploy'])) {
+            $this->gateway($registerAddress, $host, $port, $option);
+        }
+
+        Worker::runAll();
+    }
+
+    /**
+     * 启动register
+     * @access public
+     * @param  string   $registerAddress
+     * @return void
+     */
+    public function register(string $registerAddress)
+    {
+        // 初始化register
+        new Register('text://' . $registerAddress);
+    }
+
+    /**
+     * 启动businessWorker
+     * @access public
+     * @param  string   $registerAddress registerAddress
+     * @param  array    $option 参数
+     * @return void
+     */
+    public function businessWorker(string $registerAddress, array $option = [])
+    {
+        // 初始化 bussinessWorker 进程
+        $worker = new BusinessWorker();
+
+        $this->option($worker, $option);
+
+        $worker->registerAddress = $registerAddress;
+    }
+
+    /**
+     * 启动gateway
+     * @access public
+     * @param  string  $registerAddress registerAddress
+     * @param  string  $host 服务地址
+     * @param  integer $port 监听端口
+     * @param  array   $option 参数
+     * @return void
+     */
+    public function gateway(string $registerAddress, string $host, int $port, array $option = [])
+    {
+        // 初始化 gateway 进程
+        if (!empty($option['socket'])) {
+            $socket = $option['socket'];
+            unset($option['socket']);
+        } else {
+            $protocol = !empty($option['protocol']) ? $option['protocol'] : 'websocket';
+            $socket   = $protocol . '://' . $host . ':' . $port;
+            unset($option['host'], $option['port'], $option['protocol']);
+        }
+
+        $gateway = new Gateway($socket, $option['context'] ?? []);
+
+        // 以下设置参数都可以在配置文件中重新定义覆盖
+        $gateway->name                 = 'Gateway';
+        $gateway->count                = 4;
+        $gateway->lanIp                = '127.0.0.1';
+        $gateway->startPort            = 2000;
+        $gateway->pingInterval         = 30;
+        $gateway->pingNotResponseLimit = 0;
+        $gateway->pingData             = '{"type":"ping"}';
+        $gateway->registerAddress      = $registerAddress;
+
+        // 全局静态属性设置
+        foreach ($option as $name => $val) {
+            if (in_array($name, ['stdoutFile', 'daemonize', 'pidFile', 'logFile'])) {
+                Worker::${$name} = $val;
+                unset($option[$name]);
+            }
+        }
+
+        $this->option($gateway, $option);
+    }
+
+    /**
+     * 设置参数
+     * @access protected
+     * @param  Worker $worker Worker对象
+     * @param  array  $option 参数
+     * @return void
+     */
+    protected function option(Worker $worker, array $option = [])
+    {
+        // 设置参数
+        if (!empty($option)) {
+            foreach ($option as $key => $val) {
+                $worker->$key = $val;
+            }
+        }
+    }
+
+}

+ 68 - 0
composer.json

@@ -0,0 +1,68 @@
+{
+    "name": "topthink/think",
+    "description": "the new thinkphp framework",
+    "type": "project",
+    "keywords": [
+        "framework",
+        "thinkphp",
+        "ORM"
+    ],
+    "homepage": "http://thinkphp.cn/",
+    "license": "Apache-2.0",
+    "authors": [{
+            "name": "liu21st",
+            "email": "liu21st@gmail.com"
+        },
+        {
+            "name": "yunwuxin",
+            "email": "448901948@qq.com"
+        }
+    ],
+    "require": {
+        "php": ">=7.1.0",
+        "topthink/framework": "6.0.15",
+        "topthink/think-orm": "^2.0",
+        "jasny/sso": "^0.3.0",
+        "xiaodi/think-pullword": "^1.0",
+        "topthink/think-view": "^1.0",
+        "aliyuncs/oss-sdk-php": "^2.3",
+        "tcwei/imglazyload": "^1.3",
+        "tcwei/imgsrc": "^2.0",
+        "topthink/think-captcha": "^3.0",
+        "alibabacloud/client": "^1.5",
+        "xiaodi/think-pinyin": "^1.0",
+        "workerman/workerman": "^4.0",
+        "workerman/gateway-worker": "^3.0",
+        "workerman/gatewayclient": "^3.0",
+        "topthink/think-multi-app": "^1.0",
+        "thans/thinkphp-filesystem-cloud": "^1.0",
+        "topthink/think-queue": "^3.0",
+        "yunwuxin/think-cron": "^3.0",
+        "swiftmailer/swiftmailer": "^6.0",
+        "thans/tp-jwt-auth": "^1.3",
+        "singka/singka-sms": "^1.6",
+        "topthink/think-api": "^1.0"
+    },
+    "require-dev": {
+        "symfony/var-dumper": "^4.2",
+        "topthink/think-trace": "^1.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "app\\": "app"
+        },
+        "psr-0": {
+            "": "extend/"
+        }
+    },
+    "config": {
+        "preferred-install": "dist",
+        "secure-http": false 
+    },
+    "scripts": {
+        "post-autoload-dump": [
+            "@php think service:discover",
+            "@php think vendor:publish"
+        ]
+    }
+}

+ 3548 - 0
composer.lock

@@ -0,0 +1,3548 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "674bc39f3462a2a4238e15002063fa0d",
+    "packages": [
+        {
+            "name": "adbario/php-dot-notation",
+            "version": "2.5.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/adbario/php-dot-notation.git",
+                "reference": "081e2cca50c84bfeeea2e3ef9b2c8d206d80ccae"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/adbario/php-dot-notation/zipball/081e2cca50c84bfeeea2e3ef9b2c8d206d80ccae",
+                "reference": "081e2cca50c84bfeeea2e3ef9b2c8d206d80ccae",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "php": "^5.5 || ^7.0 || ^8.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.8|^5.7|^6.6|^7.5|^8.5|^9.5",
+                "squizlabs/php_codesniffer": "^3.6"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/helpers.php"
+                ],
+                "psr-4": {
+                    "Adbar\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Riku Särkinen",
+                    "email": "riku@adbar.io"
+                }
+            ],
+            "description": "PHP dot notation access to arrays",
+            "homepage": "https://github.com/adbario/php-dot-notation",
+            "keywords": [
+                "ArrayAccess",
+                "dotnotation"
+            ],
+            "time": "2022-10-14T20:31:46+00:00"
+        },
+        {
+            "name": "alibabacloud/client",
+            "version": "1.5.32",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/aliyun/openapi-sdk-php-client.git",
+                "reference": "5bc6f6d660797dcee2c3aef29700ab41ee764f4d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/aliyun/openapi-sdk-php-client/zipball/5bc6f6d660797dcee2c3aef29700ab41ee764f4d",
+                "reference": "5bc6f6d660797dcee2c3aef29700ab41ee764f4d",
+                "shasum": ""
+            },
+            "require": {
+                "adbario/php-dot-notation": "^2.4.1",
+                "clagiordano/weblibs-configmanager": "^1.0",
+                "ext-curl": "*",
+                "ext-json": "*",
+                "ext-libxml": "*",
+                "ext-mbstring": "*",
+                "ext-openssl": "*",
+                "ext-simplexml": "*",
+                "ext-xmlwriter": "*",
+                "guzzlehttp/guzzle": "^6.3|^7.0",
+                "mtdowling/jmespath.php": "^2.5",
+                "php": ">=5.5"
+            },
+            "require-dev": {
+                "composer/composer": "^1.8",
+                "drupal/coder": "^8.3",
+                "ext-dom": "*",
+                "ext-pcre": "*",
+                "ext-sockets": "*",
+                "ext-spl": "*",
+                "league/climate": "^3.2.4",
+                "mikey179/vfsstream": "^1.6",
+                "monolog/monolog": "^1.24",
+                "phpunit/phpunit": "^5.7|^6.6|^7.5|^8.5|^9.5",
+                "psr/cache": "^1.0",
+                "symfony/dotenv": "^3.4",
+                "symfony/var-dumper": "^3.4"
+            },
+            "suggest": {
+                "ext-sockets": "To use client-side monitoring"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/Functions.php"
+                ],
+                "psr-4": {
+                    "AlibabaCloud\\Client\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "Alibaba Cloud SDK",
+                    "email": "sdk-team@alibabacloud.com",
+                    "homepage": "http://www.alibabacloud.com"
+                }
+            ],
+            "description": "Alibaba Cloud Client for PHP - Use Alibaba Cloud in your PHP project",
+            "homepage": "https://www.alibabacloud.com/",
+            "keywords": [
+                "alibaba",
+                "alibabacloud",
+                "aliyun",
+                "client",
+                "cloud",
+                "library",
+                "sdk",
+                "tool"
+            ],
+            "time": "2022-12-09T04:05:55+00:00"
+        },
+        {
+            "name": "aliyuncs/oss-sdk-php",
+            "version": "v2.6.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/aliyun/aliyun-oss-php-sdk.git",
+                "reference": "572d0f8e099e8630ae7139ed3fdedb926c7a760f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/aliyun/aliyun-oss-php-sdk/zipball/572d0f8e099e8630ae7139ed3fdedb926c7a760f",
+                "reference": "572d0f8e099e8630ae7139ed3fdedb926c7a760f",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "*",
+                "satooshi/php-coveralls": "*"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "OSS\\": "src/OSS"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Aliyuncs",
+                    "homepage": "http://www.aliyun.com"
+                }
+            ],
+            "description": "Aliyun OSS SDK for PHP",
+            "homepage": "http://www.aliyun.com/product/oss/",
+            "time": "2022-08-03T08:06:01+00:00"
+        },
+        {
+            "name": "clagiordano/weblibs-configmanager",
+            "version": "v1.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/clagiordano/weblibs-configmanager.git",
+                "reference": "5c8ebcc62782313b1278afe802b120d18c07a059"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/clagiordano/weblibs-configmanager/zipball/5c8ebcc62782313b1278afe802b120d18c07a059",
+                "reference": "5c8ebcc62782313b1278afe802b120d18c07a059",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4"
+            },
+            "require-dev": {
+                "clagiordano/phpunit-result-printer": "^1",
+                "phpunit/phpunit": "^4.8"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "clagiordano\\weblibs\\configmanager\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-3.0-or-later"
+            ],
+            "authors": [
+                {
+                    "name": "Claudio Giordano",
+                    "email": "claudio.giordano@autistici.org",
+                    "role": "Developer"
+                }
+            ],
+            "description": "weblibs-configmanager is a tool library for easily read and access to php config array file and direct read/write configuration file / object",
+            "keywords": [
+                "clagiordano",
+                "configuration",
+                "manager",
+                "tool",
+                "weblibs"
+            ],
+            "time": "2021-05-18T17:55:57+00:00"
+        },
+        {
+            "name": "desarrolla2/cache",
+            "version": "v2.1.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/desarrolla2/Cache.git",
+                "reference": "cbc42cae703e6f8cc9e17231f083304f0038318e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/desarrolla2/Cache/zipball/cbc42cae703e6f8cc9e17231f083304f0038318e",
+                "reference": "cbc42cae703e6f8cc9e17231f083304f0038318e",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4.0"
+            },
+            "require-dev": {
+                "predis/predis": "~1.0.0"
+            },
+            "suggest": {
+                "predis/predis": "Predis support"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Desarrolla2\\Cache\\": "src/",
+                    "Desarrolla2\\Test\\Cache\\": "test/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Daniel González",
+                    "homepage": "http://desarrolla2.com/"
+                }
+            ],
+            "description": "Provides an cache interface for several adapters Apc, Apcu, File, Mongo, Memcache, Memcached, Mysql, Mongo, Redis is supported. New adapters is comming!",
+            "homepage": "https://github.com/desarrolla2/Cache/",
+            "keywords": [
+                "apc",
+                "apcu",
+                "cache",
+                "file",
+                "memcache",
+                "memcached",
+                "mongo",
+                "mysql",
+                "redis"
+            ],
+            "time": "2023-05-04T14:59:36+00:00"
+        },
+        {
+            "name": "doctrine/lexer",
+            "version": "1.2.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/lexer.git",
+                "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229",
+                "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1 || ^8.0"
+            },
+            "require-dev": {
+                "doctrine/coding-standard": "^9.0",
+                "phpstan/phpstan": "^1.3",
+                "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
+                "vimeo/psalm": "^4.11"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Guilherme Blanco",
+                    "email": "guilhermeblanco@gmail.com"
+                },
+                {
+                    "name": "Roman Borschel",
+                    "email": "roman@code-factory.org"
+                },
+                {
+                    "name": "Johannes Schmitt",
+                    "email": "schmittjoh@gmail.com"
+                }
+            ],
+            "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
+            "homepage": "https://www.doctrine-project.org/projects/lexer.html",
+            "keywords": [
+                "annotations",
+                "docblock",
+                "lexer",
+                "parser",
+                "php"
+            ],
+            "time": "2022-02-28T11:07:21+00:00"
+        },
+        {
+            "name": "dragonmantank/cron-expression",
+            "version": "v3.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/dragonmantank/cron-expression.git",
+                "reference": "48212cdc0a79051d50d7fc2f0645c5a321caf926"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/48212cdc0a79051d50d7fc2f0645c5a321caf926",
+                "reference": "48212cdc0a79051d50d7fc2f0645c5a321caf926",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1|^8.0"
+            },
+            "replace": {
+                "mtdowling/cron-expression": "^1.0"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "^0.11|^0.12",
+                "phpunit/phpunit": "^7.0|^8.0|^9.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Cron\\": "src/Cron/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Chris Tankersley",
+                    "email": "chris@ctankersley.com",
+                    "homepage": "https://github.com/dragonmantank"
+                }
+            ],
+            "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
+            "keywords": [
+                "cron",
+                "schedule"
+            ],
+            "time": "2020-10-13T01:26:01+00:00"
+        },
+        {
+            "name": "egulias/email-validator",
+            "version": "2.1.25",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/egulias/EmailValidator.git",
+                "reference": "0dbf5d78455d4d6a41d186da50adc1122ec066f4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/0dbf5d78455d4d6a41d186da50adc1122ec066f4",
+                "reference": "0dbf5d78455d4d6a41d186da50adc1122ec066f4",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/lexer": "^1.0.1",
+                "php": ">=5.5",
+                "symfony/polyfill-intl-idn": "^1.10"
+            },
+            "require-dev": {
+                "dominicsayers/isemail": "^3.0.7",
+                "phpunit/phpunit": "^4.8.36|^7.5.15",
+                "satooshi/php-coveralls": "^1.0.1"
+            },
+            "suggest": {
+                "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Egulias\\EmailValidator\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Eduardo Gulias Davis"
+                }
+            ],
+            "description": "A library for validating emails against several RFCs",
+            "homepage": "https://github.com/egulias/EmailValidator",
+            "keywords": [
+                "email",
+                "emailvalidation",
+                "emailvalidator",
+                "validation",
+                "validator"
+            ],
+            "time": "2020-12-29T14:50:06+00:00"
+        },
+        {
+            "name": "guzzlehttp/command",
+            "version": "1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/command.git",
+                "reference": "2aaa2521a8f8269d6f5dfc13fe2af12c76921034"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/command/zipball/2aaa2521a8f8269d6f5dfc13fe2af12c76921034",
+                "reference": "2aaa2521a8f8269d6f5dfc13fe2af12c76921034",
+                "shasum": ""
+            },
+            "require": {
+                "guzzlehttp/guzzle": "^6.2",
+                "guzzlehttp/promises": "~1.3",
+                "guzzlehttp/psr7": "~1.0",
+                "php": ">=5.5.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.0|~5.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "0.9-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Command\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Jeremy Lindblom",
+                    "email": "jeremeamia@gmail.com",
+                    "homepage": "https://github.com/jeremeamia"
+                }
+            ],
+            "description": "Provides the foundation for building command-based web service clients",
+            "time": "2016-11-24T13:34:15+00:00"
+        },
+        {
+            "name": "guzzlehttp/guzzle",
+            "version": "6.5.8",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/guzzle.git",
+                "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a52f0440530b54fa079ce76e8c5d196a42cad981",
+                "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "guzzlehttp/promises": "^1.0",
+                "guzzlehttp/psr7": "^1.9",
+                "php": ">=5.5",
+                "symfony/polyfill-intl-idn": "^1.17"
+            },
+            "require-dev": {
+                "ext-curl": "*",
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
+                "psr/log": "^1.1"
+            },
+            "suggest": {
+                "psr/log": "Required for using the Log middleware"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "6.5-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/functions_include.php"
+                ],
+                "psr-4": {
+                    "GuzzleHttp\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Jeremy Lindblom",
+                    "email": "jeremeamia@gmail.com",
+                    "homepage": "https://github.com/jeremeamia"
+                },
+                {
+                    "name": "George Mponos",
+                    "email": "gmponos@gmail.com",
+                    "homepage": "https://github.com/gmponos"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "mark.sagikazar@gmail.com",
+                    "homepage": "https://github.com/sagikazarmark"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "Guzzle is a PHP HTTP client library",
+            "homepage": "http://guzzlephp.org/",
+            "keywords": [
+                "client",
+                "curl",
+                "framework",
+                "http",
+                "http client",
+                "rest",
+                "web service"
+            ],
+            "time": "2022-06-20T22:16:07+00:00"
+        },
+        {
+            "name": "guzzlehttp/guzzle-services",
+            "version": "1.1.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/guzzle-services.git",
+                "reference": "9e3abf20161cbf662d616cbb995f2811771759f7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/guzzle-services/zipball/9e3abf20161cbf662d616cbb995f2811771759f7",
+                "reference": "9e3abf20161cbf662d616cbb995f2811771759f7",
+                "shasum": ""
+            },
+            "require": {
+                "guzzlehttp/command": "~1.0",
+                "guzzlehttp/guzzle": "^6.2",
+                "php": ">=5.5"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.0"
+            },
+            "suggest": {
+                "gimler/guzzle-description-loader": "^0.0.4"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Command\\Guzzle\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Jeremy Lindblom",
+                    "email": "jeremeamia@gmail.com",
+                    "homepage": "https://github.com/jeremeamia"
+                },
+                {
+                    "name": "Stefano Kowalke",
+                    "email": "blueduck@mail.org",
+                    "homepage": "https://github.com/konafets"
+                }
+            ],
+            "description": "Provides an implementation of the Guzzle Command library that uses Guzzle service descriptions to describe web services, serialize requests, and parse responses into easy to use model structures.",
+            "time": "2017-10-06T14:32:02+00:00"
+        },
+        {
+            "name": "guzzlehttp/promises",
+            "version": "1.5.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/promises.git",
+                "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e",
+                "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.5"
+            },
+            "require-dev": {
+                "symfony/phpunit-bridge": "^4.4 || ^5.1"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/functions_include.php"
+                ],
+                "psr-4": {
+                    "GuzzleHttp\\Promise\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "Guzzle promises library",
+            "keywords": [
+                "promise"
+            ],
+            "time": "2023-05-21T12:31:43+00:00"
+        },
+        {
+            "name": "guzzlehttp/psr7",
+            "version": "1.9.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/psr7.git",
+                "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/e4490cabc77465aaee90b20cfc9a770f8c04be6b",
+                "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4.0",
+                "psr/http-message": "~1.0",
+                "ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
+            },
+            "provide": {
+                "psr/http-message-implementation": "1.0"
+            },
+            "require-dev": {
+                "ext-zlib": "*",
+                "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10"
+            },
+            "suggest": {
+                "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/functions_include.php"
+                ],
+                "psr-4": {
+                    "GuzzleHttp\\Psr7\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "George Mponos",
+                    "email": "gmponos@gmail.com",
+                    "homepage": "https://github.com/gmponos"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "mark.sagikazar@gmail.com",
+                    "homepage": "https://github.com/sagikazarmark"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "PSR-7 message implementation that also provides common utility methods",
+            "keywords": [
+                "http",
+                "message",
+                "psr-7",
+                "request",
+                "response",
+                "stream",
+                "uri",
+                "url"
+            ],
+            "time": "2023-04-17T16:00:37+00:00"
+        },
+        {
+            "name": "jasny/sso",
+            "version": "v0.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/jasny/sso.git",
+                "reference": "0cb18c072e7b14db3d2d2549c051f41ca837e5e9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/jasny/sso/zipball/0cb18c072e7b14db3d2d2549c051f41ca837e5e9",
+                "reference": "0cb18c072e7b14db3d2d2549c051f41ca837e5e9",
+                "shasum": ""
+            },
+            "require": {
+                "desarrolla2/cache": "^2.0.0",
+                "jasny/validation-result": "^1.0.0",
+                "php": ">=5.5.0"
+            },
+            "require-dev": {
+                "codeception/codeception": "^2.1.0",
+                "jasny/php-code-quality": "^1.1.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Jasny\\SSO\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Arnold Daniels",
+                    "email": "arnold@jasny.net",
+                    "homepage": "http://www.jasny.net"
+                }
+            ],
+            "description": "Simple Single Sign-On",
+            "homepage": "http://www.jasny.net/articles/simple-single-sign-on-for-php/",
+            "keywords": [
+                "SSO",
+                "auth"
+            ],
+            "time": "2017-02-24T22:44:27+00:00"
+        },
+        {
+            "name": "jasny/validation-result",
+            "version": "v1.1.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/jasny/validation-result.git",
+                "reference": "fbce54837c8414cf5af22981ac33b26eb13b2a51"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/jasny/validation-result/zipball/fbce54837c8414cf5af22981ac33b26eb13b2a51",
+                "reference": "fbce54837c8414cf5af22981ac33b26eb13b2a51",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.6.0"
+            },
+            "require-dev": {
+                "jasny/php-code-quality": "^1.2"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Jasny\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Arnold Daniels",
+                    "email": "arnold@jasny.net",
+                    "homepage": "http://www.jasny.net"
+                }
+            ],
+            "description": "A result object for validation",
+            "keywords": [
+                "validation"
+            ],
+            "time": "2019-02-12T12:51:58+00:00"
+        },
+        {
+            "name": "league/flysystem",
+            "version": "1.0.70",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thephpleague/flysystem.git",
+                "reference": "585824702f534f8d3cf7fab7225e8466cc4b7493"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/585824702f534f8d3cf7fab7225e8466cc4b7493",
+                "reference": "585824702f534f8d3cf7fab7225e8466cc4b7493",
+                "shasum": ""
+            },
+            "require": {
+                "ext-fileinfo": "*",
+                "php": ">=5.5.9"
+            },
+            "conflict": {
+                "league/flysystem-sftp": "<1.0.6"
+            },
+            "require-dev": {
+                "phpspec/phpspec": "^3.4 || ^4.0 || ^5.0 || ^6.0",
+                "phpunit/phpunit": "^5.7.26"
+            },
+            "suggest": {
+                "ext-fileinfo": "Required for MimeType",
+                "ext-ftp": "Allows you to use FTP server storage",
+                "ext-openssl": "Allows you to use FTPS server storage",
+                "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2",
+                "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3",
+                "league/flysystem-azure": "Allows you to use Windows Azure Blob storage",
+                "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching",
+                "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem",
+                "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files",
+                "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib",
+                "league/flysystem-webdav": "Allows you to use WebDAV storage",
+                "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter",
+                "spatie/flysystem-dropbox": "Allows you to use Dropbox storage",
+                "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "League\\Flysystem\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Frank de Jonge",
+                    "email": "info@frenky.net"
+                }
+            ],
+            "description": "Filesystem abstraction: Many filesystems, one API.",
+            "keywords": [
+                "Cloud Files",
+                "WebDAV",
+                "abstraction",
+                "aws",
+                "cloud",
+                "copy.com",
+                "dropbox",
+                "file systems",
+                "files",
+                "filesystem",
+                "filesystems",
+                "ftp",
+                "rackspace",
+                "remote",
+                "s3",
+                "sftp",
+                "storage"
+            ],
+            "time": "2020-07-26T07:20:36+00:00"
+        },
+        {
+            "name": "league/flysystem-cached-adapter",
+            "version": "1.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thephpleague/flysystem-cached-adapter.git",
+                "reference": "d1925efb2207ac4be3ad0c40b8277175f99ffaff"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thephpleague/flysystem-cached-adapter/zipball/d1925efb2207ac4be3ad0c40b8277175f99ffaff",
+                "reference": "d1925efb2207ac4be3ad0c40b8277175f99ffaff",
+                "shasum": ""
+            },
+            "require": {
+                "league/flysystem": "~1.0",
+                "psr/cache": "^1.0.0"
+            },
+            "require-dev": {
+                "mockery/mockery": "~0.9",
+                "phpspec/phpspec": "^3.4",
+                "phpunit/phpunit": "^5.7",
+                "predis/predis": "~1.0",
+                "tedivm/stash": "~0.12"
+            },
+            "suggest": {
+                "ext-phpredis": "Pure C implemented extension for PHP"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "League\\Flysystem\\Cached\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "frankdejonge",
+                    "email": "info@frenky.net"
+                }
+            ],
+            "description": "An adapter decorator to enable meta-data caching.",
+            "time": "2020-07-25T15:56:04+00:00"
+        },
+        {
+            "name": "liz/flysystem-qiniu",
+            "version": "v1.23",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/l396635210/flysystem-qiniu.git",
+                "reference": "24af0f95644fcf66e82cc04ce840f31f6450e026"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/l396635210/flysystem-qiniu/zipball/24af0f95644fcf66e82cc04ce840f31f6450e026",
+                "reference": "24af0f95644fcf66e82cc04ce840f31f6450e026",
+                "shasum": ""
+            },
+            "require": {
+                "league/flysystem": "^1.0",
+                "php": "^7.0 || ^8.0",
+                "qiniu/php-sdk": "^7.2"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Liz\\Flysystem\\QiNiu\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "liz in company",
+                    "email": "396635210@qq.com"
+                }
+            ],
+            "description": "QiNiu oss adapter for flysystem",
+            "time": "2021-01-12T12:50:38+00:00"
+        },
+        {
+            "name": "mtdowling/jmespath.php",
+            "version": "2.6.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/jmespath/jmespath.php.git",
+                "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/9b87907a81b87bc76d19a7fb2d61e61486ee9edb",
+                "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.4 || ^7.0 || ^8.0",
+                "symfony/polyfill-mbstring": "^1.17"
+            },
+            "require-dev": {
+                "composer/xdebug-handler": "^1.4 || ^2.0",
+                "phpunit/phpunit": "^4.8.36 || ^7.5.15"
+            },
+            "bin": [
+                "bin/jp.php"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.6-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/JmesPath.php"
+                ],
+                "psr-4": {
+                    "JmesPath\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                }
+            ],
+            "description": "Declaratively specify how to extract elements from a JSON document",
+            "keywords": [
+                "json",
+                "jsonpath"
+            ],
+            "time": "2021-06-14T00:11:39+00:00"
+        },
+        {
+            "name": "myclabs/php-enum",
+            "version": "1.7.7",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/myclabs/php-enum.git",
+                "reference": "d178027d1e679832db9f38248fcc7200647dc2b7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/myclabs/php-enum/zipball/d178027d1e679832db9f38248fcc7200647dc2b7",
+                "reference": "d178027d1e679832db9f38248fcc7200647dc2b7",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7",
+                "squizlabs/php_codesniffer": "1.*",
+                "vimeo/psalm": "^3.8"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "MyCLabs\\Enum\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP Enum contributors",
+                    "homepage": "https://github.com/myclabs/php-enum/graphs/contributors"
+                }
+            ],
+            "description": "PHP Enum implementation",
+            "homepage": "http://github.com/myclabs/php-enum",
+            "keywords": [
+                "enum"
+            ],
+            "time": "2020-11-14T18:14:52+00:00"
+        },
+        {
+            "name": "nesbot/carbon",
+            "version": "2.68.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/briannesbitt/Carbon.git",
+                "reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4f991ed2a403c85efbc4f23eb4030063fdbe01da",
+                "reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "php": "^7.1.8 || ^8.0",
+                "symfony/polyfill-mbstring": "^1.0",
+                "symfony/polyfill-php80": "^1.16",
+                "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0"
+            },
+            "require-dev": {
+                "doctrine/dbal": "^2.0 || ^3.1.4",
+                "doctrine/orm": "^2.7",
+                "friendsofphp/php-cs-fixer": "^3.0",
+                "kylekatarnls/multi-tester": "^2.0",
+                "ondrejmirtes/better-reflection": "*",
+                "phpmd/phpmd": "^2.9",
+                "phpstan/extension-installer": "^1.0",
+                "phpstan/phpstan": "^0.12.99 || ^1.7.14",
+                "phpunit/php-file-iterator": "^2.0.5 || ^3.0.6",
+                "phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20",
+                "squizlabs/php_codesniffer": "^3.4"
+            },
+            "bin": [
+                "bin/carbon"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-3.x": "3.x-dev",
+                    "dev-master": "2.x-dev"
+                },
+                "laravel": {
+                    "providers": [
+                        "Carbon\\Laravel\\ServiceProvider"
+                    ]
+                },
+                "phpstan": {
+                    "includes": [
+                        "extension.neon"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Carbon\\": "src/Carbon/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Brian Nesbitt",
+                    "email": "brian@nesbot.com",
+                    "homepage": "https://markido.com"
+                },
+                {
+                    "name": "kylekatarnls",
+                    "homepage": "https://github.com/kylekatarnls"
+                }
+            ],
+            "description": "An API extension for DateTime that supports 281 different languages.",
+            "homepage": "https://carbon.nesbot.com",
+            "keywords": [
+                "date",
+                "datetime",
+                "time"
+            ],
+            "time": "2023-06-20T18:29:04+00:00"
+        },
+        {
+            "name": "overtrue/flysystem-cos",
+            "version": "2.1.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/overtrue/flysystem-cos.git",
+                "reference": "f6985f38f3c76a9be962f404f127b7222a9b5fff"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/overtrue/flysystem-cos/zipball/f6985f38f3c76a9be962f404f127b7222a9b5fff",
+                "reference": "f6985f38f3c76a9be962f404f127b7222a9b5fff",
+                "shasum": ""
+            },
+            "require": {
+                "guzzlehttp/guzzle": "^6.3|^7.0",
+                "league/flysystem": "^1.0",
+                "php": ">=7.0",
+                "qcloud/cos-sdk-v5": "^2.0.0"
+            },
+            "require-dev": {
+                "mockery/mockery": "~1.0",
+                "php": ">=7.1",
+                "phpunit/phpunit": "^8.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Overtrue\\Flysystem\\Cos\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "overtrue",
+                    "email": "i@overtrue.me"
+                }
+            ],
+            "description": "Flysystem adapter for the QCloud COS storage.",
+            "time": "2020-10-22T10:28:58+00:00"
+        },
+        {
+            "name": "overtrue/pinyin",
+            "version": "4.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/overtrue/pinyin.git",
+                "reference": "4d0fb4f27f0c79e81c9489e0c0ae4a4f8837eae7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/overtrue/pinyin/zipball/4d0fb4f27f0c79e81c9489e0c0ae4a4f8837eae7",
+                "reference": "4d0fb4f27f0c79e81c9489e0c0ae4a4f8837eae7",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "brainmaestro/composer-git-hooks": "^2.7",
+                "friendsofphp/php-cs-fixer": "^2.16",
+                "phpunit/phpunit": "~8.0"
+            },
+            "type": "library",
+            "extra": {
+                "hooks": {
+                    "pre-commit": [
+                        "composer test",
+                        "composer fix-style"
+                    ],
+                    "pre-push": [
+                        "composer test",
+                        "composer check-style"
+                    ]
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/const.php"
+                ],
+                "psr-4": {
+                    "Overtrue\\Pinyin\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "overtrue",
+                    "email": "anzhengchao@gmail.com",
+                    "homepage": "http://github.com/overtrue"
+                }
+            ],
+            "description": "Chinese to pinyin translator.",
+            "homepage": "https://github.com/overtrue/pinyin",
+            "keywords": [
+                "Chinese",
+                "Pinyin",
+                "cn2pinyin"
+            ],
+            "time": "2023-04-27T10:17:12+00:00"
+        },
+        {
+            "name": "psr/cache",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/cache.git",
+                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8",
+                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Cache\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for caching libraries",
+            "keywords": [
+                "cache",
+                "psr",
+                "psr-6"
+            ],
+            "time": "2016-08-06T20:24:11+00:00"
+        },
+        {
+            "name": "psr/container",
+            "version": "1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/container.git",
+                "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
+                "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Container\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common Container Interface (PHP FIG PSR-11)",
+            "homepage": "https://github.com/php-fig/container",
+            "keywords": [
+                "PSR-11",
+                "container",
+                "container-interface",
+                "container-interop",
+                "psr"
+            ],
+            "time": "2017-02-14T16:28:37+00:00"
+        },
+        {
+            "name": "psr/http-message",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-message.git",
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Message\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP messages",
+            "homepage": "https://github.com/php-fig/http-message",
+            "keywords": [
+                "http",
+                "http-message",
+                "psr",
+                "psr-7",
+                "request",
+                "response"
+            ],
+            "time": "2016-08-06T14:39:51+00:00"
+        },
+        {
+            "name": "psr/log",
+            "version": "1.1.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/log.git",
+                "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
+                "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Log\\": "Psr/Log/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for logging libraries",
+            "homepage": "https://github.com/php-fig/log",
+            "keywords": [
+                "log",
+                "psr",
+                "psr-3"
+            ],
+            "time": "2021-05-03T11:20:27+00:00"
+        },
+        {
+            "name": "psr/simple-cache",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/simple-cache.git",
+                "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+                "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\SimpleCache\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interfaces for simple caching",
+            "keywords": [
+                "cache",
+                "caching",
+                "psr",
+                "psr-16",
+                "simple-cache"
+            ],
+            "time": "2017-10-23T01:57:42+00:00"
+        },
+        {
+            "name": "qcloud/cos-sdk-v5",
+            "version": "v2.6.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/tencentyun/cos-php-sdk-v5.git",
+                "reference": "dd1b7a096cbdcafc9de265cb2bb1b4222ac60136"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/tencentyun/cos-php-sdk-v5/zipball/dd1b7a096cbdcafc9de265cb2bb1b4222ac60136",
+                "reference": "dd1b7a096cbdcafc9de265cb2bb1b4222ac60136",
+                "shasum": ""
+            },
+            "require": {
+                "ext-curl": "*",
+                "ext-json": "*",
+                "ext-mbstring": "*",
+                "ext-simplexml": "*",
+                "guzzlehttp/guzzle": "^6.2.1 || ^7.0",
+                "guzzlehttp/guzzle-services": "^1.1",
+                "guzzlehttp/psr7": "^1.3.1 || ^2.0",
+                "php": ">=5.6"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.4-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/Common.php"
+                ],
+                "psr-4": {
+                    "Qcloud\\Cos\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "yaozongyou",
+                    "email": "yaozongyou@vip.qq.com"
+                },
+                {
+                    "name": "lewzylu",
+                    "email": "327874225@qq.com"
+                },
+                {
+                    "name": "tuunalai",
+                    "email": "550566181@qq.com"
+                }
+            ],
+            "description": "PHP SDK for QCloud COS",
+            "keywords": [
+                "cos",
+                "php",
+                "qcloud"
+            ],
+            "time": "2023-06-14T03:18:14+00:00"
+        },
+        {
+            "name": "qcloudsms/qcloudsms_php",
+            "version": "v0.1.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/qcloudsms/qcloudsms_php.git",
+                "reference": "48822045772d343b93c3d505d8a187cd51153c5a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://mirrors.huaweicloud.com/repository/php/qcloudsms/qcloudsms_php/v0.1.4/qcloudsms-qcloudsms_php-v0.1.4.zip",
+                "reference": "48822045772d343b93c3d505d8a187cd51153c5a",
+                "shasum": ""
+            },
+            "require-dev": {
+                "sami/sami": "dev-master"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Qcloud\\Sms\\": "src/"
+                }
+            },
+            "license": [
+                "MIT"
+            ],
+            "description": "qcloud sms php sdk",
+            "keywords": [
+                "php",
+                "qcloud",
+                "sdk",
+                "sms"
+            ],
+            "time": "2018-09-19T07:19:17+00:00"
+        },
+        {
+            "name": "qeq66/jwt",
+            "version": "3.3.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/qeq66/jwt.git",
+                "reference": "bd2fa6c51704dc18c61026c852c789224d7190a0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://mirrors.huaweicloud.com/repository/php/qeq66/jwt/3.3.5/qeq66-jwt-3.3.5.zip",
+                "reference": "bd2fa6c51704dc18c61026c852c789224d7190a0",
+                "shasum": ""
+            },
+            "require": {
+                "ext-mbstring": "*",
+                "ext-openssl": "*",
+                "php": ">=5.6"
+            },
+            "require-dev": {
+                "mikey179/vfsstream": "~1.5",
+                "phpmd/phpmd": "~2.2",
+                "phpunit/php-invoker": "~1.1",
+                "phpunit/phpunit": "^5.7 || ^7.3",
+                "squizlabs/php_codesniffer": "~2.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.1-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Lcobucci\\JWT\\": "src"
+                }
+            },
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Luís Otávio Cobucci Oblonczyk",
+                    "email": "lcobucci@gmail.com",
+                    "role": "Developer"
+                }
+            ],
+            "description": "A simple library to work with JSON Web Token and JSON Web Signature",
+            "keywords": [
+                "JWS",
+                "jwt"
+            ],
+            "time": "2022-07-11T08:31:22+00:00"
+        },
+        {
+            "name": "qiniu/php-sdk",
+            "version": "v7.9.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/qiniu/php-sdk.git",
+                "reference": "3c0ebeee6a7439a0d2874f24b56dfe43545a1d2e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/qiniu/php-sdk/zipball/3c0ebeee6a7439a0d2874f24b56dfe43545a1d2e",
+                "reference": "3c0ebeee6a7439a0d2874f24b56dfe43545a1d2e",
+                "shasum": ""
+            },
+            "require": {
+                "myclabs/php-enum": "~1.5.2 || ~1.6.6 || ~1.7.7 || ~1.8.4",
+                "php": ">=5.3.3"
+            },
+            "require-dev": {
+                "paragonie/random_compat": ">=2",
+                "phpunit/phpunit": "^4.8 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4",
+                "squizlabs/php_codesniffer": "^2.3 || ~3.6"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/Qiniu/functions.php"
+                ],
+                "psr-4": {
+                    "Qiniu\\": "src/Qiniu"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Qiniu",
+                    "email": "sdk@qiniu.com",
+                    "homepage": "http://www.qiniu.com"
+                }
+            ],
+            "description": "Qiniu Resource (Cloud) Storage SDK for PHP",
+            "homepage": "http://developer.qiniu.com/",
+            "keywords": [
+                "cloud",
+                "qiniu",
+                "sdk",
+                "storage"
+            ],
+            "time": "2023-05-06T04:36:16+00:00"
+        },
+        {
+            "name": "ralouphie/getallheaders",
+            "version": "3.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ralouphie/getallheaders.git",
+                "reference": "120b605dfeb996808c31b6477290a714d356e822"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+                "reference": "120b605dfeb996808c31b6477290a714d356e822",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.6"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "^2.1",
+                "phpunit/phpunit": "^5 || ^6.5"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/getallheaders.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ralph Khattar",
+                    "email": "ralph.khattar@gmail.com"
+                }
+            ],
+            "description": "A polyfill for getallheaders.",
+            "time": "2019-03-08T08:55:37+00:00"
+        },
+        {
+            "name": "singka/singka-sms",
+            "version": "v1.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/SingKa-TECH/singka-sms.git",
+                "reference": "8db3543dfb7f28851b94de99dfe9b762f0b9b6a4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://mirrors.huaweicloud.com/repository/php/singka/singka-sms/v1.6/singka-singka-sms-v1.6.zip",
+                "reference": "8db3543dfb7f28851b94de99dfe9b762f0b9b6a4",
+                "shasum": ""
+            },
+            "require": {
+                "alibabacloud/client": "^1.5",
+                "guzzlehttp/guzzle": "~6.0@dev",
+                "php": ">=7.0",
+                "qcloudsms/qcloudsms_php": "0.1.*",
+                "qiniu/php-sdk": "^7.2",
+                "singka/ucloud-sms": "^1.8"
+            },
+            "type": "library",
+            "extra": {
+                "think": {
+                    "config": {
+                        "sms": "config/config.php"
+                    }
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "SingKa\\Sms\\": "src/"
+                }
+            },
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "宁波晟嘉网络科技有限公司 夏慧新",
+                    "email": "shycomet@singka.email"
+                }
+            ],
+            "description": "适用于ThinkPHP6.0的各种短信接口集成服务,本项目集成了各大云服务厂商的短信业务平台,支持ThinkPHP5.0、ThinkPHP5.1和ThinkPHP6.0,由宁波晟嘉网络科技有限公司维护,目前支持阿里云、腾讯云、七牛云、又拍云、Ucloud和华为云。",
+            "time": "2020-07-02T05:07:00+00:00"
+        },
+        {
+            "name": "singka/ucloud-sms",
+            "version": "v1.8",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/SingKa-TECH/ucloud-sms.git",
+                "reference": "7c4ae42e9c7b26b4db7e6340ff03f68b15e3b0d8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://mirrors.huaweicloud.com/repository/php/singka/ucloud-sms/v1.8/singka-ucloud-sms-v1.8.zip",
+                "reference": "7c4ae42e9c7b26b4db7e6340ff03f68b15e3b0d8",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.6"
+            },
+            "type": "library",
+            "extra": {
+                "think": {
+                    "config": {
+                        "usms": "config/config.php"
+                    }
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Singka\\UcloudSms\\": "src/"
+                }
+            },
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "宁波晟嘉网络科技有限公司 夏慧新",
+                    "email": "shycomet@singka.email"
+                }
+            ],
+            "description": "ThinkPHP系列Ucloud的短信接口,支持ThinkPHP6、ThinkPHP5.1和ThinkPHP5.0",
+            "time": "2020-03-24T16:24:09+00:00"
+        },
+        {
+            "name": "swiftmailer/swiftmailer",
+            "version": "v6.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/swiftmailer/swiftmailer.git",
+                "reference": "8a5d5072dca8f48460fce2f4131fcc495eec654c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/8a5d5072dca8f48460fce2f4131fcc495eec654c",
+                "reference": "8a5d5072dca8f48460fce2f4131fcc495eec654c",
+                "shasum": ""
+            },
+            "require": {
+                "egulias/email-validator": "^2.0|^3.1",
+                "php": ">=7.0.0",
+                "symfony/polyfill-iconv": "^1.0",
+                "symfony/polyfill-intl-idn": "^1.10",
+                "symfony/polyfill-mbstring": "^1.0"
+            },
+            "require-dev": {
+                "mockery/mockery": "^1.0",
+                "symfony/phpunit-bridge": "^4.4|^5.4"
+            },
+            "suggest": {
+                "ext-intl": "Needed to support internationalized email addresses"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "6.2-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "lib/swift_required.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Chris Corbyn"
+                },
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                }
+            ],
+            "description": "Swiftmailer, free feature-rich PHP mailer",
+            "homepage": "https://swiftmailer.symfony.com",
+            "keywords": [
+                "email",
+                "mail",
+                "mailer"
+            ],
+            "abandoned": "symfony/mailer",
+            "time": "2021-10-18T15:26:12+00:00"
+        },
+        {
+            "name": "symfony/polyfill-iconv",
+            "version": "v1.27.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-iconv.git",
+                "reference": "927013f3aac555983a5059aada98e1907d842695"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/927013f3aac555983a5059aada98e1907d842695",
+                "reference": "927013f3aac555983a5059aada98e1907d842695",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "provide": {
+                "ext-iconv": "*"
+            },
+            "suggest": {
+                "ext-iconv": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.27-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Iconv\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for the Iconv extension",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "iconv",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2022-11-03T14:55:06+00:00"
+        },
+        {
+            "name": "symfony/polyfill-intl-idn",
+            "version": "v1.27.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-intl-idn.git",
+                "reference": "639084e360537a19f9ee352433b84ce831f3d2da"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/639084e360537a19f9ee352433b84ce831f3d2da",
+                "reference": "639084e360537a19f9ee352433b84ce831f3d2da",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1",
+                "symfony/polyfill-intl-normalizer": "^1.10",
+                "symfony/polyfill-php72": "^1.10"
+            },
+            "suggest": {
+                "ext-intl": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.27-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Intl\\Idn\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Laurent Bassin",
+                    "email": "laurent@bassin.info"
+                },
+                {
+                    "name": "Trevor Rowbotham",
+                    "email": "trevor.rowbotham@pm.me"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "idn",
+                "intl",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2022-11-03T14:55:06+00:00"
+        },
+        {
+            "name": "symfony/polyfill-intl-normalizer",
+            "version": "v1.27.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
+                "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6",
+                "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "suggest": {
+                "ext-intl": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.27-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for intl's Normalizer class and related functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "intl",
+                "normalizer",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2022-11-03T14:55:06+00:00"
+        },
+        {
+            "name": "symfony/polyfill-mbstring",
+            "version": "v1.27.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-mbstring.git",
+                "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
+                "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "provide": {
+                "ext-mbstring": "*"
+            },
+            "suggest": {
+                "ext-mbstring": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.27-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Mbstring\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for the Mbstring extension",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "mbstring",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2022-11-03T14:55:06+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php72",
+            "version": "v1.27.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php72.git",
+                "reference": "869329b1e9894268a8a61dabb69153029b7a8c97"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/869329b1e9894268a8a61dabb69153029b7a8c97",
+                "reference": "869329b1e9894268a8a61dabb69153029b7a8c97",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.27-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php72\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2022-11-03T14:55:06+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php80",
+            "version": "v1.27.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php80.git",
+                "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
+                "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.27-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php80\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ion Bazan",
+                    "email": "ion.bazan@gmail.com"
+                },
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2022-11-03T14:55:06+00:00"
+        },
+        {
+            "name": "symfony/process",
+            "version": "v4.4.44",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/process.git",
+                "reference": "5cee9cdc4f7805e2699d9fd66991a0e6df8252a2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/process/zipball/5cee9cdc4f7805e2699d9fd66991a0e6df8252a2",
+                "reference": "5cee9cdc4f7805e2699d9fd66991a0e6df8252a2",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/polyfill-php80": "^1.16"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Process\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Executes commands in sub-processes",
+            "homepage": "https://symfony.com",
+            "time": "2022-06-27T13:16:42+00:00"
+        },
+        {
+            "name": "symfony/translation",
+            "version": "v4.4.47",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/translation.git",
+                "reference": "45036b1d53accc48fe9bab71ccd86d57eba0dd94"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/45036b1d53accc48fe9bab71ccd86d57eba0dd94",
+                "reference": "45036b1d53accc48fe9bab71ccd86d57eba0dd94",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/polyfill-php80": "^1.16",
+                "symfony/translation-contracts": "^1.1.6|^2"
+            },
+            "conflict": {
+                "symfony/config": "<3.4",
+                "symfony/dependency-injection": "<3.4",
+                "symfony/http-kernel": "<4.4",
+                "symfony/yaml": "<3.4"
+            },
+            "provide": {
+                "symfony/translation-implementation": "1.0|2.0"
+            },
+            "require-dev": {
+                "psr/log": "^1|^2|^3",
+                "symfony/config": "^3.4|^4.0|^5.0",
+                "symfony/console": "^3.4|^4.0|^5.0",
+                "symfony/dependency-injection": "^3.4|^4.0|^5.0",
+                "symfony/finder": "~2.8|~3.0|~4.0|^5.0",
+                "symfony/http-kernel": "^4.4",
+                "symfony/intl": "^3.4|^4.0|^5.0",
+                "symfony/service-contracts": "^1.1.2|^2",
+                "symfony/yaml": "^3.4|^4.0|^5.0"
+            },
+            "suggest": {
+                "psr/log-implementation": "To use logging capability in translator",
+                "symfony/config": "",
+                "symfony/yaml": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Translation\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Provides tools to internationalize your application",
+            "homepage": "https://symfony.com",
+            "time": "2022-10-03T15:15:11+00:00"
+        },
+        {
+            "name": "symfony/translation-contracts",
+            "version": "v1.1.13",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/translation-contracts.git",
+                "reference": "7462e5c4cb8b9cd152f992e8f10963b5641921f6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/7462e5c4cb8b9cd152f992e8f10963b5641921f6",
+                "reference": "7462e5c4cb8b9cd152f992e8f10963b5641921f6",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3"
+            },
+            "suggest": {
+                "symfony/translation-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.1-dev"
+                },
+                "thanks": {
+                    "name": "symfony/contracts",
+                    "url": "https://github.com/symfony/contracts"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\Translation\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to translation",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2022-06-27T13:16:42+00:00"
+        },
+        {
+            "name": "tcwei/imglazyload",
+            "version": "v1.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ITzhiwei/ArticleImgLazyload.git",
+                "reference": "f7634e446de972a26aac6973141c39ca2ea62b89"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ITzhiwei/ArticleImgLazyload/zipball/f7634e446de972a26aac6973141c39ca2ea62b89",
+                "reference": "f7634e446de972a26aac6973141c39ca2ea62b89",
+                "shasum": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "tcwei\\smallTools\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "zhiwei",
+                    "email": "2394599321@qq.com"
+                }
+            ],
+            "description": "文章图片懒加载,当文章中存在大量图片时,可使用该库进行图片懒加载,看不到的图片不进行加载,节省带宽",
+            "time": "2020-06-25T14:08:30+00:00"
+        },
+        {
+            "name": "tcwei/imgsrc",
+            "version": "v2.02",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ITzhiwei/getImgSrc.git",
+                "reference": "832342b664fc7d84e0c6253ab90407f2b565887e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ITzhiwei/getImgSrc/zipball/832342b664fc7d84e0c6253ab90407f2b565887e",
+                "reference": "832342b664fc7d84e0c6253ab90407f2b565887e",
+                "shasum": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "tcwei\\smallTools\\": "imgSrc/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "zhiwei",
+                    "email": "2394599321@qq.com"
+                }
+            ],
+            "description": "高效率字符串模式(可比正则获取快3倍)从HTML或者文章内容等字符串中提取图片的src,可指定提取第几张图片、顺数第几或逆数第几、可指定黑名单等。页面下面有使用例子:",
+            "time": "2021-06-16T15:07:12+00:00"
+        },
+        {
+            "name": "thans/thinkphp-filesystem-cloud",
+            "version": "v1.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/QThans/thinkphp-filesystem-cloud.git",
+                "reference": "b8d6d61a8b28df12ae9b1b19249b90e8a645ffa2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/QThans/thinkphp-filesystem-cloud/zipball/b8d6d61a8b28df12ae9b1b19249b90e8a645ffa2",
+                "reference": "b8d6d61a8b28df12ae9b1b19249b90e8a645ffa2",
+                "shasum": ""
+            },
+            "require": {
+                "liz/flysystem-qiniu": "^1.10",
+                "overtrue/flysystem-cos": "^2.0.0",
+                "php": ">=7.1.0",
+                "topthink/framework": "^6.0.0",
+                "xxtime/flysystem-aliyun-oss": "^1.4"
+            },
+            "type": "library",
+            "extra": {
+                "think": {
+                    "services": [
+                        "thans\\filesystem\\Service"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "thans\\filesystem\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Thans",
+                    "email": "360641274@qq.com"
+                }
+            ],
+            "description": "thinkphp6.0 filesystem,include Aliyun and Qiniu",
+            "time": "2019-11-29T00:57:33+00:00"
+        },
+        {
+            "name": "thans/tp-jwt-auth",
+            "version": "v1.3.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/QThans/jwt-auth.git",
+                "reference": "ab5efcc0fd920df81fea2c404c34bb967ef13aba"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://mirrors.huaweicloud.com/repository/php/thans/tp-jwt-auth/v1.3.1/thans-tp-jwt-auth-v1.3.1.zip",
+                "reference": "ab5efcc0fd920df81fea2c404c34bb967ef13aba",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0 || ^8.0",
+                "qeq66/jwt": "3.3.*",
+                "topthink/framework": "^5.1.10 || ^6.0.0"
+            },
+            "type": "library",
+            "extra": {
+                "think": {
+                    "services": [
+                        "thans\\jwt\\Service"
+                    ],
+                    "config": {
+                        "jwt": "config/config.php"
+                    }
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/helper.php"
+                ],
+                "psr-4": {
+                    "thans\\jwt\\": "src"
+                }
+            },
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Thans",
+                    "email": "360641274@qq.com"
+                }
+            ],
+            "description": "thinkphp  jwt auth composer",
+            "time": "2022-11-01T02:44:23+00:00"
+        },
+        {
+            "name": "topthink/framework",
+            "version": "v6.0.8",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/top-think/framework.git",
+                "reference": "4789343672aef06d571d556da369c0e156609bce"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/top-think/framework/zipball/4789343672aef06d571d556da369c0e156609bce",
+                "reference": "4789343672aef06d571d556da369c0e156609bce",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "ext-mbstring": "*",
+                "league/flysystem": "^1.0",
+                "league/flysystem-cached-adapter": "^1.0",
+                "php": ">=7.1.0",
+                "psr/container": "~1.0",
+                "psr/log": "~1.0",
+                "psr/simple-cache": "^1.0",
+                "topthink/think-helper": "^3.1.1",
+                "topthink/think-orm": "^2.0"
+            },
+            "require-dev": {
+                "mikey179/vfsstream": "^1.6",
+                "mockery/mockery": "^1.2",
+                "phpunit/phpunit": "^7.0"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [],
+                "psr-4": {
+                    "think\\": "src/think/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "liu21st",
+                    "email": "liu21st@gmail.com"
+                },
+                {
+                    "name": "yunwuxin",
+                    "email": "448901948@qq.com"
+                }
+            ],
+            "description": "The ThinkPHP Framework.",
+            "homepage": "http://thinkphp.cn/",
+            "keywords": [
+                "framework",
+                "orm",
+                "thinkphp"
+            ],
+            "time": "2021-04-27T00:41:08+00:00"
+        },
+        {
+            "name": "topthink/think-captcha",
+            "version": "v3.0.9",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/top-think/think-captcha.git",
+                "reference": "b1ef360670578214edeebcf824aaf6ab7ee0528b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/top-think/think-captcha/zipball/b1ef360670578214edeebcf824aaf6ab7ee0528b",
+                "reference": "b1ef360670578214edeebcf824aaf6ab7ee0528b",
+                "shasum": ""
+            },
+            "require": {
+                "topthink/framework": "^6.0|^8.0"
+            },
+            "type": "library",
+            "extra": {
+                "think": {
+                    "services": [
+                        "think\\captcha\\CaptchaService"
+                    ],
+                    "config": {
+                        "captcha": "src/config.php"
+                    }
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/helper.php"
+                ],
+                "psr-4": {
+                    "think\\captcha\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "yunwuxin",
+                    "email": "448901948@qq.com"
+                }
+            ],
+            "description": "captcha package for thinkphp",
+            "time": "2023-04-27T07:18:40+00:00"
+        },
+        {
+            "name": "topthink/think-helper",
+            "version": "v3.1.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/top-think/think-helper.git",
+                "reference": "769acbe50a4274327162f9c68ec2e89a38eb2aff"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/top-think/think-helper/zipball/769acbe50a4274327162f9c68ec2e89a38eb2aff",
+                "reference": "769acbe50a4274327162f9c68ec2e89a38eb2aff",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.5"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/helper.php"
+                ],
+                "psr-4": {
+                    "think\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "yunwuxin",
+                    "email": "448901948@qq.com"
+                }
+            ],
+            "description": "The ThinkPHP6 Helper Package",
+            "time": "2021-12-15T04:27:55+00:00"
+        },
+        {
+            "name": "topthink/think-multi-app",
+            "version": "v1.0.17",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/top-think/think-multi-app.git",
+                "reference": "4055a6187296ac16c0bc7bbab4ed5d92f82f791c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/top-think/think-multi-app/zipball/4055a6187296ac16c0bc7bbab4ed5d92f82f791c",
+                "reference": "4055a6187296ac16c0bc7bbab4ed5d92f82f791c",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.0",
+                "topthink/framework": "^6.0|^8.0"
+            },
+            "type": "library",
+            "extra": {
+                "think": {
+                    "services": [
+                        "think\\app\\Service"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "think\\app\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "liu21st",
+                    "email": "liu21st@gmail.com"
+                }
+            ],
+            "description": "thinkphp multi app support",
+            "time": "2023-03-29T02:04:29+00:00"
+        },
+        {
+            "name": "topthink/think-orm",
+            "version": "v2.0.61",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/top-think/think-orm.git",
+                "reference": "10528ebf4a5106b19c3bac9c6deae7a67ff49de6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/top-think/think-orm/zipball/10528ebf4a5106b19c3bac9c6deae7a67ff49de6",
+                "reference": "10528ebf4a5106b19c3bac9c6deae7a67ff49de6",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "ext-pdo": "*",
+                "php": ">=7.1.0",
+                "psr/log": "^1.0|^2.0",
+                "psr/simple-cache": "^1.0|^2.0",
+                "topthink/think-helper": "^3.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7|^8|^9.5"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "stubs/load_stubs.php"
+                ],
+                "psr-4": {
+                    "think\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "liu21st",
+                    "email": "liu21st@gmail.com"
+                }
+            ],
+            "description": "think orm",
+            "keywords": [
+                "database",
+                "orm"
+            ],
+            "time": "2023-04-20T14:27:51+00:00"
+        },
+        {
+            "name": "topthink/think-queue",
+            "version": "v3.0.9",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/top-think/think-queue.git",
+                "reference": "654812b47dd7c708c4443deed27f212f8382e8da"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/top-think/think-queue/zipball/654812b47dd7c708c4443deed27f212f8382e8da",
+                "reference": "654812b47dd7c708c4443deed27f212f8382e8da",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "nesbot/carbon": "^2.16",
+                "symfony/process": ">=4.2",
+                "topthink/framework": "^6.0 || ^8.0"
+            },
+            "require-dev": {
+                "mockery/mockery": "^1.2",
+                "phpunit/phpunit": "^6.2",
+                "topthink/think-migration": "^3.0"
+            },
+            "type": "library",
+            "extra": {
+                "think": {
+                    "services": [
+                        "think\\queue\\Service"
+                    ],
+                    "config": {
+                        "queue": "src/config.php"
+                    }
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/common.php"
+                ],
+                "psr-4": {
+                    "think\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "yunwuxin",
+                    "email": "448901948@qq.com"
+                }
+            ],
+            "description": "The ThinkPHP6 Queue Package",
+            "time": "2023-07-03T05:42:01+00:00"
+        },
+        {
+            "name": "topthink/think-template",
+            "version": "v2.0.9",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/top-think/think-template.git",
+                "reference": "6d25642ae0e306166742fd7073dc7a159e18073c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/top-think/think-template/zipball/6d25642ae0e306166742fd7073dc7a159e18073c",
+                "reference": "6d25642ae0e306166742fd7073dc7a159e18073c",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.0",
+                "psr/simple-cache": "^1.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "think\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "liu21st",
+                    "email": "liu21st@gmail.com"
+                }
+            ],
+            "description": "the php template engine",
+            "time": "2023-02-14T10:50:39+00:00"
+        },
+        {
+            "name": "topthink/think-view",
+            "version": "v1.0.14",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/top-think/think-view.git",
+                "reference": "edce0ae2c9551ab65f9e94a222604b0dead3576d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/top-think/think-view/zipball/edce0ae2c9551ab65f9e94a222604b0dead3576d",
+                "reference": "edce0ae2c9551ab65f9e94a222604b0dead3576d",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.0",
+                "topthink/think-template": "^2.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "think\\view\\driver\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "liu21st",
+                    "email": "liu21st@gmail.com"
+                }
+            ],
+            "description": "thinkphp template driver",
+            "time": "2019-11-06T11:40:13+00:00"
+        },
+        {
+            "name": "workerman/gateway-worker",
+            "version": "v3.0.28",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/walkor/GatewayWorker.git",
+                "reference": "a7dffc53403133131a51b9fd3c6c6d70869cb6d3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/walkor/GatewayWorker/zipball/a7dffc53403133131a51b9fd3c6c6d70869cb6d3",
+                "reference": "a7dffc53403133131a51b9fd3c6c6d70869cb6d3",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0",
+                "workerman/workerman": "^4.0.30"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "GatewayWorker\\": "./src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "homepage": "http://www.workerman.net",
+            "keywords": [
+                "communication",
+                "distributed"
+            ],
+            "time": "2023-03-24T03:56:27+00:00"
+        },
+        {
+            "name": "workerman/gatewayclient",
+            "version": "v3.0.14",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/walkor/GatewayClient.git",
+                "reference": "4362468d68251015b2b385c310252afb4d6648ed"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/walkor/GatewayClient/zipball/4362468d68251015b2b385c310252afb4d6648ed",
+                "reference": "4362468d68251015b2b385c310252afb4d6648ed",
+                "shasum": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "GatewayClient\\": "./"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "homepage": "http://www.workerman.net",
+            "time": "2021-11-29T07:03:50+00:00"
+        },
+        {
+            "name": "workerman/workerman",
+            "version": "v4.1.10",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/walkor/workerman.git",
+                "reference": "e967b79f95b9251a72acb971be05623ec1a51e83"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/walkor/workerman/zipball/e967b79f95b9251a72acb971be05623ec1a51e83",
+                "reference": "e967b79f95b9251a72acb971be05623ec1a51e83",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0"
+            },
+            "suggest": {
+                "ext-event": "For better performance. "
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Workerman\\": "./"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "walkor",
+                    "email": "walkor@workerman.net",
+                    "homepage": "http://www.workerman.net",
+                    "role": "Developer"
+                }
+            ],
+            "description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.",
+            "homepage": "http://www.workerman.net",
+            "keywords": [
+                "asynchronous",
+                "event-loop"
+            ],
+            "time": "2023-05-01T02:12:20+00:00"
+        },
+        {
+            "name": "xiaodi/think-pinyin",
+            "version": "v1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/edenleung/think-pinyin.git",
+                "reference": "4675515d3be42bebff712383c306ff99ef279f97"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/edenleung/think-pinyin/zipball/4675515d3be42bebff712383c306ff99ef279f97",
+                "reference": "4675515d3be42bebff712383c306ff99ef279f97",
+                "shasum": ""
+            },
+            "require": {
+                "overtrue/pinyin": "~4.0",
+                "topthink/framework": "6.0.*|5.1.*"
+            },
+            "type": "library",
+            "extra": {
+                "think": {
+                    "services": [
+                        "xiaodi\\ThinkPinyin\\PinyinService"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "xiaodi\\ThinkPinyin\\": "src/"
+                },
+                "files": [
+                    "src/helpers.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "xiaodi",
+                    "email": "liangjinbiao@live.com"
+                }
+            ],
+            "description": "ThinkPHP 中文转拼音扩展包",
+            "keywords": [
+                "Chinese",
+                "Pinyin",
+                "thinkphp"
+            ],
+            "time": "2019-10-25T09:19:32+00:00"
+        },
+        {
+            "name": "xiaodi/think-pullword",
+            "version": "v1.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/friendsofthinkphp/think-pullword.git",
+                "reference": "0e6f1ee141090a012dc8876209eaa3cf166c84c4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/friendsofthinkphp/think-pullword/zipball/0e6f1ee141090a012dc8876209eaa3cf166c84c4",
+                "reference": "0e6f1ee141090a012dc8876209eaa3cf166c84c4",
+                "shasum": ""
+            },
+            "require": {
+                "guzzlehttp/guzzle": "^5.0|^6.0|^7.0",
+                "topthink/framework": "6.0.*|5.1.*"
+            },
+            "type": "library",
+            "extra": {
+                "think": {
+                    "services": [
+                        "PullWord\\PullWordService"
+                    ]
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/helper.php"
+                ],
+                "psr-4": {
+                    "PullWord\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "xiaodi",
+                    "email": "liangjinbiao@live.com"
+                }
+            ],
+            "description": "ThinkPHP 分词/抽词 扩展包",
+            "keywords": [
+                "php",
+                "think-extend",
+                "thinkphp"
+            ],
+            "time": "2021-01-05T02:48:17+00:00"
+        },
+        {
+            "name": "xxtime/flysystem-aliyun-oss",
+            "version": "1.5.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/xxtime/flysystem-aliyun-oss.git",
+                "reference": "ae873b5919076157b9cfeaf39d2f56d2dbb39ee9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/xxtime/flysystem-aliyun-oss/zipball/ae873b5919076157b9cfeaf39d2f56d2dbb39ee9",
+                "reference": "ae873b5919076157b9cfeaf39d2f56d2dbb39ee9",
+                "shasum": ""
+            },
+            "require": {
+                "aliyuncs/oss-sdk-php": "~2.3",
+                "league/flysystem": "^1.0.49",
+                "php": ">=5.5.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Xxtime\\Flysystem\\Aliyun\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Joe",
+                    "email": "joe@xxtime.com",
+                    "homepage": "https://github.com/xxtime",
+                    "role": "Developer"
+                }
+            ],
+            "description": "AliYun OSS adapter for flysystem. aliyuncs/oss-sdk-php ~2.3",
+            "homepage": "https://github.com/xxtime/flysystem-aliyun-oss",
+            "keywords": [
+                "Flysystem",
+                "aliyun-oss",
+                "flysystem-aliyun-oss"
+            ],
+            "time": "2019-11-12T07:57:34+00:00"
+        },
+        {
+            "name": "yunwuxin/think-cron",
+            "version": "v3.0.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/yunwuxin/think-cron.git",
+                "reference": "4013c39cea4600e05ffd10de5b63177bfb9bf480"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/yunwuxin/think-cron/zipball/4013c39cea4600e05ffd10de5b63177bfb9bf480",
+                "reference": "4013c39cea4600e05ffd10de5b63177bfb9bf480",
+                "shasum": ""
+            },
+            "require": {
+                "dragonmantank/cron-expression": "^3.0",
+                "nesbot/carbon": "^2.28",
+                "symfony/process": "^4.4 || ^5.0",
+                "topthink/framework": "^6.0 || ^8.0"
+            },
+            "require-dev": {
+                "topthink/think-swoole": "^4.0"
+            },
+            "type": "library",
+            "extra": {
+                "think": {
+                    "config": {
+                        "cron": "src/config.php"
+                    },
+                    "services": [
+                        "yunwuxin\\cron\\Service"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "yunwuxin\\cron\\": "src/cron"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "yunwuxin",
+                    "email": "448901948@qq.com"
+                }
+            ],
+            "description": "计划任务",
+            "time": "2023-07-01T11:10:51+00:00"
+        }
+    ],
+    "packages-dev": [
+        {
+            "name": "symfony/var-dumper",
+            "version": "v4.4.47",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/var-dumper.git",
+                "reference": "1069c7a3fca74578022fab6f81643248d02f8e63"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/1069c7a3fca74578022fab6f81643248d02f8e63",
+                "reference": "1069c7a3fca74578022fab6f81643248d02f8e63",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/polyfill-php72": "~1.5",
+                "symfony/polyfill-php80": "^1.16"
+            },
+            "conflict": {
+                "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0",
+                "symfony/console": "<3.4"
+            },
+            "require-dev": {
+                "ext-iconv": "*",
+                "symfony/console": "^3.4|^4.0|^5.0",
+                "symfony/process": "^4.4|^5.0",
+                "twig/twig": "^1.43|^2.13|^3.0.4"
+            },
+            "suggest": {
+                "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).",
+                "ext-intl": "To show region name in time zone dump",
+                "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script"
+            },
+            "bin": [
+                "Resources/bin/var-dump-server"
+            ],
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "Resources/functions/dump.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Component\\VarDumper\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Provides mechanisms for walking through any arbitrary PHP variable",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "debug",
+                "dump"
+            ],
+            "time": "2022-10-03T15:15:11+00:00"
+        },
+        {
+            "name": "topthink/think-trace",
+            "version": "v1.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/top-think/think-trace.git",
+                "reference": "136cd5d97e8bdb780e4b5c1637c588ed7ca3e142"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/top-think/think-trace/zipball/136cd5d97e8bdb780e4b5c1637c588ed7ca3e142",
+                "reference": "136cd5d97e8bdb780e4b5c1637c588ed7ca3e142",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.0",
+                "topthink/framework": "^6.0|^8.0"
+            },
+            "type": "library",
+            "extra": {
+                "think": {
+                    "services": [
+                        "think\\trace\\Service"
+                    ],
+                    "config": {
+                        "trace": "src/config.php"
+                    }
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "think\\trace\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "liu21st",
+                    "email": "liu21st@gmail.com"
+                }
+            ],
+            "description": "thinkphp debug trace",
+            "time": "2023-02-07T08:36:32+00:00"
+        }
+    ],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": {
+        "php": ">=7.1.0"
+    },
+    "platform-dev": []
+}

+ 51 - 0
config/app.php

@@ -0,0 +1,51 @@
+<?php
+// +----------------------------------------------------------------------
+// | 应用设置
+// +----------------------------------------------------------------------
+
+return [
+    'app_name' =>env('app.name', 'Raingad-IM'),
+    'app_logo' =>env('app.logo', env('app.host', '')."/static/common/img/uniapp.png"),
+    'app_version' => env('app.version', '5.3.1'),
+    'app_release' =>env('app.release', '20241127'),
+    // 应用地址
+    'app_host'         => env('app.host', ''),
+    // 应用的命名空间
+    'app_namespace'    => '',
+    // 是否启用路由
+    'with_route'       => true,
+    'app_express'    =>    true,
+    // 默认应用
+    'default_app'      => 'index',
+    // 默认时区
+    'default_timezone' => env('app.default_timezone', 'Asia/Shanghai'),
+
+    // 应用映射(自动多应用模式有效)
+    'app_map'          => [],
+    // 域名绑定(自动多应用模式有效)
+    'domain_bind'      => [],
+    // 禁止URL访问的应用列表(自动多应用模式有效)
+    'deny_app_list'    => [],
+
+    // 异常页面的模板文件
+    'exception_tmpl'   => app()->getThinkPath() . 'tpl/think_exception.tpl',
+
+    // 错误显示信息,非调试模式有效
+    'error_message'    => '页面错误!请稍后再试~',
+    // 显示错误信息
+    'show_error_msg'   => false,
+    'auto_multi_app' =>true,
+     //用户token加密用的秘钥
+     'aes_token_key' => env('AES_TOKEN_KEY', ''),
+     //用户LOGIN加密用的秘钥
+     'aes_login_key' => env('AES_LOGIN_KEY', ''),
+     //用户chat加密用的秘钥
+     'aes_chat_key' => env('AES_CHAT_KEY', ''),
+    //  接口加密用的秘钥
+     'app_id' => env('APP_ID', ''),
+     'app_secret' => env('APP_SECRET', ''),
+     'api_status' => env('APP_API_STATUS', true),
+     //thinkAPI的令牌
+     'thinkapi_token' => env('APP_THINKAPI_TOKEN', ''),
+];
+

+ 38 - 0
config/cache.php

@@ -0,0 +1,38 @@
+<?php
+
+// +----------------------------------------------------------------------
+// | 缓存设置
+// +----------------------------------------------------------------------
+
+return [
+    // 默认缓存驱动
+    'default' => env('cache.driver', 'redis'),
+
+    // 缓存连接方式配置
+    'stores'  => [
+        'file' => [
+            // 驱动方式
+            'type'       => 'File',
+            // 缓存保存目录
+            'path'       => '',
+            // 缓存前缀
+            'prefix'     => '',
+            // 缓存有效期 0表示永久缓存
+            'expire'     => 0,
+            // 缓存标签前缀
+            'tag_prefix' => 'tag:',
+            // 序列化机制 例如 ['serialize', 'unserialize']
+            'serialize'  => [],
+        ],
+        'redis'                  => [
+            // 驱动方式
+            'type'   => 'redis',
+            'host'   =>env('redis.host', '127.0.0.1'),
+            'port'   => env('redis.port', '6379'),
+            'password' => env('redis.password', ''),
+            // 缓存前缀
+            'prefix' => env('redis.prefix', ''),
+        ]
+        // 更多的缓存连接
+    ],
+];

+ 19 - 0
config/captcha.php

@@ -0,0 +1,19 @@
+<?php
+
+
+return [
+    // 验证码字符集合
+    'codeSet'  => '2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY',
+    // 验证码字体大小(px)
+    'fontSize' => 20,
+    // 是否画混淆曲线
+    'useCurve' => false,
+    // 验证码图片高度
+    'imageH'   => 40,
+    // 验证码图片宽度
+    'imageW'   => 150,
+    // 验证码位数
+    'length'   => 4,
+    // 验证成功后是否重置
+    'reset'    => true
+];

+ 14 - 0
config/console.php

@@ -0,0 +1,14 @@
+<?php
+// +----------------------------------------------------------------------
+// | 控制台配置
+// +----------------------------------------------------------------------
+return [
+    // 指令定义
+    'commands' => [
+        'queue:work' => think\queue\command\Work::class,
+        'queue:listen' => think\queue\command\Listen::class,
+        'queue:Restart' => think\queue\command\Restart::class,
+        'task' => task\command\Task::class,
+        'worker:gateway' => app\worker\command\GatewayWorker::class
+    ],
+];

+ 18 - 0
config/cookie.php

@@ -0,0 +1,18 @@
+<?php
+// +----------------------------------------------------------------------
+// | Cookie设置
+// +----------------------------------------------------------------------
+return [
+    // cookie 保存时间
+    'expire'    => 0,
+    // cookie 保存路径
+    'path'      => '/',
+    // cookie 有效域名
+    'domain'    => '',
+    //  cookie 启用安全传输
+    'secure'    => false,
+    // httponly设置
+    'httponly'  => false,
+    // 是否使用 setcookie
+    'setcookie' => true,
+];

+ 8 - 0
config/cron.php

@@ -0,0 +1,8 @@
+<?php
+
+return [
+    'tasks' => [
+        \app\common\task\ClearMessage::class, //定时清理消息
+        \app\common\task\SetAtRead::class, //定时清理@消息
+        ]
+];

+ 62 - 0
config/database.php

@@ -0,0 +1,62 @@
+<?php
+
+return [
+    // 默认使用的数据库连接配置
+    'default'         => env('database.driver', 'mysql'),
+
+    // 自定义时间查询规则
+    'time_query_rule' => [],
+
+    // 自动写入时间戳字段
+    // true为自动识别类型 false关闭
+    // 字符串则明确指定时间字段类型 支持 int timestamp datetime date
+    'auto_timestamp'  => true,
+
+    // 时间字段取出后的默认时间格式
+    'datetime_format' => 'Y-m-d H:i:s',
+
+    // 数据库连接配置信息
+    'connections'     => [
+        'mysql' => [
+            // 数据库类型
+            'type'              => env('database.type', 'mysql'),
+            // 服务器地址
+            'hostname'          => env('database.hostname', '127.0.0.1'),
+            // 数据库名
+            'database'          => env('database.database', ''),
+            // 用户名
+            'username'          => env('database.username', 'root'),
+            // 密码
+            'password'          => env('database.password', ''),
+            // 端口
+            'hostport'          => env('database.hostport', '3306'),
+            // 数据库连接参数
+            'params'            => [],
+            // 数据库编码默认采用utf8
+            'charset'           => env('database.charset', 'utf8'),
+            // 数据库表前缀
+            'prefix'            => env('database.prefix', ''),
+
+            // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
+            'deploy'            => 0,
+            // 数据库读写是否分离 主从式有效
+            'rw_separate'       => false,
+            // 读写分离后 主服务器数量
+            'master_num'        => 1,
+            // 指定从服务器序号
+            'slave_no'          => '',
+            // 是否严格检查字段是否存在
+            'fields_strict'     => true,
+            // 是否需要断线重连
+            'break_reconnect'   => false,
+            // 监听SQL
+            'trigger_sql'       => env('app_debug', true),
+            // 开启字段缓存
+            'fields_cache'      => false,
+            // 字段缓存路径
+            'schema_cache_path' => app()->getRuntimePath() . 'schema' . DIRECTORY_SEPARATOR,
+        ],
+
+        // 更多的数据库配置信息
+    ],
+];

+ 42 - 0
config/filesystem.php

@@ -0,0 +1,42 @@
+<?php
+
+return [
+    // 默认磁盘
+    'default' => env('filesystem.driver', 'local'),
+    // 磁盘列表
+    'disks'   => [
+        'local'  => [
+            'type' => 'local',
+            'root'       => app()->getRootPath() . 'public/storage',
+        ],
+        // 更多的磁盘配置信息
+        'aliyun' => [
+            'type'         => 'aliyun',
+            'accessId'     => env('filesystem.aliyun_accessId',''),
+            'accessSecret' => env('filesystem.aliyun_accessSecret',''),
+            'bucket'       => env('filesystem.aliyun_bucket',''),
+            'endpoint'     => env('filesystem.aliyun_endpoint',''),
+            'url'          => env('filesystem.aliyun_url',''),//不要斜杠结尾,此处为URL地址域名。
+        ],
+        'qiniu'  => [
+            'type'      => 'qiniu',
+            'accessKey' => env('filesystem.qiniu_accessKey',''),
+            'secretKey' => env('filesystem.qiniu_secretKey',''),
+            'bucket'    => env('filesystem.qiniu_bucket',''),
+            'url'       => env('filesystem.qiniu_url',''),//不要斜杠结尾,此处为URL地址域名。
+        ],
+        'qcloud' => [
+            'type'       => 'qcloud',
+            'region'      => env('filesystem.qcloud_region',''),//bucket 所属区域 英文
+            'appId'      => env('filesystem.qcloud_appId',''), // 域名中数字部分
+            'secretId'   => env('filesystem.qcloud_secretId',''),
+            'secretKey'  => env('filesystem.qcloud_secretKey',''),
+            'bucket'          => env('filesystem.qcloud_bucket',''),
+            'timeout'         => 60,
+            'connect_timeout' => 60,
+            'cdn'             => env('filesystem.qcloud_cdn',''),
+            'scheme'          => 'https',
+            'read_from_cdn'   => false,
+        ]
+    ],
+];

+ 33 - 0
config/gateway.php

@@ -0,0 +1,33 @@
+<?php
+return [
+    // 扩展自身需要的配置
+    'protocol'              => 'websocket', // 协议 支持 tcp udp unix http websocket text
+    'host'                  => '0.0.0.0', // 监听地址
+    'port'                  => env('worker_port',8282), // 监听端口
+    'socket'                => '', // 完整监听地址
+    'context'               => [], // socket 上下文选项
+    'register_deploy'       => env('worker_register_deploy',true), // 是否需要部署register
+    'businessWorker_deploy' => true, // 是否需要部署businessWorker
+    'gateway_deploy'        => true, // 是否需要部署gateway
+
+    // Register配置
+    'registerAddress'       => env('worker_register_address','127.0.0.1:1236'),
+
+    // Gateway配置
+    'name'                  => env('worker_name','pushGateWay'),
+    'count'                 => env('worker_count',1),
+    'lanIp'                 => env('worker_lan_ip','127.0.0.1'),
+    'startPort'             => env('worker_start_port',2300),
+    'daemonize'             => false,
+    'pingInterval'          => 20,
+    'pingNotResponseLimit'  => 0,
+    'pingData'              => '{"type":"ping"}',
+
+    // BusinsessWorker配置
+    'businessWorker'        => [
+        'name'         => 'BusinessWorker',
+        'count'        => 1,
+        'eventHandler' => 'app\worker\Events',
+    ],
+
+];

+ 16 - 0
config/hashids.php

@@ -0,0 +1,16 @@
+<?php
+/**
+ * tpAdmin [a web admin based ThinkPHP5]
+ *
+ * @author yuan1994 <tianpian0805@gmail.com>
+ * @link http://tpadmin.yuan1994.com/
+ * @copyright 2016 yuan1994 all rights reserved.
+ * @license http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+return [
+    // Hashids 的配置项
+    'length'   => 12, // 加密字符串长度
+    'salt'     => 'raingads', // 加密盐值
+    'alphabet' => '', // 字符仓库,不填写默认为扩展里的字符仓库
+];

+ 21 - 0
config/jwt.php

@@ -0,0 +1,21 @@
+<?php
+
+
+return [
+    'secret'      => env('JWT_SECRET'),
+    //Asymmetric key
+    'public_key'  => env('JWT_PUBLIC_KEY'),
+    'private_key' => env('JWT_PRIVATE_KEY'),
+    'password'    => env('JWT_PASSWORD'),
+    //JWT time to live
+    'ttl'         => env('JWT_TTL', 60),
+    //Refresh time to live
+    'refresh_ttl' => env('JWT_REFRESH_TTL', 20160),
+    //JWT hashing algorithm
+    'algo'        => env('JWT_ALGO', 'HS256'),
+    //token获取方式,数组靠前值优先
+    'token_mode'    => ['header', 'cookie', 'param'],
+    //黑名单后有效期
+    'blacklist_grace_period' => env('BLACKLIST_GRACE_PERIOD', 10),
+    'blacklist_storage' => thans\jwt\provider\storage\Tp6::class,
+];

+ 33 - 0
config/lang.php

@@ -0,0 +1,33 @@
+<?php
+// +----------------------------------------------------------------------
+// | 多语言设置
+// +----------------------------------------------------------------------
+
+return [
+    // 默认语言
+    'default_lang'    => env('lang.default_lang', 'zh-cn'),
+    // 允许的语言列表
+    'allow_lang_list' => [],
+    // 多语言自动侦测变量名
+    'detect_var'      => 'lang',
+    // 是否使用Cookie记录
+    'use_cookie'      => true,
+    // 多语言cookie变量
+    'cookie_var'      => 'think_lang',
+    // 扩展语言包
+    'extend_list'     => [
+        'zh-cn'    => [
+            app()->getBasePath() . 'lang/zh_cn.php',
+        ],
+        'en-us'    => [
+            app()->getBasePath() . 'lang/en_us.php',
+        ],
+    ],
+    // Accept-Language转义为对应语言包名称
+    'accept_language' => [
+        'zh-Hans' => 'zh-cn',
+        'en' => 'en-us',
+    ],
+    // 是否支持语言分组
+    'allow_group'     => true,
+];

+ 45 - 0
config/log.php

@@ -0,0 +1,45 @@
+<?php
+
+// +----------------------------------------------------------------------
+// | 日志设置
+// +----------------------------------------------------------------------
+return [
+    // 默认日志记录通道
+    'default'      => env('log.channel', 'file'),
+    // 日志记录级别
+    'level'        => [],
+    // 日志类型记录的通道 ['error'=>'email',...]
+    'type_channel' => [],
+    // 关闭全局日志写入
+    'close'        => false,
+    // 全局日志处理 支持闭包
+    'processor'    => null,
+
+    // 日志通道列表
+    'channels'     => [
+        'file' => [
+            // 日志记录方式
+            'type'           => 'File',
+            // 日志保存目录
+            'path'           => '',
+            // 单文件日志写入
+            'single'         => false,
+            // 独立日志级别
+            'apart_level'    => [],
+            // 最大日志文件数量
+            'max_files'      => 0,
+            // 使用JSON格式记录
+            'json'           => false,
+            // 日志处理
+            'processor'      => null,
+            // 关闭通道日志写入
+            'close'          => false,
+            // 日志输出格式化
+            'format'         => '[%s][%s] %s',
+            // 是否实时写入
+            'realtime_write' => false,
+        ],
+        // 其它日志通道配置
+    ],
+
+];

+ 13 - 0
config/middleware.php

@@ -0,0 +1,13 @@
+<?php
+// 中间件配置
+return [
+    // 别名或分组
+    'alias'    => [
+        'checkAuth'=>app\common\middleware\CheckAuth::class,
+        'manageAuth'=>app\common\middleware\ManageAuth::class,
+        'apiAuth'=>app\common\middleware\ApiAuth::class,
+        'locale'=>app\common\middleware\Locale::class,
+    ],
+    // 优先级设置,此数组中的中间件会按照数组中的顺序优先执行
+    'priority' => [],
+];

+ 8 - 0
config/oss.php

@@ -0,0 +1,8 @@
+<?php
+return [
+    'accessKeyId'=>env('oss.accesskeyid', ''),
+    'accessKeySecret'=>env('oss.accesskeysecret', ''),
+    'endpoint'=>env('oss.endpoint', ''),
+    'bucket'=>env('oss.bucket', ''),
+    'ossUrl'=>env('oss.ossurl', '')
+];

+ 6 - 0
config/preview.php

@@ -0,0 +1,6 @@
+<?php
+return [
+    'own'=>env('preview.own', ''),
+    'yzdcs'=>env('preview.yzdcs', ''),
+    'keycode'=>env('preview.keycode', ''),
+];

+ 41 - 0
config/queue.php

@@ -0,0 +1,41 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+return [
+    'default'     => 'redis',
+    'connections' => [
+        'sync'     => [
+            'type' => 'sync',
+        ],
+        'database' => [
+            'type'       => 'database',
+            'queue'      => 'default',
+            'table'      => 'jobs',
+            'connection' => null,
+        ],
+        'redis'    => [
+            'type'       => 'redis',
+            'queue'      => 'default',
+            'host'   =>env('redis.host', '127.0.0.1'),
+            'port'   => env('redis.port', '6379'),
+            'password' => env('redis.password', ''),
+            // 缓存前缀
+            'prefix' => env('redis.prefix', ''),
+            'select'     => 0,
+            'timeout'    => 0,
+            'persistent' => false,
+        ],
+    ],
+    'failed'      => [
+        'type'  => 'none',
+        'table' => 'failed_jobs',
+    ],
+];

+ 45 - 0
config/route.php

@@ -0,0 +1,45 @@
+<?php
+// +----------------------------------------------------------------------
+// | 路由设置
+// +----------------------------------------------------------------------
+
+return [
+    // pathinfo分隔符
+    'pathinfo_depr'         => '/',
+    // URL伪静态后缀
+    'url_html_suffix'       => 'html',
+    // URL普通方式参数 用于自动生成
+    'url_common_param'      => true,
+    // 是否开启路由延迟解析
+    'url_lazy_route'        => false,
+    // 是否强制使用路由
+    'url_route_must'        => false,
+    // 合并路由规则
+    'route_rule_merge'      => false,
+    // 路由是否完全匹配
+    'route_complete_match'  => false,
+    // 访问控制器层名称
+    'controller_layer'      => 'controller',
+    // 空控制器名
+    'empty_controller'      => 'Error',
+    // 是否使用控制器后缀
+    'controller_suffix'     => false,
+    // 默认的路由变量规则
+    'default_route_pattern' => '[\w\.]+',
+    // 是否开启请求缓存 true自动缓存 支持设置请求缓存规则
+    'request_cache_key'     => false,
+    // 请求缓存有效期
+    'request_cache_expire'  => null,
+    // 全局请求缓存排除规则
+    'request_cache_except'  => [],
+    // 默认控制器名
+    'default_controller'    => 'Index',
+    // 默认操作名
+    'default_action'        => 'index',
+    // 操作方法后缀
+    'action_suffix'         => '',
+    // 默认JSONP格式返回的处理方法
+    'default_jsonp_handler' => 'jsonpReturn',
+    // 默认JSONP处理方法
+    'var_jsonp_handler'     => 'callback',
+];

+ 19 - 0
config/session.php

@@ -0,0 +1,19 @@
+<?php
+// +----------------------------------------------------------------------
+// | 会话设置
+// +----------------------------------------------------------------------
+
+return [
+    // session name
+    'name'           => 'PHPSESSID',
+    // SESSION_ID的提交变量,解决flash上传跨域
+    'var_session_id' => '',
+    // 驱动方式 支持file cache
+    'type'           => 'file',
+    // 存储连接标识 当type使用cache的时候有效
+    'store'          => null,
+    // 过期时间
+    'expire'         => 9*3600,
+    // 前缀
+    'prefix'         => '',
+];

+ 159 - 0
config/sms.php

@@ -0,0 +1,159 @@
+<?php
+// +----------------------------------------------------------------------
+// | 胜家云 [ SingKa Cloud ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.singka.net All rights reserved.
+// +----------------------------------------------------------------------
+// | 宁波晟嘉网络科技有限公司
+// +----------------------------------------------------------------------
+// | Author: ShyComet <shycomet@qq.com>
+// +----------------------------------------------------------------------
+return [
+    'driver'       => 'aliyun', // 驱动器
+    'aliyun'       => [
+        'version'       => '2017-05-25',
+        'host'          => 'dysmsapi.aliyuncs.com',
+        'scheme'        => 'http',
+        'region_id'     => 'cn-hangzhou',
+        'access_key'    => '',
+        'access_secret' => '',
+        'sign_name'     => '',
+        'actions'       => [
+            'register'        => [
+                'actions_name'      => '注册验证',
+                'template_id'  => 'SMS_53115055',
+            ],
+            'login'           => [
+                'actions_name'      => '登录验证',
+                'template_id'  => 'SMS_53115057',
+            ],
+            'changePassword' => [
+                'actions_name'      => '修改密码',
+                'template_id'  => 'SMS_53115053',
+            ],
+            'changeUserinfo' => [
+                'actions_name'      => '变更信息',
+                'template_id'  => 'SMS_53115052',
+            ],
+        ],
+    ],
+    'ucloud'       => [
+        'public_key'   =>  '',
+        'private_key'  =>  '',
+        'project_id'   =>  '',
+        'base_url'     =>  'https://api.ucloud.cn',
+        'sign_name'       => '',
+        'actions'       => [
+            'register'        => [
+                'actions_name'      => '注册验证',
+                'template_id'  => 'UTA1910164E29F4',
+            ],
+            'login'           => [
+                'actions_name'      => '登录验证',
+                'template_id'  => 'UTA1910164E29F4',
+            ],
+            'changePassword' => [
+                'actions_name'      => '修改密码',
+                'template_id'  => 'UTA1910164E29F4',
+            ],
+            'changeUserinfo' => [
+                'actions_name'      => '变更信息',
+                'template_id'  => 'UTA1910164E29F4',
+            ],
+        ],
+    ],
+    'qcloud'       => [
+        'appid'   =>  '',
+        'appkey'  =>  '',
+        'sign_name'       => '',
+        'actions'       => [
+            'register'        => [
+                'actions_name'      => '注册验证',
+                'template_id'  => '566198',
+            ],
+            'login'           => [
+                'actions_name'      => '登录验证',
+                'template_id'  => '566197',
+            ],
+            'changePassword' => [
+                'actions_name'      => '修改密码',
+                'template_id'  => '566199',
+            ],
+            'changeUserinfo' => [
+                'actions_name'      => '变更信息',
+                'template_id'  => '566200',
+            ],
+        ],
+    ],
+    'qiniu'       => [
+        'AccessKey'   =>  '',
+        'SecretKey'  =>  '',
+        'actions'       => [
+            'register'        => [
+                'actions_name'      => '注册验证',
+                'template_id'  => '1246849772845797376',
+            ],
+            'login'           => [
+                'actions_name'      => '登录验证',
+                'template_id'  => '1246849654881001472',
+            ],
+            'changePassword' => [
+                'actions_name'      => '修改密码',
+                'template_id'  => '1246849964902977536',
+            ],
+            'changeUserinfo' => [
+                'actions_name'      => '变更信息',
+                'template_id'  => '1246849860733243392',
+            ],
+        ],
+    ],
+    'upyun'       => [
+        'id'   =>  '',
+        'token'  =>  '',
+        'apiurl'  =>  '',
+        'actions'       => [
+            'register'        => [
+                'actions_name'      => '注册验证',
+                'template_id'  => '2591',
+            ],
+            'login'           => [
+                'actions_name'      => '登录验证',
+                'template_id'  => '2592',
+            ],
+            'changePassword' => [
+                'actions_name'      => '修改密码',
+                'template_id'  => '2590',
+            ],
+            'changeUserinfo' => [
+                'actions_name'      => '变更信息',
+                'template_id'  => '2589',
+            ],
+        ],
+    ],
+    'huawei'       => [
+        'url'  =>  '',
+        'appKey'   =>  '',
+        'appSecret'  =>  '',
+        'sender'  =>  '',
+        'signature'  =>  '',
+        'statusCallback'  =>  '',
+        'actions'       => [
+            'register'        => [
+                'actions_name'      => '注册验证',
+                'template_id'  => '2591',
+            ],
+            'login'           => [
+                'actions_name'      => '登录验证',
+                'template_id'  => '2592',
+            ],
+            'changePassword' => [
+                'actions_name'      => '修改密码',
+                'template_id'  => '2590',
+            ],
+            'changeUserinfo' => [
+                'actions_name'      => '变更信息',
+                'template_id'  => '2589',
+            ],
+        ],
+    ]
+];

+ 10 - 0
config/trace.php

@@ -0,0 +1,10 @@
+<?php
+// +----------------------------------------------------------------------
+// | Trace设置 开启调试模式后有效
+// +----------------------------------------------------------------------
+return [
+    // 内置Html和Console两种方式 支持扩展
+    'type'    => 'Html',
+    // 读取的日志通道名
+    'channel' => '',
+];

+ 45 - 0
config/version.php

@@ -0,0 +1,45 @@
+<?php
+# app_name 应用名称,所有的安装包都是用该名称命名,不能用中文!!!!!!!!!!!!
+
+# VERSION:移动端app版本信息
+# RELEASE:根据该参数确定版本,版本比移动罐的版本大就会提示更新,苹果和ios通过这个检测
+# UPDATE_TYPE :forcibly 强制更新, solicit弹窗确认更新, silent 静默更新 
+# UPDATE_INFO :更新说明,换行用\n 
+return [
+    'app_name'=>env('app.name', 'Raingad-IM'),  //在.env中配置
+    'android' => [
+        'version' => env('app.version', '6.2.0'),  //在.env中配置
+        'release' => env('app.release', '20251204'), //在.env中配置
+        'url' =>env('app.android_webclip',''),
+        'update_info' => '1.新增国际化,优化大量UI\n2.新增新的保活插件\n3.聊天记录采用虚拟列表,提升性能\n4.修复若干BUG',
+        'update_type' => 'solicit',
+    ],
+    'ios' => [
+        'version' => env('app.version', '6.0.0'),  //在.env中配置
+        'release' => env('app.release', '20250520'), //在.env中配置
+        'url' => env('app.ios_webclip',''),
+        'update_info' => '暂无',
+        'update_type' => 'solicit',
+    ],
+    'windows' => [
+        'version' => '6.2.0',
+        'release' => '20251204',
+        'url' => env('app.win_webclip',''),
+        'update_info' => '1.lemon-imui本地化,消息底部检测',
+        'update_type' => 'solicit',
+    ],
+    'mac' => [
+        'version' => '4.0.0',
+        'release' => '20240323',
+        'url' => env('app.mac_webclip',''),
+        'update_info' => '1.修复了一些bug\n2.优化了一些功能',
+        'update_type' => 'solicit',
+    ],
+    'serve' => [
+        'version' => '5.2.2',
+        'release' => '20241118',
+        'url' => '',
+        'update_info' => '1.增加聊天记录查看\n2.增加系统公告,移动端首页滚动提醒\n3.增加群聊支持单个人禁言,支持新成员查看历史聊天记录\n4.优化移动端输入框,解决ios低版本H5问题\n5.修复若干BUG',
+        'update_type' => 'solicit',
+    ],
+];

+ 28 - 0
config/view.php

@@ -0,0 +1,28 @@
+<?php
+// +----------------------------------------------------------------------
+// | 模板设置
+// +----------------------------------------------------------------------
+
+return [
+    // 模板引擎类型使用Think
+    'type'          => 'Think',
+    // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法
+    'auto_rule'     => 1,
+    // 模板目录名
+    'view_dir_name' => 'view',
+    // 模板后缀
+    'view_suffix'   => 'html',
+    // 模板文件名分隔符
+    'view_depr'     => DIRECTORY_SEPARATOR,
+    // 模板引擎普通标签开始标记
+    'tpl_begin'     => '{',
+    // 模板引擎普通标签结束标记
+    'tpl_end'       => '}',
+    // 标签库标签开始标记
+    'taglib_begin'  => '{',
+    // 标签库标签结束标记
+    'taglib_end'    => '}',
+    'tpl_replace_string'  =>  [
+        '__STATIC__'=>'/static',
+    ]
+];

+ 12 - 0
crontab.txt

@@ -0,0 +1,12 @@
+2024-01-17 14:17:58:任务开始执行
+2024-01-17 14:17:58:设置已读成功
+2024-01-17 14:18:58:任务开始执行
+2024-01-17 14:18:58:设置已读成功
+2024-01-17 14:19:58:任务开始执行
+2024-01-17 14:19:58:设置已读成功
+2024-01-17 14:20:58:任务开始执行
+2024-01-17 14:20:58:设置已读成功
+2024-01-17 14:21:58:任务开始执行
+2024-01-17 14:21:58:设置已读成功
+2024-01-17 14:22:58:任务开始执行
+2024-01-17 14:22:58:设置已读成功

+ 114 - 0
example.env

@@ -0,0 +1,114 @@
+APP_DEBUG = true
+
+[APP]
+NAME = IM
+LOGO = 
+# 如果安卓和ios不同步更新,请到/config/version.php中单独设置版本号
+VERSION = 6.1.0
+RELEASE = 20250916
+# 主域名必填,例如:https://im.example.com
+HOST = 
+DEFAULT_TIMEZONE = Asia/Shanghai
+#开放api的开关
+API_STATUS = true
+#开放api接口的配置信息
+ID = a1b2c3d4e5f
+SECRET = 1234sddsc56ssdfghjkl
+
+# thinkapi的令牌,目前只用于敏感词过滤,其他接口自行接入
+THINKAPI_TOKEN =
+
+# 下载页分发链接
+DOWNAPP_URL = 
+# 安卓包名,如果上架了市场,根据市场ID跳转市场
+ANDROID_APPID = 
+#安卓下载地址,如果未设置会检测根目录是否有app.apk
+ANDROID_WEBCLIP =
+#APPSTORE市场ID
+IOS_APPID =
+#IOS下载地址,如果没有市场的ID则使用下载地址,如果用于更新可以放苹果市场的链接地址
+IOS_WEBCLIP =
+#windows下载地址
+WIN_WEBCLIP = 
+#mac下载地址
+MAC_WEBCLIP = 
+
+[DATABASE]
+TYPE = mysql
+HOSTNAME = 127.0.0.1
+DATABASE = im
+USERNAME = root
+PASSWORD = My01020304
+HOSTPORT = 3306
+CHARSET = utf8mb4
+DEBUG = true
+prefix = yu_
+[LANG]
+default_lang = zh-cn
+
+[REDIS]
+HOST = 127.0.0.1
+PORT = 6379
+PASSWORD =
+PREFIX = 
+
+视频封面截取配置,需要单独安装,宝塔安装默认地址为/www/server/ffmpeg/ffmpeg-6.1
+[FFMPEG]
+BIN_PATH =
+
+[AES]
+TOKEN_KEY = tHTisd8USApxsdfnhTM
+LOGIN_KEY = t2fe6HddMnmssswDVi2
+#最后是自定义自己能记的,不要太长,不要太短,不要太简单,不要太复杂,不要太难记,一旦确定之后就不需要再修改。否者无法解析聊天记录,开启后聊天记录不可被搜索
+CHAT_KEY  =
+
+[JWT]
+SECRET = 17b190sdfcxw321f94f57325ae5a8b4c
+TTL = 2592000
+
+
+[WORKER]
+NAME = businessWorker
+PORT = 8282
+# 根据自己的核心数而配置
+COUNT = 1
+START_PORT = 2300
+REGISTER_ADDRESS =127.0.0.1:1236
+lAN_IP = 127.0.0.1
+# 分部署部署只需要启动一个gateway,其他的gateway只需要配置register_address即可
+REGISTER_DEPLOY = true
+
+#配置预览功能,本系统主要使用第三方的预览工具,比如永中云转换,自带预览系统
+[PREVIEW]
+# 自带预览系统URL,主要用于预览媒体文件,已内置,必须要有最后的/斜杠
+own=
+# 永中云文件预览,主要用于文档预览,必须要有最后的/斜杠
+yzdcs=http://domain/
+# 永中云api code
+keycode=17444844212312
+
+[UNIPUSH]
+# unipush的云函数转url地址,主要用于推送
+URL=
+# unipush直接推送通知栏还是app接收后再创建通知栏
+IS_FORCE=false
+
+# 配置对象储存,主要用于聊天文件储存,可以通过后台进行配置
+
+[FILESYSTEM]
+driver=local
+aliyun_accessId=false
+aliyun_accessSecret=false
+aliyun_bucket=false
+aliyun_endpoint=false
+aliyun_url=false
+qiniu_accessKey=false
+qiniu_secretKey=false
+qiniu_bucket=false
+qiniu_url=false
+qcloud_region=false
+qcloud_appId=false
+qcloud_secretId=false
+qcloud_secretKey=false
+qcloud_bucket=false
+qcloud_cdn=false

+ 2 - 0
extend/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 7 - 0
public/404.html

@@ -0,0 +1,7 @@
+<html>
+<head><title>404 Not Found</title></head>
+<body>
+<center><h1>404 Not Found</h1></center>
+<hr><center>nginx</center>
+</body>
+</html>

+ 1 - 0
public/assets/css/133.7f9367b6.css

@@ -0,0 +1 @@
+.error-wrapper{position:absolute;top:40%;left:50%;transform:translate(-50%,-50%)}.error-wrapper .error-content .pic-error{float:left;width:120%;overflow:hidden;opacity:0;animation-name:slideUp;animation-duration:.5s;animation-delay:.3s;animation-fill-mode:forwards}.error-wrapper .error-content .pic-error img{width:100%;height:100%}.error-wrapper .error-content .bullshit{position:relative;float:left;width:300px;padding:30px 0;overflow:hidden}.error-wrapper .error-content .bullshit-oops{margin-bottom:20px;font-size:32px;font-weight:700;line-height:40px;color:#175cff;opacity:0;animation-name:slideUp;animation-duration:.5s;animation-fill-mode:forwards}.error-wrapper .error-content .bullshit-headline{margin-bottom:10px;font-size:20px;font-weight:700;line-height:24px;color:#222;opacity:0;animation-name:slideUp;animation-duration:.5s;animation-delay:.1s;animation-fill-mode:forwards}.error-wrapper .error-content .bullshit-info{margin-bottom:30px;font-size:13px;line-height:21px;color:rgba(0,0,0,.65);opacity:0;animation-name:slideUp;animation-duration:.5s;animation-delay:.2s;animation-fill-mode:forwards}.error-wrapper .error-content .bullshit-return-home{display:block;float:left;width:110px;height:36px;font-size:14px;line-height:36px;color:#fff;text-align:center;cursor:pointer;background:#175cff;border-radius:100px;opacity:0;animation-name:slideUp;animation-duration:.5s;animation-delay:.3s;animation-fill-mode:forwards}@keyframes slideUp{0%{opacity:0;transform:translateY(60px)}to{opacity:1;transform:translateY(0)}}

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
public/assets/css/547.abaee743.css


+ 1 - 0
public/assets/css/567.08ec972a.css

@@ -0,0 +1 @@
+.main-file-item{height:calc(100vh - 102px)}

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
public/assets/css/687.d8d9e482.css


+ 1 - 0
public/assets/css/761.338eef9a.css

@@ -0,0 +1 @@
+.im-statistic-title[data-v-1052b203]{font-size:12px;color:#999;margin-bottom:10px;display:flex;align-items:center}.im-statistic-tips[data-v-1052b203]{margin-left:5px}.im-statistic-content[data-v-1052b203]{font-size:20px;color:#333}.im-statistic-content-value[data-v-1052b203]{font-weight:700}.im-statistic-content-prefix[data-v-1052b203]{margin-right:5px}.im-statistic-content-suffix[data-v-1052b203]{margin-left:5px;font-size:12px}.im-statistic-description[data-v-1052b203]{margin-top:10px;color:#999}.dark .im-statistic-content[data-v-1052b203]{color:#d0d0d0}.item-background[data-v-f23837a6]{color:#666;line-height:1.8}.welcome .logo[data-v-f23837a6]{text-align:center}.welcome .logo img[data-v-f23837a6]{vertical-align:bottom;width:80px;height:80px;margin-bottom:20px}.welcome .logo h2[data-v-f23837a6]{font-size:24px;font-weight:400;display:flex;align-items:center;justify-content:center}.tips[data-v-f23837a6]{margin-top:20px}.tips-item[data-v-f23837a6]{display:flex;align-items:center;justify-content:center;padding-bottom:10px}.tips-item-icon[data-v-f23837a6]{width:40px;height:40px;display:flex;align-items:center;justify-content:center;border-radius:50%;font-size:18px;margin-right:20px;color:var(--el-color-primary);background:hsla(0,0%,71%,.1)}.tips-item-message[data-v-f23837a6]{flex:1;font-size:14px}.actions[data-v-f23837a6]{text-align:center;margin:20px 0 20px 0}.stop-task[data-v-f23837a6]:hover{color:#f56c6c}.start-task[data-v-f23837a6]{color:#409eff}.task-name[data-v-f23837a6]{font-weight:600}.task-log[data-v-f23837a6]{background:#000;color:#fff;white-space:break-spaces;letter-spacing:1px}

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
public/assets/css/789.62ab164a.css


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
public/assets/css/982.f6ad2033.css


Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff