zhangguidong 2 vuotta sitten
commit
26189ed751
100 muutettua tiedostoa jossa 21907 lisäystä ja 0 poistoa
  1. 10 0
      .bowerrc
  2. 22 0
      .env.sample
  3. 17 0
      .gitignore
  4. 1 0
      .user.ini
  5. 191 0
      LICENSE
  6. 1 0
      addons/.gitkeep
  7. 0 0
      addons/fastim/.addonrc
  8. 77 0
      addons/fastim/Fastim.php
  9. 36 0
      addons/fastim/bootstrap.js
  10. 258 0
      addons/fastim/command/Swoole.php
  11. 122 0
      addons/fastim/config.php
  12. 26 0
      addons/fastim/controller/Base.php
  13. 353 0
      addons/fastim/controller/Index.php
  14. 20 0
      addons/fastim/controller/api/Base.php
  15. 304 0
      addons/fastim/controller/api/User.php
  16. 128 0
      addons/fastim/data/menu.php
  17. 10 0
      addons/fastim/info.ini
  18. 482 0
      addons/fastim/install.sql
  19. 21 0
      addons/fastim/lang/api/zh-cn.php
  20. 28 0
      addons/fastim/lang/zh-cn.php
  21. 28 0
      addons/fastim/library/Captcha.php
  22. 2233 0
      addons/fastim/library/Common.php
  23. 96 0
      addons/fastim/library/CommonCode.php
  24. 50 0
      addons/fastim/library/controller/Base.php
  25. 829 0
      addons/fastim/library/controller/ImBase.php
  26. 1868 0
      addons/fastim/library/controller/Message.php
  27. 2343 0
      addons/fastim/library/controller/User.php
  28. 81 0
      addons/fastim/library/pushapi/GTBaseApi.php
  29. 135 0
      addons/fastim/library/pushapi/GTClient.php
  30. 86 0
      addons/fastim/library/pushapi/GTPushApi.php
  31. 48 0
      addons/fastim/library/pushapi/GTStatisticsApi.php
  32. 122 0
      addons/fastim/library/pushapi/GTUserApi.php
  33. 201 0
      addons/fastim/library/pushapi/LICENSE
  34. 4 0
      addons/fastim/library/pushapi/README.md
  35. 219 0
      addons/fastim/library/pushapi/UniPush.php
  36. 16 0
      addons/fastim/library/pushapi/composer.json
  37. 34 0
      addons/fastim/library/pushapi/exception/GTException.php
  38. 15 0
      addons/fastim/library/pushapi/request/GTApiRequest.php
  39. 48 0
      addons/fastim/library/pushapi/request/auth/GTAuthRequest.php
  40. 68 0
      addons/fastim/library/pushapi/request/push/GTAudienceRequest.php
  41. 52 0
      addons/fastim/library/pushapi/request/push/GTCondition.php
  42. 261 0
      addons/fastim/library/pushapi/request/push/GTNotification.php
  43. 50 0
      addons/fastim/library/pushapi/request/push/GTPushBatchRequest.php
  44. 48 0
      addons/fastim/library/pushapi/request/push/GTPushChannel.php
  45. 82 0
      addons/fastim/library/pushapi/request/push/GTPushMessage.php
  46. 189 0
      addons/fastim/library/pushapi/request/push/GTPushRequest.php
  47. 39 0
      addons/fastim/library/pushapi/request/push/GTRevoke.php
  48. 76 0
      addons/fastim/library/pushapi/request/push/GTSettings.php
  49. 138 0
      addons/fastim/library/pushapi/request/push/GTStrategy.php
  50. 29 0
      addons/fastim/library/pushapi/request/push/android/GTAndroid.php
  51. 129 0
      addons/fastim/library/pushapi/request/push/android/GTThirdNotification.php
  52. 95 0
      addons/fastim/library/pushapi/request/push/android/GTUps.php
  53. 187 0
      addons/fastim/library/pushapi/request/push/ios/GTAlert.php
  54. 90 0
      addons/fastim/library/pushapi/request/push/ios/GTAps.php
  55. 120 0
      addons/fastim/library/pushapi/request/push/ios/GTIos.php
  56. 54 0
      addons/fastim/library/pushapi/request/push/ios/GTMultimedia.php
  57. 38 0
      addons/fastim/library/pushapi/request/user/GTAliasRequest.php
  58. 39 0
      addons/fastim/library/pushapi/request/user/GTBadgeSetRequest.php
  59. 39 0
      addons/fastim/library/pushapi/request/user/GTCidAlias.php
  60. 41 0
      addons/fastim/library/pushapi/request/user/GTTagBatchSetRequest.php
  61. 34 0
      addons/fastim/library/pushapi/request/user/GTTagSetRequest.php
  62. 34 0
      addons/fastim/library/pushapi/request/user/GTUserQueryRequest.php
  63. 275 0
      addons/fastim/library/pushapi/test/PushApiTest.php
  64. 52 0
      addons/fastim/library/pushapi/test/StatisticsApiTest.php
  65. 192 0
      addons/fastim/library/pushapi/test/UserApiTest.php
  66. 104 0
      addons/fastim/library/pushapi/utils/GTConfig.php
  67. 116 0
      addons/fastim/library/pushapi/utils/GTHttpManager.php
  68. 609 0
      addons/fastim/library/swoole/Common.php
  69. 289 0
      addons/fastim/library/swoole/WebSocket.php
  70. 124 0
      addons/fastim/uniapp/App.vue
  71. 12 0
      addons/fastim/uniapp/common/config.js
  72. 10 0
      addons/fastim/uniapp/common/css/common.scss
  73. 36 0
      addons/fastim/uniapp/common/http.interceptor.js
  74. 2728 0
      addons/fastim/uniapp/common/websocket.js
  75. 37 0
      addons/fastim/uniapp/components/common/common.vue
  76. 44 0
      addons/fastim/uniapp/components/link-message/link-message.vue
  77. 238 0
      addons/fastim/uniapp/components/message/message.vue
  78. 118 0
      addons/fastim/uniapp/components/session/session.vue
  79. 272 0
      addons/fastim/uniapp/js_sdk/wa-permission/permission.js
  80. 23 0
      addons/fastim/uniapp/main.js
  81. 130 0
      addons/fastim/uniapp/manifest.json
  82. 284 0
      addons/fastim/uniapp/pages.json
  83. 207 0
      addons/fastim/uniapp/pages/address-list/address-list.vue
  84. 112 0
      addons/fastim/uniapp/pages/address-list/group-chat.vue
  85. 111 0
      addons/fastim/uniapp/pages/address-list/top-contacts.vue
  86. 407 0
      addons/fastim/uniapp/pages/center/collection.vue
  87. 363 0
      addons/fastim/uniapp/pages/center/edit-info.vue
  88. 119 0
      addons/fastim/uniapp/pages/center/edit-quick-reply.vue
  89. 170 0
      addons/fastim/uniapp/pages/center/general-settings.vue
  90. 278 0
      addons/fastim/uniapp/pages/center/group_chat_notice.vue
  91. 130 0
      addons/fastim/uniapp/pages/center/group_chat_notice_users.vue
  92. 195 0
      addons/fastim/uniapp/pages/center/group_chat_users.vue
  93. 150 0
      addons/fastim/uniapp/pages/center/index.vue
  94. 317 0
      addons/fastim/uniapp/pages/center/info.vue
  95. 258 0
      addons/fastim/uniapp/pages/center/login.vue
  96. 181 0
      addons/fastim/uniapp/pages/center/post_group_chat_notice.vue
  97. 191 0
      addons/fastim/uniapp/pages/center/privacy-settings.vue
  98. 139 0
      addons/fastim/uniapp/pages/center/quick-reply.vue
  99. 209 0
      addons/fastim/uniapp/pages/center/register.vue
  100. 182 0
      addons/fastim/uniapp/pages/center/report.vue

+ 10 - 0
.bowerrc

@@ -0,0 +1,10 @@
+{
+  "directory": "public/assets/libs",
+  "ignoredDependencies": [
+    "es6-promise",
+    "file-saver",
+    "html2canvas",
+    "jspdf",
+    "jspdf-autotable"
+  ]
+}

+ 22 - 0
.env.sample

@@ -0,0 +1,22 @@
+[app]
+debug = false
+trace = false
+
+[database]
+hostname = 127.0.0.1
+database = fastadmin
+username = root
+password = root
+hostport = 3306
+prefix = fa_
+
+[main_token]
+key=bB85gKhrdz4Ri2SuLY1HkM9lxQIAqj0W
+hashalgo=ripemd160
+db_host_name=127.0.0.1
+db_host_database=pet
+db_host_username=root
+db_host_password=root
+
+[map]
+key=YV7BZ-7IFWV-GWDPV-U2C5Q-2XNT5-MNBY6

+ 17 - 0
.gitignore

@@ -0,0 +1,17 @@
+/nbproject/
+/thinkphp/
+/vendor/
+/runtime/*
+/application/admin/command/Install/*.lock
+/public/uploads/*
+.idea
+composer.lock
+*.log
+*.css.map
+!.gitkeep
+.env
+.svn
+.vscode
+node_modules
+/public/.user.ini
+/public/assets/libs/*.DS_Store

+ 1 - 0
.user.ini

@@ -0,0 +1 @@
+open_basedir=/www/wwwroot/im.hdlkeji.com/im/:/tmp/

+ 191 - 0
LICENSE

@@ -0,0 +1,191 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+2. Grant of Copyright License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution.
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+5. Submission of Contributions.
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+6. Trademarks.
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty.
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+8. Limitation of Liability.
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability.
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "{}" replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same "printed page" as the copyright notice for easier identification within
+third-party archives.
+
+   Copyright 2017 Karson
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 1 - 0
addons/.gitkeep

@@ -0,0 +1 @@
+

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
addons/fastim/.addonrc


+ 77 - 0
addons/fastim/Fastim.php

@@ -0,0 +1,77 @@
+<?php
+
+namespace addons\fastim;
+
+use think\Addons;
+use app\common\library\Menu;
+
+/**
+ * 插件
+ */
+class Fastim extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        $menu = require_once ADDON_PATH . 'fastim' . DS . 'data' . DS . 'menu.php';
+        Menu::create($menu);
+
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        Menu::delete('fastim');
+        return true;
+    }
+
+    /**
+     * 插件启用方法
+     * @return bool
+     */
+    public function enable()
+    {
+        Menu::enable('fastim');
+        return true;
+    }
+
+    /**
+     * 插件禁用方法
+     * @return bool
+     */
+    public function disable()
+    {
+        Menu::disable('fastim');
+        return true;
+    }
+
+    /**
+     * 插件升级方法
+     */
+    public function upgrade()
+    {
+        $menu = include ADDON_PATH . 'fastim' . DS . 'data' . DS . 'menu.php';
+        Menu::upgrade('fastim', $menu);
+    }
+
+    /**
+     * 增加命令
+     */
+    public function appInit($param)
+    {
+        if (request()->isCli()) {
+            \think\Console::addDefaultCommands([
+                'addons\fastim\command\Swoole'
+            ]);
+        }
+    }
+
+}

+ 36 - 0
addons/fastim/bootstrap.js

@@ -0,0 +1,36 @@
+require.config({
+    paths: {
+        'fastim': '../addons/fastim/js/fastim'
+    },
+    shim: {
+        'fastim': {
+            deps: ['css!../addons/fastim/css/fastim_default.css'],
+            exports: 'FastIm'
+        }
+    }
+});
+if (Config.modulename == 'admin' && Config.controllername == 'index' && Config.actionname == 'index') {
+    require(['fastim'], function (FastIm) {
+        FastIm.initialize(document.domain);
+    });
+} else {
+
+    try {
+        var parentConifg = window.parent.Config;
+    } catch (err) {
+        var parentConifg = false;
+    }
+
+    if (parentConifg && parentConifg.modulename == 'admin') {
+        // 监听后台 iframe 内的快捷键打开会话窗口
+        $(document).on('keyup', function (event) {
+            if (window.parent.FastIm && event.keyCode === 191 && event.ctrlKey) {
+                window.parent.FastIm.common.extractMessage();
+            }
+        })
+    } else {
+        require(['fastim'], function (FastIm) {
+            FastIm.initialize(document.domain);
+        });
+    }
+}

+ 258 - 0
addons/fastim/command/Swoole.php

@@ -0,0 +1,258 @@
+<?php
+
+/**
+ * FastIm v1.0.0
+ * FastAdmin企业IM客服系统
+ * https://www.fastadmin.net/store/fastim.html
+ *
+ * Copyright 2021 FastAdmin:thinkphp
+ *
+ * FastAdmin企业IM客服系统不是开源产品,所有文字、图片、样式、风格等版权归企业IM客服系统作者所有,如有复制、仿冒、抄袭、盗用,FastAdmin和企业IM客服系统作者将追究法律责任
+ *
+ * Released on: November 8, 2021
+ */
+
+namespace addons\fastim\command;
+
+use Swoole\Process;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\input\Option;
+use think\console\Output;
+use think\Config;
+use think\Exception;
+use addons\fastim\library\swoole\WebSocket as WebSocketServer;
+
+/**
+ * Swoole WebSocket 命令行,支持操作:start|stop|restart|reload
+ * 此文件的修改不支持热更新,请于更新后重启swoole-websocket服务
+ */
+class Swoole extends Command
+{
+    /**
+     * 支持Swoole的所有配置参数
+     * 若需使用Swoole的常量,请添加到`init`方法的"检查扩展"之后
+     * @config array
+     */
+    protected $config = [
+        'dispatch_mode'            => 2,// 固定模式
+        'pid_file'                 => RUNTIME_PATH . 'swoole/fastim-swoole.pid',
+        'log_file'                 => RUNTIME_PATH . 'swoole/fastim-swoole.log',
+        'heartbeat_check_interval' => 30
+    ];
+
+    /**
+     * 文件修改监听重启服务
+     * @monitor array
+     */
+    protected $monitor = [
+        'file_monitor'          => false, // 是否开启PHP文件更改监控(正式环境勿开,很占cpu)
+        'file_monitor_interval' => 3, // 文件变化监控检测时间间隔(秒)
+        'file_monitor_path'     => [
+            ROOT_PATH . 'addons/fastim',
+        ], // 文件监控目录
+    ];
+
+    public function configure()
+    {
+        $this->setName('fastim')
+            ->addArgument('action', Argument::OPTIONAL, "start|stop|restart|reload", 'start')
+            ->addOption('daemon', 'd', Option::VALUE_NONE, 'Run the swoole server in daemon mode.')
+            ->setDescription('Swoole WebSocket Server for FastIm');
+    }
+
+    public function initialize(Input $input, Output $output)
+    {
+        $config       = get_addon_config('fastim');
+        $this->config = array_merge($this->config, $config);
+    }
+
+    public function execute(Input $input, Output $output)
+    {
+        $action = $input->getArgument('action');
+
+        $this->init();
+
+        if (in_array($action, ['start', 'stop', 'reload', 'restart'])) {
+            $this->$action();
+        } else {
+            $output->writeln("<error>Invalid argument action:{$action}, Expected start|stop|restart|reload .</error>");
+        }
+    }
+
+    protected function init()
+    {
+
+        // 检查swoole扩展
+        if (!extension_loaded('swoole')) {
+            throw new Exception('Please install swoole extension. See https://wiki.swoole.com/#/environment');
+        }
+
+        // 涉及swoole常量的配置
+        $this->config = array_merge($this->config, [
+            'log_rotation' => SWOOLE_LOG_ROTATION_DAILY, // 按日期分隔日志
+            'log_level'    => SWOOLE_LOG_DEBUG,// 日志等级:调试=SWOOLE_LOG_DEBUG,线上=SWOOLE_LOG_WARNING
+        ]);
+
+        if (!is_dir(RUNTIME_PATH . 'swoole')) {
+            @mkdir(RUNTIME_PATH . 'swoole', 0755);
+        }
+        if (empty($this->config['pid_file'])) {
+            $this->config['pid_file'] = RUNTIME_PATH . 'swoole/fastim-swoole.pid';
+        }
+
+        // 文件名加端口号,避免pid混乱
+        $this->config['pid_file'] .= '_' . $this->config['websocket_port'];
+
+        // 线上仅记录警告以上日志
+        if (!Config::get('app_debug')) {
+            $this->config['log_level'] = SWOOLE_LOG_WARNING;
+        }
+    }
+
+    /**
+     * 启动server
+     * @access protected
+     * @return void
+     */
+    protected function start()
+    {
+        $pid = $this->getMasterPid();
+
+        if ($this->isRunning($pid)) {
+            $this->output->writeln('<error>swoole WebSocket server process is already running.</error>');
+            return false;
+        }
+
+        $this->output->writeln('Starting swoole WebSocket server...');
+
+        $server = new WebSocketServer();
+
+        // 开启守护进程模式
+        $exitTips = 'You can exit with <info>`CTRL-C`</info>';
+        if ($this->input->hasOption('daemon')) {
+            $this->config['daemonize'] = true;
+            $exitTips                  = 'Stop swoole WebSocket server command <info>`php think fastim stop`</info>';
+        }
+
+        // 设置文件监控
+        if ($this->monitor['file_monitor']) {
+            $interval = $this->monitor['file_monitor_interval'] ?? 2;
+            $paths    = $this->monitor['file_monitor_path'] ?? [];
+            $server->setMonitor($interval, $paths);
+        }
+
+        $server->option($this->config);
+
+        $agreement = $this->config['wss_switch'] ? 'wss://' : 'ws://';
+        $address   = $agreement . "0.0.0.0:" . $this->config['websocket_port'];
+
+        $this->output->writeln("Swoole WebSocket server started: <" . $address . ">");
+        $this->output->writeln($exitTips);
+
+        $server->start();
+    }
+
+    /**
+     * 柔性重启server
+     * @access protected
+     * @return void
+     */
+    protected function reload()
+    {
+        $pid = $this->getMasterPid();
+
+        if (!$this->isRunning($pid)) {
+            $this->output->writeln('<error>no swoole WebSocket server process running.</error>');
+            return false;
+        }
+
+        $this->output->writeln('Reloading swoole WebSocket server...');
+        Process::kill($pid, SIGUSR1);
+        $this->output->writeln('> success');
+    }
+
+
+    /**
+     * 停止server
+     * @access protected
+     * @return void
+     */
+    protected function stop()
+    {
+        $pid = $this->getMasterPid();
+
+        if (!$this->isRunning($pid)) {
+            $this->output->writeln('<error>no swoole WebSocket server process running.</error>');
+            return false;
+        }
+
+        $this->output->writeln('Stopping swoole WebSocket server...');
+
+        Process::kill($pid, SIGTERM);
+        $this->removePid();
+
+        $this->output->writeln('> success');
+    }
+
+
+    /**
+     * 重启server
+     * @access protected
+     * @return void
+     */
+    protected function restart()
+    {
+        $pid = $this->getMasterPid();
+
+        if ($this->isRunning($pid)) {
+            $this->stop();
+        }
+
+        $this->start();
+    }
+
+    /**
+     * 获取主进程PID
+     * @access protected
+     * @return int
+     */
+    protected function getMasterPid()
+    {
+        if (is_file($this->config['pid_file'])) {
+            $masterPid = (int)file_get_contents($this->config['pid_file']);
+        } else {
+            $masterPid = 0;
+        }
+
+        return $masterPid;
+    }
+
+    /**
+     * 删除PID文件
+     * @access protected
+     * @return void
+     */
+    protected function removePid()
+    {
+        if (is_file($this->config['pid_file'])) {
+            unlink($this->config['pid_file']);
+        }
+    }
+
+    /**
+     * 判断PID是否在运行
+     * @access protected
+     * @param int $pid
+     * @return bool
+     */
+    protected function isRunning($pid)
+    {
+        if (empty($pid)) {
+            return false;
+        }
+
+        return Process::kill($pid, 0);
+    }
+}

+ 122 - 0
addons/fastim/config.php

@@ -0,0 +1,122 @@
+<?php
+
+return [
+    [
+        'name' => '__tips__',
+        'title' => '温馨提示',
+        'type' => 'string',
+        'content' => [],
+        'value' => '1. <b><font color="red">本窗口的所有配置项,非技术人员不建议进行调整</font></b><br>'."\n"
+            .'                      2. 若需开启wss协议,请先配置<b>ssl证书</b>、<b>ssl证书KEY</b>并重启服务才会生效;https站点必须配置wss<br>'."\n"
+            .'                      3. <b>WebSocket端口</b>需对外开放<br>'."\n"
+            .'                      4. 消息提醒铃声建议大小100kb,若文件无法上传请先配置application/extra/upload.php文件',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'theme',
+        'title' => '主题模板',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'default',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '请确保addons/fastim/view有相应的目录',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'wss_switch',
+        'title' => 'wss协议',
+        'type' => 'radio',
+        'content' => [
+            '不开启',
+            '开启',
+        ],
+        'value' => '0',
+        'rule' => '',
+        'tip' => '请先参考常见问题配置好wss服务再开启,否则将无法链接',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'ssl_cert_file',
+        'title' => 'ssl证书',
+        'type' => 'string',
+        'content' => [],
+        'value' => '/www/wwwroot/yourDomain/cert/im.pem',
+        'rule' => '',
+        'tip' => '请填写证书pem或crt文件的绝对路径',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'ssl_key_file',
+        'title' => 'ssl证书KEY',
+        'type' => 'string',
+        'content' => [],
+        'value' => '/www/wwwroot/yourDomain/cert/im.key',
+        'rule' => '',
+        'tip' => '请填写证书密匙(key)文件的绝对路径',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'websocket_port',
+        'title' => 'WebSocket 端口',
+        'type' => 'number',
+        'content' => [],
+        'value' => '8080',
+        'rule' => 'required,range(1024~65535)',
+        'tip' => '请在安全组、防火墙等开放此端口',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'worker_num',
+        'title' => 'Worker 进程数',
+        'type' => 'number',
+        'content' => [],
+        'value' => '2',
+        'rule' => '',
+        'tip' => '建议设置为CPU核数的1-4倍',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'reactor_num',
+        'title' => 'Reactor 线程数',
+        'type' => 'number',
+        'content' => [],
+        'value' => '2',
+        'rule' => '',
+        'tip' => '建议设置为CPU核数的1-4倍',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'max_connections',
+        'title' => 'Swoole\\Table大小',
+        'type' => 'number',
+        'content' => [],
+        'value' => '20480',
+        'rule' => '',
+        'tip' => '值越大可同时在线的用户越多(内存足够的情况下)',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'ringing',
+        'title' => '消息提醒铃声',
+        'type' => 'file',
+        'content' => [],
+        'value' => '/assets/addons/fastim/audio/ringing.mp3',
+        'rule' => 'required,file',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+];

+ 26 - 0
addons/fastim/controller/Base.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace addons\fastim\controller;
+
+use think\addons\Controller;
+use think\Request;
+
+class Base extends Controller
+{
+
+    public function __construct(Request $request = null)
+    {
+        parent::__construct($request);
+        $config = get_addon_config('fastim');
+        // 设定主题模板目录
+        $this->view->engine->config('view_path', $this->view->engine->config('view_path') . trim($config['theme']) . DS);
+    }
+
+    protected function _initialize()
+    {
+        //跨域检测
+        check_cors_request();
+        parent::_initialize();
+    }
+
+}

+ 353 - 0
addons/fastim/controller/Index.php

@@ -0,0 +1,353 @@
+<?php
+
+/**
+ * FastIm v1.0.0
+ * FastAdmin企业IM客服系统
+ * https://www.fastadmin.net/store/fastim.html
+ *
+ * Copyright 2021 FastAdmin:thinkphp
+ *
+ * FastAdmin企业IM客服系统不是开源产品,所有文字、图片、样式、风格等版权归企业IM客服系统作者所有,如有复制、仿冒、抄袭、盗用,FastAdmin和企业IM客服系统作者将追究法律责任
+ *
+ * Released on: November 8, 2021
+ */
+
+namespace addons\fastim\controller;
+
+use addons\fastim\library\Common;
+use addons\fastim\library\CommonCode;
+use think\Db;
+use think\Cookie;
+use Exception;
+
+class Index extends Base
+{
+    protected $noNeedLogin = [
+        'initialize',
+        'loadMessagePrompt',
+        'upload',
+        'mobile',
+        'index',
+        'icon',
+        'getEmoji',
+        'getConfigItemData',
+        'fullScreen'
+    ];
+
+    protected $config;
+
+    protected $userInfo = false;
+
+    protected $tokens = []; // 用于连接ws的token数组
+
+    protected $referrer = ''; // 来源:网址或uni-app
+
+    public function index()
+    {
+        $this->view->assign('im_name', $this->config['im_name']);
+        return $this->view->fetch();
+    }
+
+    public function fullScreen()
+    {
+        $this->view->assign('im_name', $this->config['im_name']);
+        return $this->view->fetch();
+    }
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->config            = get_addon_config('fastim');
+        $this->config['__CDN__'] = config('view_replace_str.__CDN__');
+        $imDbConfig              = Db::name('fastim_config')->field('name,value,user_settings')->select();
+        foreach ($imDbConfig as $key => $value) {
+            if ($value['user_settings'] == 1) {
+                $imDefaultUserConfig[$value['name']] = $value['value'];
+            } else {
+                $imConfig[$value['name']] = $value['value'];
+            }
+        }
+
+        // 上传配置
+        $upload['upload'] = \app\common\model\Config::upload();
+        // 上传信息配置后
+        \think\Hook::listen("upload_config_init", $upload['upload']);
+        if (isset($upload['upload']['storage']) && $upload['upload']['storage'] != 'local') {
+            $upload['upload']['uploadurl'] = preg_match('/^http(s)?:\/\//', $upload['upload']['uploadurl']) ? $upload['upload']['uploadurl'] : $this->request->domain() . $upload['upload']['uploadurl'];
+        } else {
+            $upload['upload']['cdnurl']    = false;
+            $upload['upload']['uploadurl'] = addon_url('fastim/index/upload', [], '', true);
+        }
+
+        // 来源
+        $this->referrer = $this->request->get('referrer') ?? '';
+
+        /**
+         * 用户身份识别
+         * FastAdmin用户或管理员可转换为ImUser
+         * 用户token以前台传递的为主,Cookie内的为辅
+         */
+        $data                              = $this->request->only(['token', 'im_tourists_token']);
+        $this->tokens['token']             = Cookie::get('token');
+        $this->tokens['im_tourists_token'] = Cookie::get('im_tourists_token');
+        if (!$this->tokens['token'] && isset($data['token'])) {
+            $this->tokens['token'] = $data['token'];
+        }
+        if (isset($data['im_tourists_token'])) {
+            $this->tokens['im_tourists_token'] = $data['im_tourists_token'];
+        }
+
+        // 若已登录管理员,通过管理员自动登录到im
+        $auth = \app\admin\library\Auth::instance();
+        if ($auth->isLogin() && $this->referrer != 'uni-app') {
+            $this->userInfo = Common::checkAdmin(false, $auth->id);
+            if ($this->userInfo) {
+                $keeptime   = 864000;
+                $expiretime = time() + $keeptime;
+
+                // `$sign`原规则为单纯的id,若需修改附加的字符串,请将`CommonCode::imAuthSign`字符串一起修改
+                $sign = $this->userInfo['id'] . CommonCode::$imAuthSign;
+
+                $key                          = md5(md5($sign) . md5($keeptime) . md5($expiretime) . $this->userInfo['token']);
+                $tokenData                    = [$this->userInfo['id'], $keeptime, $expiretime, $key];
+                $this->tokens['fastim_token'] = implode('|', $tokenData);
+                unset($this->userInfo['token']);
+            }
+        }
+
+        // 若已登录前台用户,通过用户自动登录到fastim
+        if (!$this->userInfo && isset($this->tokens['token']) && $this->referrer != 'uni-app') {
+            $auth = \app\common\library\Auth::instance();// 多种token来源,重新初始化
+            $auth->init($this->tokens['token']);
+            if ($auth->isLogin()) {
+                $this->userInfo = Common::checkFaUser(null, $auth->id);
+                if ($this->userInfo) {
+                    if (!config('cookie.httponly')) {
+                        $this->tokens['fastim_token'] = $this->tokens['token'];
+                    } else {
+                        // 需用 user_token 数据表中的token字段同样的加密算法对token进行加密
+                        $sign                         = Common::getEncryptedToken($this->tokens['token']);
+                        $sign                         .= CommonCode::$imAuthSign;
+                        $key                          = md5(md5($auth->id) . md5($sign));
+                        $tokenData                    = [$auth->id, $key];
+                        $this->tokens['fastim_token'] = implode('|', $tokenData);
+                    }
+                }
+            }
+        }
+        unset($this->tokens['token']);
+
+        // 用户和管理员以及游客token都可通过此处转换为IMuser
+        $imUser = Common::checkIMUser($this->tokens['im_tourists_token'], $this->userInfo);
+        if (!$imUser) {
+            if ($this->referrer == 'uni-app') {
+                $this->result(null, 402, $imConfig['im_name'] . ' 无法识别用户,请重新登录!', 'json');
+            }
+            // 建立
+            $imUser = Common::createIMUser($this->referrer, $this->request->ip() ?? '');
+            if ($imUser) {
+                $this->tokens['im_tourists_token'] = $imUser['cache_token'];
+                Cookie::set('im_tourists_token', $imUser['cache_token'], 315360000);
+            }
+        }
+
+        $this->userInfo = Common::bindFaUserToIMUser($imUser, $this->userInfo);
+        if (!$this->userInfo) {
+            $this->result(null, 401, $imConfig['im_name'] . ' 无法识别用户,请清理浏览器缓存后再试!', 'json');
+        }
+        unset($imUser);
+
+        // 记录访问日志
+        $tourist = ($this->userInfo['identity'] == 'imuser') ? 1 : 0; // imuser必然是游客
+        $tourist = ($this->referrer == 'uni-app') ? 0 : $tourist; // 在uni-app端不存在游客与会员区分
+
+        $trajectory = [
+            'user_id'    => $this->userInfo['id'],
+            'type'       => 0,
+            'url'        => $this->request->get('url') ?? '',
+            'referrer'   => $this->referrer,
+            'ip'         => $this->request->ip(),
+            'tourist'    => $tourist,
+            'createtime' => time()
+        ];
+        Db::name('fastim_trajectory')->insert($trajectory);
+
+        // 用户配置
+        $imConfigGroup = \app\admin\model\fastim\Config::getConfigListByGroup(1, null);
+        $userConfig    = Db::name('fastim_user_config')->where('user_id', $this->userInfo['id'])->column('name,value');
+        foreach ($imConfigGroup as $key => $value) {
+            foreach ($value['list'] as $k => $v) {
+                if (isset($userConfig[$v['name']])) {
+                    $imConfigGroup[$key]['list'][$k]['value'] = $userConfig[$v['name']];
+                }
+            }
+        }
+
+        $this->config['user_config'] = array_merge($imDefaultUserConfig, $userConfig);
+        $this->config                = array_merge($this->config, $upload, $imConfig);
+
+        if ($this->referrer != 'uni-app') {
+            $this->view->assign('imConfigGroup', $imConfigGroup);
+            $this->view->assign('userinfo', $this->userInfo);
+        }
+    }
+
+    public function initialize()
+    {
+        // 清理轨迹
+        switch ($this->config['trajectory_save_cycle']) {
+            case 0:
+                $clearWhereTime = 604800; // 清理7天前的
+                break;
+            case 1:
+                $clearWhereTime = 2592000; // 30天
+                break;
+            case 2:
+                $clearWhereTime = 5184000; // 60天
+                break;
+
+            default:
+                $clearWhereTime = false; // 不清理
+                break;
+        }
+        if ($clearWhereTime) {
+            Db::name('fastim_trajectory')->where('createtime', '<', time() - $clearWhereTime)->delete();
+        }
+
+        // 配置排除
+        $except_config = [
+            '__tips__',
+            'reactor_num',
+            'worker_num',
+            'ssl_cert_file',
+            'ssl_key_file',
+            'configgroup',
+            'trajectory_save_cycle'
+        ];
+        foreach ($except_config as $key => $value) {
+            if (array_key_exists($value, $this->config)) {
+                unset($this->config[$value]);
+            }
+        }
+
+        // 表情包数据
+        $emoji = Db::name('fastim_emoji')->where('status', '1')->order('weigh desc')->select();
+        foreach ($emoji as $key => $value) {
+            $emoji[$key]['image'] = cdnurl($value['image'], true);
+        }
+
+        // 快捷回复
+        if ($this->userInfo['type'] == 'csr') {
+            $fastReply = Db::name('fastim_fast_reply')->field('id,title,content')->where(function ($query) {
+                $query->where('user_id', $this->userInfo['id'])->whereOr('user_id', 0);
+            })->where('status', '1')->where('deletetime', null)->order('user_id desc,createtime desc')->select();
+            if ($this->referrer != 'uni-app') {
+                $this->view->assign('fast_reply', $fastReply);
+            } else {
+                $this->config = array_merge($this->config, ['fast_reply' => $fastReply]);
+            }
+        }
+
+        if ($this->referrer != 'uni-app') {
+            $this->view->assign('emoji', $emoji);
+            $this->view->assign('user_type', $this->userInfo['type']);
+            $res['window'] = $this->view->fetch(ROOT_PATH . 'addons/fastim/view/' . trim($this->config['theme']) . '/pc.html', [], ['__CDN__' => $this->config['__CDN__'] ? $this->config['__CDN__'] : cdnurl('', true)]);
+
+            // 最后未读消息
+            $res['unreadMessage'] = Common::getUnreadMessage($this->userInfo['id'], false);
+        }
+        $res['tokens']   = $this->tokens;
+        $res['userinfo'] = $this->userInfo;
+        $res['config']   = $this->config;
+        $this->result($res, 1, 'ok', 'json');
+    }
+
+    /**
+     * 供跨站下载来信提示音文件(未使用云存储)
+     */
+    public function loadMessagePrompt()
+    {
+        $file = ROOT_PATH . 'public' . $this->config['ringing'];
+        header("Content-type:application/octet-stream");
+        $filename = basename($file);
+        header("Content-Disposition:attachment;filename = " . $filename);
+        header("Accept-ranges:bytes");
+        header("Accept-length:" . filesize($file));
+        readfile($file);
+    }
+
+    /**
+     * 上传文件
+     */
+    public function upload()
+    {
+        $file = $this->request->file('file');
+        if (empty($file)) {
+            $this->result(null, 0, $this->config['im_name'] . '没有文件被上传~', 'json');
+        }
+        try {
+            $upload     = new \app\common\library\Upload($file);
+            $attachment = $upload->upload();
+        } catch (app\common\exception\UploadException $e) {
+            $this->result(null, 0, $e->getError(), 'json');
+        } catch (Exception $e) {
+            $this->result(null, 0, $e->getMessage(), 'json');
+        }
+
+        $this->result([
+            'url'     => $attachment->url,
+            'fullurl' => cdnurl($attachment->url, true)
+        ], 1, __('Uploaded successful'), 'json');
+    }
+
+    /**
+     * uni-app获取emoji
+     */
+    public function getEmoji()
+    {
+        // 表情包数据
+        $emoji = Db::name('fastim_emoji')->where('status', '1')->order('weigh desc')->select();
+        foreach ($emoji as $key => $value) {
+            $emoji[$key]['image'] = cdnurl($value['image'], true);
+        }
+
+        $this->result([
+            'emoji' => $emoji
+        ], 1, 'ok', 'json');
+    }
+
+    /**
+     * uni-app获得用户设置项
+     */
+    public function getConfigItemData()
+    {
+        $imConfigGroup = \app\admin\model\fastim\Config::getConfigListByGroup(1, null, 1);
+        $userConfig    = Db::name('fastim_user_config')->where('user_id', $this->userInfo['id'])->column('name,value');
+        foreach ($imConfigGroup as $key => $value) {
+            foreach ($value['list'] as $k => $v) {
+                if (isset($userConfig[$v['name']])) {
+                    if (in_array($imConfigGroup[$key]['list'][$k]['type'], ['select', 'selects', 'checkbox'])) {
+                        $userConfig[$v['name']] = explode(',', $userConfig[$v['name']]);
+                    }
+                    $imConfigGroup[$key]['list'][$k]['value'] = $userConfig[$v['name']];
+                }
+            }
+        }
+
+        $this->result([
+            'imConfigGroup' => $imConfigGroup
+        ], 1, 'ok', 'json');
+    }
+
+    /**
+     * 生成文件类型 icon
+     */
+    public function icon()
+    {
+        $suffix = $this->request->param("suffix");
+        header('Content-type: image/svg+xml');
+        $suffix = $suffix ? $suffix : "FILE";
+        echo Common::buildSuffixImage($suffix);
+        exit;
+    }
+}

+ 20 - 0
addons/fastim/controller/api/Base.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace addons\fastim\controller\api;
+
+use app\common\controller\Api;
+use think\Lang;
+
+class Base extends Api
+{
+    protected $noNeedLogin = [];
+    protected $noNeedRight = ['*'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+
+        // 这里手动载入语言包
+        Lang::load(ROOT_PATH . '/addons/fastim/lang/api/zh-cn.php');
+    }
+}

+ 304 - 0
addons/fastim/controller/api/User.php

@@ -0,0 +1,304 @@
+<?php
+
+namespace addons\fastim\controller\api;
+
+use app\common\library\Token;
+use think\Db;
+use think\Config;
+use think\Validate;
+use app\common\library\Sms;
+use addons\fastim\library\Common;
+
+/**
+ * im会员登录
+ */
+class User extends Base
+{
+    protected $noNeedLogin = ['login', 'register'];
+
+    protected $userInfo = false;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+    }
+
+    /**
+     * 支持账户和手机号登录
+     */
+    public function login()
+    {
+        /*$tab      = $this->request->post('tab');
+        $account  = $this->request->post('account');
+        $password = $this->request->post('password');
+        $captcha  = $this->request->post('captcha');*/
+
+        $id=input('id/d');
+        $token=input('token');
+        $username=input('username',session_create_id());
+        $nickname=input('nickname');
+        $group_id=input('group_id',0);
+        $avatar=input('avatar',df_avatar());
+        if (!$id) {
+            $this->error(__('Please enter the correct account number'));
+        }
+        if (!$token) {
+            $this->error(__('Please enter the correct account token'));
+        }
+        $this->checkToken($token,$id);
+
+        $user=\app\common\model\User::where('id',$id)->find();
+        if ($user) {
+            /*if ($user->status != 'normal') {
+                $this->error('用户已被禁用');
+            }*/
+            /*$user['avatar']=$avatar;
+            if($nickname) {
+                $user['username'] = $username;
+                $user['nickname'] = $nickname;
+                $user['group_id'] = $group_id;
+                $user->save();
+            }*/
+            $this->userInfo = $this->directLogin($user);
+        } else {
+            // 注册
+            $userInfo = $this->createUser($username, '', '', '', [
+                'nickname'=>$nickname,
+                'group_id'=>$group_id,
+                'avatar'=>$avatar,
+                'id'=>$id,
+            ]);
+            $this->success(__('Sign up successful'), [
+                'userinfo' => $userInfo
+            ]);
+        }
+
+        /**
+         * 1、前台有必填验证
+         * 2、用户提交了错误的tab也无法登录
+         */
+        /*if ($tab == 0 && (!$password || strlen($password) < 6)) {
+            $this->error(__('Password must be 6 to 30 characters'));
+        }*/
+/*        if ($tab == 1) {
+            // 手机验证码检查
+            $captchaResult = Sms::check($account, $captcha, 'login');
+            if (!$captchaResult) {
+                $this->error(__('Captcha is incorrect'));
+            }
+
+            $user = \app\common\model\User::get(['mobile' => $account]);
+            if ($user) {
+                if ($user->status != 'normal') {
+                    return false;
+                }
+                $this->userInfo = $this->directLogin($user);
+                Sms::flush($account, 'login');
+            } else {
+                // 注册
+                $userInfo = $this->createUser('', '', '', $account, []);
+                Sms::flush($account, 'login');
+                $this->success(__('Sign up successful'), [
+                    'userinfo' => $userInfo
+                ]);
+            }
+        }*/
+
+        // 检查是否为会员
+        /*if (!$this->userInfo) {
+            $this->userInfo = $this->checkUserByPassword($account, $password);
+            if (!$this->userInfo) {
+                // 检查是否为管理员
+                $this->userInfo = $this->checkAdminByPassword($account, $password);
+            }
+        }
+        if (!$this->userInfo) {
+            $this->error(__('Username or password is incorrect'));
+        }*/
+
+        // 用户和管理员以及游客token都可通过此处转换为IMuser
+        $imUser = Common::checkIMUser(false, $this->userInfo);
+        if (!$imUser) {
+            // 建立
+            $imUser = Common::createIMUser($this->request->get('referrer') ?? '', $this->request->ip());
+            if ($imUser) {
+                $userInfoToken = $imUser['cache_token'];
+            }
+        } else {
+            $keeptime      = 864000;// token 10天后过期
+            $expiretime    = time() + $keeptime;
+            $token         = Db::name('fastim_user')->where('id', $imUser['id'])->value('token');
+            $key           = md5(md5($imUser['id']) . md5($keeptime) . md5($expiretime) . $token);
+            $userInfoToken = [$imUser['id'], $keeptime, $expiretime, $key];
+            $userInfoToken = implode('|', $userInfoToken);
+        }
+
+        // 绑定
+        $this->userInfo = Common::bindFaUserToIMUser($imUser, $this->userInfo);
+        if (!$this->userInfo) {
+            $this->error(__('Unrecognized user'), [], 401);
+        }
+
+        $this->userInfo['token']      = $userInfoToken;
+        $this->userInfo['auth_token'] = Common::createAuthToken($imUser['id']);
+
+        $this->success('ok', [
+            'userinfo' => $this->userInfo
+        ]);
+    }
+
+    /*public function register()
+    {
+        $mobile   = $this->request->post('mobile');
+        $username = $this->request->post('username');
+        $password = $this->request->post('password');
+        $captcha  = $this->request->post('captcha');
+
+        $rule = [
+            'mobile'   => 'regex:/^1\d{10}$/',
+            'username' => 'require|length:3,30',
+            'password' => 'require|length:6,30'
+        ];
+        $msg  = [
+            'mobile'           => 'Mobile is incorrect',
+            'username.require' => 'Username can not be empty',
+            'username.length'  => 'Username must be 3 to 30 characters',
+            'password.require' => 'Password can not be empty',
+            'password.length'  => 'Password must be 6 to 30 characters'
+        ];
+        $data = [
+            'mobile'   => $mobile,
+            'username' => $username,
+            'password' => $password,
+            'captcha'  => $captcha,
+        ];
+
+        $captchaResult = Sms::check($mobile, $captcha, 'register');
+        if (!$captchaResult) {
+            $this->error(__('Captcha is incorrect'));
+        }
+
+        $validate = new Validate($rule, $msg);
+        $result   = $validate->check($data);
+        if (!$result) {
+            $this->error(__($validate->getError()));
+        }
+
+        $userInfo = $this->createUser($username, $password, '', $mobile, []);
+        Sms::flush($mobile, 'register');
+        $this->success(__('Sign up successful'), [
+            'userinfo' => $userInfo
+        ]);
+    }*/
+
+    private function createUser($username, $password, $email = '', $mobile = '', $extend = [])
+    {
+        $auth = \app\common\library\Auth::instance();
+        /*if ($username) {*/
+            $ret = $auth->register($username, $password, '', $mobile, $extend);
+        /*} else {
+            $ret = $auth->register($mobile, \fast\Random::alnum(), '', $mobile, $extend);
+        }*/
+        if ($ret) {
+            $userInfo             = $auth->getUserinfo();
+            $userInfo['identity'] = 'fauser';
+
+            // 建立imUser
+            $imUser = Common::createIMUser('uni-app', $this->request->ip());
+            // 绑定
+            $userInfo = Common::bindFaUserToIMUser($imUser, $userInfo);
+            if (!$userInfo) {
+                $this->error(__('Failed to bind new user, please try again'), [], 401);
+            }
+
+            $userInfo['token']      = $imUser['cache_token'];
+            $userInfo['auth_token'] = Common::createAuthToken($imUser['id']);
+            unset($userInfo['cache_token']);
+            return $userInfo;
+        } else {
+            $this->error($this->auth->getError());
+        }
+    }
+
+    /**
+     * 直接登录
+     */
+    private function directLogin($user)
+    {
+        Db::startTrans();
+        try {
+            //判断连续登录和最大连续登录
+            if ($user->logintime < \fast\Date::unixtime('day')) {
+                $user->successions    = $user->logintime < \fast\Date::unixtime('day', -1) ? 1 : $user->successions + 1;
+                $user->maxsuccessions = max($user->successions, $user->maxsuccessions);
+            }
+
+            $user->prevtime = $user->logintime;
+            //记录本次登录的IP和时间
+            $user->loginip   = request()->ip();
+            $user->logintime = time();
+            //重置登录失败次数
+            $user->loginfailure = 0;
+
+            $user->save();
+            Db::commit();
+        } catch (Exception $e) {
+            Db::rollback();
+            return false;
+        }
+
+        $user->identity = 'fauser';
+        return $user->toArray();
+    }
+
+    private function checkUserByPassword($account, $password)
+    {
+        $field = Validate::is($account, 'email') ? 'email' : (Validate::regex($account, '/^1\d{10}$/') ? 'mobile' : 'username');
+        $user  = \app\common\model\User::get([$field => $account]);
+        if (!$user) {
+            return false;
+        }
+        if ($user->status != 'normal') {
+            return false;
+        }
+        $auth = \app\common\library\Auth::instance();
+        if ($user->password != $auth->getEncryptPassword($password, $user->salt)) {
+            return false;
+        }
+        return $this->directLogin($user);
+    }
+
+    private function checkAdminByPassword($username, $password)
+    {
+        $admin = \app\admin\model\Admin::get(['username' => $username]);
+        if (!$admin) {
+            return false;
+        }
+        if ($admin['status'] == 'hidden') {
+            return false;
+        }
+        if (Config::get('fastadmin.login_failure_retry') && $admin->loginfailure >= 10 && time() - $admin->updatetime < 86400) {
+            return false;
+        }
+        if ($admin->password != md5(md5($password) . $admin->salt)) {
+            $admin->loginfailure++;
+            $admin->save();
+            return false;
+        }
+
+        // 不重置管理员token
+        $admin->loginfailure = 0;
+        $admin->logintime    = time();
+        $admin->loginip      = request()->ip();
+        $admin->save();
+        $admin->identity = 'admin';
+        return $admin->toArray();
+    }
+
+    protected function checkToken($token,$uid){
+        //dump(Token::connect(\config('main_token'),'main'));
+        if(!Token::check($token,$uid)){
+            $this->error('token check failed');
+        }
+    }
+}

+ 128 - 0
addons/fastim/data/menu.php

@@ -0,0 +1,128 @@
+<?php
+
+$menu = [
+    [
+        'name'    => 'fastim',
+        'title'   => '企业IM客服系统',
+        'icon'    => 'fa fa-commenting-o',
+        'sublist' => [
+            [
+                'name'    => 'fastim/config',
+                'title'   => 'IM配置',
+                'icon'    => 'fa fa-gears',
+                'weigh'   => '99',
+                'sublist' => [
+                    ['name' => 'fastim/config/index', 'title' => '查看'],
+                    ['name' => 'fastim/config/update', 'title' => '编辑'],
+                ]
+            ],
+            [
+                'name'    => 'fastim/user',
+                'title'   => '用户管理',
+                'icon'    => 'fa fa-users',
+                'weigh'   => '98',
+                'sublist' => [
+                    ['name' => 'fastim/user/index', 'title' => '查看'],
+                    ['name' => 'fastim/user/edit', 'title' => '编辑'],
+                    ['name' => 'fastim/user/del', 'title' => '删除'],
+                    ['name' => 'fastim/user/loaduser', 'title' => '在其他模块选择用户']
+                ]
+            ],
+            [
+                'name'    => 'fastim/session',
+                'title'   => '会话管理',
+                'icon'    => 'fa fa-user',
+                'weigh'   => '97',
+                'sublist' => [
+                    ['name' => 'fastim/session/index', 'title' => '查看'],
+                    ['name' => 'fastim/session/record', 'title' => '浏览沟通记录']
+                ]
+            ],
+            [
+                'name'    => 'fastim/groupchat',
+                'title'   => '群聊管理',
+                'icon'    => 'fa fa-users',
+                'weigh'   => '96',
+                'sublist' => [
+                    ['name' => 'fastim/groupchat/index', 'title' => '查看'],
+                    ['name' => 'fastim/groupchat/edit', 'title' => '编辑'],
+                    ['name' => 'fastim/groupchat/record', 'title' => '浏览沟通记录']
+                ]
+            ],
+            [
+                'name'    => 'fastim/fast_reply',
+                'title'   => '快捷回复管理',
+                'icon'    => 'fa fa-pencil',
+                'weigh'   => '95',
+                'sublist' => [
+                    ['name' => 'fastim/fast_reply/index', 'title' => '查看'],
+                    ['name' => 'fastim/fast_reply/add', 'title' => '增加'],
+                    ['name' => 'fastim/fast_reply/edit', 'title' => '编辑'],
+                    ['name' => 'fastim/fast_reply/del', 'title' => '删除'],
+                    ['name' => 'fastim/fast_reply/multi', 'title' => '批量更新'],
+                    ['name' => 'fastim/fast_reply/recyclebin', 'title' => '回收站'],
+                    ['name' => 'fastim/fast_reply/destroy', 'title' => '真实删除'],
+                    ['name' => 'fastim/fast_reply/restore', 'title' => '还原'],
+                ]
+            ],
+            [
+                'name'    => 'fastim/kbs',
+                'title'   => '知识库管理',
+                'icon'    => 'fa fa-book',
+                'weigh'   => '94',
+                'sublist' => [
+                    ['name' => 'fastim/kbs/index', 'title' => '查看'],
+                    ['name' => 'fastim/kbs/add', 'title' => '增加'],
+                    ['name' => 'fastim/kbs/edit', 'title' => '编辑'],
+                    ['name' => 'fastim/kbs/del', 'title' => '删除'],
+                    ['name' => 'fastim/kbs/multi', 'title' => '批量更新'],
+                    ['name' => 'fastim/kbs/recyclebin', 'title' => '回收站'],
+                    ['name' => 'fastim/kbs/destroy', 'title' => '真实删除'],
+                    ['name' => 'fastim/kbs/restore', 'title' => '还原'],
+                    ['name' => 'fastim/kbs/import', 'title' => '导入'],
+                ]
+            ],
+            [
+                'name'    => 'fastim/csr_group',
+                'title'   => '客服分组管理',
+                'icon'    => 'fa fa-cubes',
+                'weigh'   => '93',
+                'sublist' => [
+                    ['name' => 'fastim/csr_group/index', 'title' => '查看'],
+                    ['name' => 'fastim/csr_group/add', 'title' => '增加'],
+                    ['name' => 'fastim/csr_group/edit', 'title' => '编辑'],
+                    ['name' => 'fastim/csr_group/del', 'title' => '删除'],
+                    ['name' => 'fastim/csr_group/multi', 'title' => '批量更新'],
+                    ['name' => 'fastim/csr_group/recyclebin', 'title' => '回收站'],
+                    ['name' => 'fastim/csr_group/destroy', 'title' => '真实删除'],
+                    ['name' => 'fastim/csr_group/restore', 'title' => '还原'],
+                ]
+            ],
+            [
+                'name'    => 'fastim/report',
+                'title'   => '举报与反馈管理',
+                'icon'    => 'fa fa-feed',
+                'weigh'   => '92',
+                'sublist' => [
+                    ['name' => 'fastim/report/index', 'title' => '查看'],
+                    ['name' => 'fastim/report/edit', 'title' => '编辑'],
+                    ['name' => 'fastim/report/del', 'title' => '删除'],
+                    ['name' => 'fastim/report/multi', 'title' => '批量更新']
+                ]
+            ],
+            [
+                'name'    => 'fastim/records',
+                'title'   => '聊天记录汇总管理',
+                'icon'    => 'fa fa-file-text-o',
+                'weigh'   => '91',
+                'sublist' => [
+                    ['name' => 'fastim/records/index', 'title' => '查看'],
+                    ['name' => 'fastim/records/edit', 'title' => '编辑'],
+                    ['name' => 'fastim/records/del', 'title' => '删除']
+                ]
+            ]
+        ]
+    ]
+];
+
+return $menu;

+ 10 - 0
addons/fastim/info.ini

@@ -0,0 +1,10 @@
+name = fastim
+title = 企业IM客服系统
+intro = 基于FastAdmin+Swoole的高性能即时通讯和客服系统
+author = thankphp
+website = 
+version = 1.0.1
+state = 1
+url = /addons/fastim
+license = regular
+licenseto = 52593

+ 482 - 0
addons/fastim/install.sql

@@ -0,0 +1,482 @@
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_config`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_config` (
+    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+    `name` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '变量名',
+    `group` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '分组',
+    `title` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '变量标题',
+    `tip` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '变量描述',
+    `type` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '类型:string,text,int,bool,array,datetime,date,file',
+    `value` text COLLATE utf8mb4_unicode_ci COMMENT '变量值',
+    `content` text COLLATE utf8mb4_unicode_ci COMMENT '变量字典数据',
+    `rule` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '验证规则',
+    `extend` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '扩展属性',
+    `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
+    `user_settings` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '用户的配置项',
+    `user_exclusive` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '用户专有配置项',
+    `uniapp_available` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT 'uniapp可用的配置项',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `name` (`name`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM配置';
+
+-- ----------------------------
+-- Records of __PREFIX__fastim_config
+-- ----------------------------
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'configgroup', 'general', 'Config group', '', 'array', '{\"general\":\"General\",\"privacy\":\"Privacy\",\"csr\":\"CSR\",\"message_push\":\"Message Push\"}', null, '', '', '0', '0', '0', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'login_status', 'general', 'Post login status', '', 'select', '0', '[\"在线\",\"繁忙\",\"隐身\"]', '', '', '50', '1', '1', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'busy_reply', 'general', 'Auto reply when busy', '', 'text', '你好,我现在有事不在,一会儿再和你联系~', null, '', '', '49', '1', '1', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'im_name', 'general', 'Im name', '', 'string', 'FastIm', null, '', '', '48', '0', '0', '0');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'send_message_key', 'general', 'Send Message button', '', 'radio', '0', '[\"回车键\",\"CTRL+回车键\"]', '', '', '47', '1', '0', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'ecs_exit', 'general', 'Press ESC to close the window', '', 'radio', '1', '[\"不关闭\",\"关闭\"]', '', '', '46', '1', '0', '0');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'new_message_shake', 'general', 'New message window jitter', '', 'radio', '1', '[\"不抖动\",\"抖动\"]', '', '', '45', '1', '0', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'new_message_sound', 'general', 'New message playing tone', '', 'radio', '1', '[\"不播放\",\"播放\"]', '', '', '44', '1', '0', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'new_message_push_notice', 'general', 'New message push notice', 'web端使用Notification API推送消息', 'radio', '1', '[\"不推送\",\"推送\"]', '', '', '43', '1', '0', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'window_display_push_notice', 'general', 'Window display push notification', '用户在浏览当前网页时,不推送通知', 'radio', '0', '[\"不推送\",\"推送\"]', '', '', '42', '1', '0', '0');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'trajectory_save_cycle', 'general', 'Trajectory Save Cycle', '', 'radio', '0', '[\"保留7天\",\"保留30天\",\"保留60天\",\"不清理\"]', '', '', '41', '0', '0', '0');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'h5_url', 'general', 'H5 Url', '请使用uni-app代码编译H5端,具体请查看<a target="_blank" href="https://doc.fastadmin.net/fastim/1020.html">文档</a>', 'string', '/h5', null, '', '', '40', '0', '0', '0');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'verify_method', 'privacy', 'Add me as a friend', '', 'radio', '0', '[\"需要验证信息\",\"允许任何人添加\"]', '', '', '50', '1', '1', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'add_my_way', 'privacy', 'Add my way', '', 'checkbox', '0,1,2,3', '[\"搜索ID\",\"搜索昵称\",\"搜索手机号\",\"搜索邮箱\"]', '', '', '49', '1', '1', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'temp_session', 'privacy', 'Temporary session', '', 'radio', '1', '[\"不允许\",\"允许\"]', '', '', '48', '1', '0', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'input_status', 'privacy', 'Display input status', '', 'radio', '1', '[\"不展示\",\"展示\"]', '', '', '47', '1', '0', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'mobile_privacy', 'privacy', 'Mobile privacy', '', 'select', '1', '[\"全部可见\",\"仅好友可见\",\"仅自己可见\"]', '', '', '46', '1', '0', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'email_privacy', 'privacy', 'Email privacy', '', 'select', '1', '[\"全部可见\",\"仅好友可见\",\"仅自己可见\"]', '', '', '45', '1', '0', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'age_privacy', 'privacy', 'Age privacy', '', 'select', '1', '[\"全部可见\",\"仅好友可见\",\"仅自己可见\"]', '', '', '44', '1', '0', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'occupation_privacy', 'privacy', 'Occupation privacy', '', 'select', '1', '[\"全部可见\",\"仅好友可见\",\"仅自己可见\"]', '', '', '43', '1', '0', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'company_privacy', 'privacy', 'Company privacy', '', 'select', '1', '[\"全部可见\",\"仅好友可见\",\"仅自己可见\"]', '', '', '42', '1', '0', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'open_csr', 'csr', 'Open CSR', '开启客服功能,将自动为所有用户分配\"我的客服\"会话', 'radio', '1', '[\"关闭\",\"开启\"]', '', '', '50', '0', '0', '0');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'new_user_tip', 'csr', 'new User Tip', '', 'string', '您准备好体验企业IM客服系统了吗?', '', '', '', '49', '0', '0', '0');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'distribution_group_select', 'csr', 'Distribution Group Select', '为用户分配客服时是否要求用户选择客服分组', 'radio', '1', '[\"为用户分配客服前要求用户选择客服类型\",\"无需选择客服分组\"]', '', '', '48', '0', '0', '0');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'distribution_type', 'csr', 'Distribution Type', '客服分配方式', 'radio', '2', '[\"轮训自动分配\",\"按工作量自动分配\",\"用户自行选择客服代表\"]', '', '', '47', '0', '0', '0');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'welcome_new_user_msg', 'csr', 'Welcome New User Msg', '客服接入新用户时自动发送的欢迎消息', 'text', '欢迎访问!', null, '', '', '46', '0', '0', '0');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'no_csr_tip', 'csr', 'No CSR Tip', '无客服在线时给用户的提示', 'text', '您好,人工客服工作时间为:上午8:00至下午18:00。请在工作时间联系人工客服哦~您也可以选择:1、使用不同的关键词询问智能客服。2、点击左下角菜单内的问题反馈给我们留言,我们会尽快与您联系~', null, '', '', '45', '0', '0', '0');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'uni_push_switch', 'message_push', 'UniPush Switch', '', 'radio', '0', '[\"关闭\",\"开启\"]', '', '', '50', '0', '0', '0');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'uni_push_appid', 'message_push', 'UniPush appId', '请<a target="_blank" href="https://doc.fastadmin.net/fastim/1021.html">参考文档</a>后进行配置', 'string', '', null, '', '', '49', '0', '0', '0');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'uni_push_appkey', 'message_push', 'UniPush appKey', '', 'string', '', null, '', '', '48', '0', '0', '0');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'uni_push_master_secret', 'message_push', 'UniPush masterSecret', '', 'string', '', null, '', '', '47', '0', '0', '0');
+INSERT IGNORE INTO `__PREFIX__fastim_config` VALUES ('', 'package_name_android', 'message_push', 'Package Name Android', '', 'string', '', null, '', '', '46', '0', '0', '0');
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_csr_group`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_csr_group` (
+    `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `name` varchar(100) NOT NULL DEFAULT '' COMMENT '分组名称',
+    `note` varchar(100) NOT NULL DEFAULT '' COMMENT '备注',
+    `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
+    `status` enum('1','0') NOT NULL DEFAULT '0' COMMENT '状态:0=隐藏,1=正常',
+    `createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
+    `deletetime` int(10) unsigned DEFAULT NULL COMMENT '删除时间',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='客服分组表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_emoji`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_emoji` (
+    `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `group` varchar(20) NOT NULL DEFAULT 'default' COMMENT '分组',
+    `name` varchar(20) NOT NULL DEFAULT '' COMMENT '名称',
+    `code` varchar(20) NOT NULL DEFAULT '' COMMENT '唯一代码',
+    `image` varchar(200) NOT NULL DEFAULT '' COMMENT '图片地址',
+    `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
+    `unicode` varchar(20) NOT NULL DEFAULT '' COMMENT 'unicode编码',
+    `status` enum('1','0') NOT NULL DEFAULT '1' COMMENT '状态:0=隐藏,1=正常',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='Emoji表';
+
+-- ----------------------------
+-- Records of __PREFIX__fastim_emoji
+-- ----------------------------
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '龇牙', '[/zy]', '/assets/addons/fastim/img/emoji/1f601.png', '0', '1f601', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '笑哭', '[/xk]', '/assets/addons/fastim/img/emoji/1f602.png', '0', '1f602', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '开心', '[/kx]', '/assets/addons/fastim/img/emoji/1f603.png', '0', '1f603', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '微笑', '[/wx]', '/assets/addons/fastim/img/emoji/1f604.png', '0', '1f604', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '尴尬', '[/gg]', '/assets/addons/fastim/img/emoji/1f605.png', '0', '1f605', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '对眼笑', '[/dyx]', '/assets/addons/fastim/img/emoji/1f606.png', '0', '1f606', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '微笑光环', '[/wxgh]', '/assets/addons/fastim/img/emoji/1f607.png', '0', '1f607', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '眨眼', '[/zy]', '/assets/addons/fastim/img/emoji/1f609.png', '0', '1f609', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '不悦', '[/by]', '/assets/addons/fastim/img/emoji/1f612.png', '0', '1f612', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '汗', '[/h]', '/assets/addons/fastim/img/emoji/1f613.png', '0', '1f613', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '悲伤', '[/bs]', '/assets/addons/fastim/img/emoji/1f614.png', '0', '1f614', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '折磨', '[/zm]', '/assets/addons/fastim/img/emoji/1f616.png', '0', '1f616', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '右亲亲', '[/yqq]', '/assets/addons/fastim/img/emoji/1f618.png', '0', '1f618', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '发怒', '[/fn]', '/assets/addons/fastim/img/emoji/1f621.png', '0', '1f621', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '瞪眼汗', '[/dyh]', '/assets/addons/fastim/img/emoji/1f622.png', '0', '1f622', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '生气', '[/sq]', '/assets/addons/fastim/img/emoji/1f624.png', '0', '1f624', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '迷茫', '[/mm]', '/assets/addons/fastim/img/emoji/1f625.png', '0', '1f625', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '害怕', '[/hp]', '/assets/addons/fastim/img/emoji/1f628.png', '0', '1f628', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '快哭了', '[/kkl]', '/assets/addons/fastim/img/emoji/1f629.png', '0', '1f629', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '惊讶', '[/jy]', '/assets/addons/fastim/img/emoji/1f631.png', '0', '1f631', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '不忍直视', '[/brzs]', '/assets/addons/fastim/img/emoji/1f632.png', '0', '1f632', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '瞪着你', '[/dzn]', '/assets/addons/fastim/img/emoji/1f633.png', '0', '1f633', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '戴口罩', '[/dkz]', '/assets/addons/fastim/img/emoji/1f637.png', '0', '1f637', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '不可以', '[/bky]', '/assets/addons/fastim/img/emoji/1f645.png', '0', '1f645', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '摸头', '[/mt]', '/assets/addons/fastim/img/emoji/1f646.png', '0', '1f646', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '举手', '[/js]', '/assets/addons/fastim/img/emoji/1f64b.png', '0', '1f64b', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '举双手', '[/jss]', '/assets/addons/fastim/img/emoji/1f64c.png', '0', '1f64c', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '哀伤', '[/as]', '/assets/addons/fastim/img/emoji/1f64d.png', '0', '1f64d', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '捂口鼻', '[/wkb]', '/assets/addons/fastim/img/emoji/1f64a.png', '0', '1f64a', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '捂眼', '[/wy]', '/assets/addons/fastim/img/emoji/1f648.png', '0', '1f648', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '祈祷', '[/qd]', '/assets/addons/fastim/img/emoji/1f64f.png', '0', '1f64f', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '大哭', '[/dk]', '/assets/addons/fastim/img/emoji/1f62d.png', '0', '1f62d', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '可爱', '[/ka]', '/assets/addons/fastim/img/emoji/1f60a.png', '0', '1f60a', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '吐舌头', '[/tst]', '/assets/addons/fastim/img/emoji/1f60b.png', '0', '1f60b', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '闭眼笑', '[/byx]', '/assets/addons/fastim/img/emoji/1f60c.png', '0', '1f60c', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '色', '[/s]', '/assets/addons/fastim/img/emoji/1f60d.png', '0', '1f60d', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '邪笑', '[/xx]', '/assets/addons/fastim/img/emoji/1f60f.png', '0', '1f60f', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '眨眼笑', '[/zyx]', '/assets/addons/fastim/img/emoji/1f61c.png', '0', '1f61c', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '眯眼笑', '[/myx]', '/assets/addons/fastim/img/emoji/1f61d.png', '0', '1f61d', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '不开心', '[/bkx]', '/assets/addons/fastim/img/emoji/1f61e.png', '0', '1f61e', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '叹气', '[/tq]', '/assets/addons/fastim/img/emoji/1f62a.png', '0', '1f62a', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '快哭了', '[/kkl]', '/assets/addons/fastim/img/emoji/1f62b.png', '0', '1f62b', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '右斜笑眼', '[/yxxy]', '/assets/addons/fastim/img/emoji/1f31a.png', '0', '1f31a', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '左斜笑眼', '[/zxxy]', '/assets/addons/fastim/img/emoji/1f31d.png', '0', '1f31d', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '爱心', '[/ax]', '/assets/addons/fastim/img/emoji/2764.png', '0', '2764', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '信封', '[/xf]', '/assets/addons/fastim/img/emoji/2709.png', '0', '2709', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '飞机', '[/fj]', '/assets/addons/fastim/img/emoji/2708.png', '0', '2708', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', 'OK', '[/ok]', '/assets/addons/fastim/img/emoji/1f44c.png', '0', '1f44c', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '赞', '[/z]', '/assets/addons/fastim/img/emoji/1f44d.png', '0', '1f44d', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '石头', '[/st]', '/assets/addons/fastim/img/emoji/270a.png', '0', '270a', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '布', '[/b]', '/assets/addons/fastim/img/emoji/270b.png', '0', '270b', '1');
+INSERT IGNORE INTO `__PREFIX__fastim_emoji` VALUES ('', 'default', '剪刀', '[/jd]', '/assets/addons/fastim/img/emoji/270c.png', '0', '270c', '1');
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_fast_reply`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_fast_reply` (
+    `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '所属客服',
+    `title` varchar(100) NOT NULL DEFAULT '' COMMENT '标题',
+    `content` text NOT NULL COMMENT '回复内容',
+    `status` enum('1','0') NOT NULL DEFAULT '1' COMMENT '状态:0=关闭,1=启用',
+    `createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
+    `deletetime` int(10) unsigned DEFAULT NULL COMMENT '删除时间',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='快捷回复表';
+
+-- ----------------------------
+-- Records of __PREFIX__fastim_fast_reply
+-- ----------------------------
+INSERT IGNORE INTO `__PREFIX__fastim_fast_reply` VALUES ('', '0', '打招呼', '您好,请问有什么可以帮您?', '1', '1620659906', null);
+INSERT IGNORE INTO `__PREFIX__fastim_fast_reply` VALUES ('', '0', '询问联系方式', '您可以提供下您的联系方式么?您的电话是?或者QQ,我们可以更方便的联系您!', '1', '1620659906', null);
+INSERT IGNORE INTO `__PREFIX__fastim_fast_reply` VALUES ('', '0', '提示客户等待-处理中', '请稍等片刻,我们正在为您处理!', '1', '1620659906', null);
+INSERT IGNORE INTO `__PREFIX__fastim_fast_reply` VALUES ('', '0', '提示客户等待-问', '我去问一下,您稍等片刻~', '1', '1620659906', null);
+INSERT IGNORE INTO `__PREFIX__fastim_fast_reply` VALUES ('', '0', '道别', '那好,祝您生活愉快,再见!', '1', '1620659906', null);
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_friendship`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_friendship` (
+    `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
+    `friend_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '好友ID',
+    `group` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '分组ID',
+    `remark` varchar(30) NOT NULL DEFAULT '' COMMENT '备注名',
+    `top_contacts` tinyint(1) NOT NULL DEFAULT '0' COMMENT '常用联系人',
+    `createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
+    PRIMARY KEY (`user_id`,`friend_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='好友关系表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_group`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_group` (
+    `id` int(10) NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `type` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '类型:0=好友分组,1=群聊分组,2=收藏分组',
+    `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户',
+    `name` varchar(50) NOT NULL DEFAULT '' COMMENT '分组名称',
+    `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
+    PRIMARY KEY (`id`),
+    KEY `type_userid` (`type`,`user_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='各类分组表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_group_chat`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_group_chat` (
+    `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `leader` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '群主',
+    `avatar` varchar(200) NOT NULL DEFAULT '' COMMENT '头像',
+    `nickname` varchar(50) NOT NULL DEFAULT '' COMMENT '群昵称',
+    `bio` varchar(200) NOT NULL DEFAULT '' COMMENT '群介绍',
+    `max_user_count` smallint(5) unsigned NOT NULL DEFAULT '200' COMMENT '最大成员数量',
+    `add_mode` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '加群模式:0=需管理员审核,1=无需审核',
+    `invite_join_group` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '邀请模式:0=成员邀请好友无需审核,1=需要审核',
+    `speak` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '禁止成员发言:0=允许发言,1=禁止发言',
+    `history_message` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '历史消息:0=不允许新入群用户查看,1=允许新入群用户查看',
+    `retrieval_settings` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '检索设置:0=禁用搜素加群,1=允许被搜素到',
+    `createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
+    `deletetime` int(10) unsigned DEFAULT NULL COMMENT '删除时间',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='群聊表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_group_user`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_group_user` (
+    `group_id` int(10) unsigned NOT NULL COMMENT '群ID',
+    `user_id` int(10) unsigned NOT NULL COMMENT '用户ID',
+    `session_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '群聊会话ID',
+    `nickname` varchar(50) NOT NULL DEFAULT '' COMMENT '群内昵称',
+    `level` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '等级',
+    `score` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '积分',
+    `last_read_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最后阅读消息',
+    `user_group_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户分组',
+    `block_messages` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '屏蔽群消息',
+    `speaktime` int(10) unsigned DEFAULT NULL COMMENT '最后发言时间',
+    `jointime` int(10) unsigned DEFAULT NULL COMMENT '加入时间',
+    PRIMARY KEY (`group_id`,`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='群聊用户表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_kbs`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_kbs` (
+    `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `title` varchar(100) NOT NULL DEFAULT '' COMMENT '标题',
+    `keyword` varchar(100) NOT NULL DEFAULT '' COMMENT '关键词',
+    `content` text NOT NULL COMMENT '内容',
+    `views` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '浏览量',
+    `likes` mediumint(9) unsigned NOT NULL DEFAULT '0' COMMENT '有帮助数',
+    `dislikes` mediumint(9) unsigned NOT NULL DEFAULT '0' COMMENT '无帮助数',
+    `note` varchar(100) NOT NULL DEFAULT '' COMMENT '备注',
+    `status` enum('1','0') NOT NULL DEFAULT '0' COMMENT '状态:0=隐藏,1=正常',
+    `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
+    `updatetime` int(10) unsigned DEFAULT NULL COMMENT '更新时间',
+    `createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
+    `deletetime` int(10) unsigned DEFAULT NULL COMMENT '删除时间',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='知识库';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_message`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_message` (
+    `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `type` enum('voice','group_chat_notice','kbs_list','friend_apply','group_invitation','group_notice','group_apply','system','link','file','video','audio','image','default') NOT NULL DEFAULT 'default' COMMENT '消息类型:default=普通,image=图片,audio=音频,video=视频,file=文件,link=链接,system=系统,group_apply=申请入群,group_notice=群通知,group_invitation=邀请入群,friend_apply=好友申请,kbs_list=知识库消息,group_chat_notice=群公告,voice=语音',
+    `group_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '群聊ID',
+    `session_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '会话ID',
+    `sender_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '发送人',
+    `recipient_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '接受人',
+    `message` text COMMENT '消息内容',
+    `read_number` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '群消息阅读数',
+    `status` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态:0=未读,1=已读,2=发送中,3=隐藏状态,4=失败',
+    `createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
+    `deleteuser` int(10) unsigned DEFAULT NULL COMMENT '删除消息用户',
+    PRIMARY KEY (`id`),
+    KEY `group_id` (`group_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='消息记录表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_report`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_report` (
+    `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `type` enum('user','group','feedback') NOT NULL DEFAULT 'feedback' COMMENT '类型:user=举报用户,group=举报群聊,feedback=问题反馈',
+    `session_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '会话ID',
+    `report_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '举报人',
+    `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '被举报对象',
+    `describe` varchar(200) NOT NULL DEFAULT '' COMMENT '举报详情',
+    `mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '联系方式',
+    `reportimage` varchar(400) NOT NULL DEFAULT '' COMMENT '举报证据',
+    `status` enum('1','0') NOT NULL DEFAULT '0' COMMENT '状态:0=未处理,1=已处理',
+    `createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='举报记录表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_service`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_service` (
+    `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `avatar` varchar(100) NOT NULL DEFAULT '' COMMENT '头像',
+    `nickname` varchar(50) NOT NULL DEFAULT '' COMMENT '显示昵称',
+    `bio` varchar(100) NOT NULL DEFAULT '' COMMENT '介绍',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='服务账号表';
+
+-- ----------------------------
+-- Records of __PREFIX__fastim_service
+-- ----------------------------
+INSERT IGNORE INTO `__PREFIX__fastim_service` VALUES ('1', '/assets/addons/fastim/img/new_friends.png', '新朋友', '接受新好友验证消息');
+INSERT IGNORE INTO `__PREFIX__fastim_service` VALUES ('2', '', '群通知', '接受群聊通知消息');
+INSERT IGNORE INTO `__PREFIX__fastim_service` VALUES ('3', '/assets/addons/fastim/img/csr.png', '智能客服小蜜', '接受客服服务消息');
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_session`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_session` (
+    `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `type` enum('service','group','single') NOT NULL DEFAULT 'single' COMMENT '类型:single=单聊,group=群聊,service=服务号',
+    `user_one` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户1',
+    `user_two` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户2',
+    `chat_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '群聊或服务号id',
+    `createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
+    `deleteuser` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户单方删除',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='会话表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_session_top`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_session_top` (
+    `user_id` int(10) NOT NULL DEFAULT '0' COMMENT '用户',
+    `session_id` int(10) NOT NULL DEFAULT '0' COMMENT '会话',
+    `top` int(10) NOT NULL DEFAULT '0' COMMENT '置顶顺序',
+    PRIMARY KEY (`user_id`,`session_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会话置顶记录';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_todo`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_todo` (
+    `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户',
+    `title` text NOT NULL COMMENT '待办标题',
+    `status` enum('1','0') NOT NULL DEFAULT '0' COMMENT '状态:0=未完成,1=已完成',
+    `createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
+    `deletetime` int(10) unsigned DEFAULT NULL COMMENT '删除时间',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='用户待办事项';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_trajectory`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_trajectory` (
+    `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
+    `type` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '轨迹类型:0=访问',
+    `note` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '轨迹描述',
+    `url` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '访问URL',
+    `referrer` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '来路',
+    `ip` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'IP',
+    `tourist` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '游客登录:0=否,1=是',
+    `extend` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '扩展数据',
+    `createtime` int(10) unsigned DEFAULT NULL COMMENT '添加时间',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户轨迹表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_user`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_user` (
+    `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `type` enum('tourist','user','csr') NOT NULL DEFAULT 'tourist' COMMENT '用户类型:tourist=游客,user=会员,csr=客服',
+    `group_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '客服分组',
+    `user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '绑定用户',
+    `admin_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '绑定管理员',
+    `avatar` varchar(200) NOT NULL DEFAULT '' COMMENT '头像',
+    `nickname` varchar(50) NOT NULL DEFAULT '' COMMENT '昵称',
+    `referrer` varchar(200) NOT NULL DEFAULT '' COMMENT '用户来路',
+    `mobile` varchar(11) NOT NULL DEFAULT '' COMMENT '手机',
+    `email` varchar(50) NOT NULL DEFAULT '' COMMENT '邮箱',
+    `gender` enum('female','male','secrecy') NOT NULL DEFAULT 'secrecy' COMMENT '性别:secrecy=保密,male=男,female=女',
+    `age` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '年龄',
+    `birthday` date DEFAULT NULL COMMENT '生日',
+    `bio` varchar(200) NOT NULL DEFAULT '' COMMENT '格言',
+    `occupation` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '职业',
+    `company` varchar(100) NOT NULL DEFAULT '' COMMENT '公司',
+    `status` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态:0=离线,1=在线,2=忙碌,3=隐身',
+    `note` varchar(255) NOT NULL DEFAULT '' COMMENT '客服备注',
+    `token` varchar(59) NOT NULL DEFAULT '' COMMENT 'Token',
+    `auth_token` varchar(32) NOT NULL DEFAULT '' COMMENT '授权Token',
+    `loginip` varchar(50) NOT NULL DEFAULT '' COMMENT '登录ip',
+    `wechat_openid` varchar(28) NOT NULL DEFAULT '' COMMENT '微信openid',
+    `welcome_msg` text COMMENT '客服欢迎语',
+    `reception_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '客服当前接待量',
+    `last_reception_time` int(10) unsigned DEFAULT NULL COMMENT '客服上次接待时间',
+    `createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_user_collection`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_user_collection` (
+    `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户',
+    `group_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '分组',
+    `type` enum('kbs_list','link','file','video','audio','image','default') NOT NULL DEFAULT 'default' COMMENT '消息类型:default=普通,image=图片,audio=音频,video=视频,file=文件,link=链接,kbs_list=知识库消息',
+    `value` text COMMENT '收藏内容',
+    `from` varchar(50) NOT NULL DEFAULT '' COMMENT '来自',
+    `createtime` int(10) unsigned DEFAULT NULL COMMENT '收藏时间',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='用户收藏';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_user_config`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_user_config` (
+    `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户',
+    `name` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '变量名',
+    `value` text COLLATE utf8mb4_unicode_ci COMMENT '变量值',
+    PRIMARY KEY (`id`),
+    KEY `user_id` (`user_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户配置表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_user_push_clientid`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_user_push_clientid` (
+    `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
+    `clientid` varchar(32) NOT NULL DEFAULT '' COMMENT 'clientid',
+    `platform` enum('android','ios') NOT NULL DEFAULT 'android' COMMENT '用户系统平台',
+    `updatetime` int(10) unsigned DEFAULT NULL COMMENT '更新时间',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='用户推送clientid表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__fastim_user_shield`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_user_shield` (
+    `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户',
+    `shield_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '被屏蔽人',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='用户屏蔽关系表';
+
+-- ----------------------------
+-- v1.0.1 Table structure for `__PREFIX__fastim_group_chat_notice`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_group_chat_notice` (
+    `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `group_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '群聊',
+    `publisher` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '发布人',
+    `content` text NOT NULL COMMENT '内容',
+    `images` varchar(500) NOT NULL DEFAULT '' COMMENT '图片',
+    `popup` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '使用弹窗展示公告:0=否,1=是',
+    `receipt` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '需群成员确认:0=否,1=是',
+    `top` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '置顶:0=否,1=是',
+    `createtime` int(10) unsigned DEFAULT NULL COMMENT '发布时间',
+    `deletetime` int(10) unsigned DEFAULT NULL COMMENT '删除时间',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='群聊公告';
+
+-- ----------------------------
+-- v1.0.1 Table structure for `__PREFIX__fastim_reading_log`
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_reading_log` (
+    `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `type` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '阅读类型:0=群公告,1=语音消息播放状态,2=预阅读的@消息',
+    `group_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '群聊ID',
+    `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
+    `data_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '数据ID',
+    `receipt` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '确认:0=否,1=是',
+    `createtime` int(10) unsigned DEFAULT NULL COMMENT '阅读时间',
+    `confirmtime` int(10) unsigned DEFAULT NULL COMMENT '确认时间',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='通用阅读日志';
+
+-- ---------------------------
+-- v1.0.1 Table structure for `__PREFIX__fastim_captcha`
+-- ---------------------------
+CREATE TABLE IF NOT EXISTS `__PREFIX__fastim_captcha` (
+    `key` varchar(32) NOT NULL DEFAULT '' COMMENT '验证码Key',
+    `code` varchar(32) NOT NULL DEFAULT '' COMMENT '验证码',
+    `captcha` varchar(6) NOT NULL DEFAULT '' COMMENT '验证码(供uniapp安卓二次生成图片)',
+    `createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
+    `expiretime` int(10) unsigned DEFAULT NULL COMMENT '过期时间',
+    PRIMARY KEY (`key`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户验证码表';

+ 21 - 0
addons/fastim/lang/api/zh-cn.php

@@ -0,0 +1,21 @@
+<?php
+return [
+    'Invalid parameter'                            => '无效的参数',
+    'Mobile is incorrect'                          => '手机号不正确',
+    'Username or password is incorrect'            => '用户名或密码不正确',
+    'Username already exist'                       => '用户名已经存在',
+    'Nickname already exist'                       => '昵称已经存在',
+    'Username can not be empty'                    => '用户名不能为空',
+    'Username must be 3 to 30 characters'          => '用户名必须3-30个字符',
+    'Password can not be empty'                    => '密码不能为空',
+    'Password must be 6 to 30 characters'          => '密码必须6-30个字符',
+    'Captcha is incorrect'                         => '验证码不正确',
+    'Please enter the correct mobile phone number' => '请输入正确的手机号',
+    'Mobile already exist'                         => '手机号已经存在',
+    'Sign up successful'                           => '注册成功',
+    'Failed to bind new user, please try again'    => '绑定新用户失败,请重试!',
+    'Unrecognized user'                            => '无法识别用户!',
+    'Abnormal account status'                      => '账户状态异常',
+    'Please enter the verification code'           => '请输入验证码!',
+    'Please enter the correct verification code'   => '请输入正确的验证码~',
+];

+ 28 - 0
addons/fastim/lang/zh-cn.php

@@ -0,0 +1,28 @@
+<?php
+return [
+    'General'                                        => '通用配置',
+    'Privacy'                                        => '隐私配置',
+    'Im name'                                        => '应用名称',
+    'Send Message button'                            => '发送消息按键',
+    'Press ESC to close the window'                  => '按ESC键关闭窗口',
+    'New message window jitter'                      => '新消息抖动(窗口)',
+    'New message playing tone'                       => '新消息播放提示音',
+    'New message push notice'                        => '新消息推送通知',
+    'Window display push notification'               => '窗口显示时推送通知',
+    'Temporary session'                              => '临时会话',
+    'Add my way'                                     => '添加我的方式',
+    'Display input status'                           => '展示输入状态',
+    'Post login status'                              => '登录后<a href="javascript:;" title="你当前状态在离线1分钟后才会注销,之后重新登录Im,将自动使用此处指定的新状态">状态</a>',
+    'Auto reply when busy'                           => '忙碌时的自动回复',
+    'Add me as a friend'                             => '加我为好友时',
+    'Uploaded file format is limited'                => '上传文件格式受限制',
+    'No file upload or server upload limit exceeded' => '未上传文件或超出服务器上传限制',
+    'Uploaded file is not a valid image'             => '上传文件不是有效的图片文件',
+    'File is too big (%sMiB), Max filesize: %sMiB'   => '当前上传(%sM),最大允许上传文件大小:%sM',
+    'Uploaded successful'                            => '上传成功',
+    'Mobile privacy'                                 => '手机号隐私性',
+    'Email privacy'                                  => '邮箱隐私性',
+    'Company privacy'                                => '公司隐私性',
+    'Age privacy'                                    => '年龄和生日隐私性',
+    'Occupation privacy'                             => '职业隐私性'
+];

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 28 - 0
addons/fastim/library/Captcha.php


+ 2233 - 0
addons/fastim/library/Common.php

@@ -0,0 +1,2233 @@
+<?php
+
+namespace addons\fastim\library;
+
+use think\Db;
+use addons\fastim\library\CommonCode;
+
+/**
+ * 公共类
+ */
+class Common
+{
+
+    /**
+     * 将数组通过拼命的首字排序
+     * @param array $arr 待排序数组
+     * @param string $field 排序依据字段
+     * @return array
+     */
+    public static function initialPinyinArrSort($arr, $field)
+    {
+        $capitalPreg = '/\b[A-Z]+\b/'; // 匹配大写字母
+
+        $initialPinyinArr     = [];
+        $initialPinyinSpecial = [];
+
+        foreach ($arr as $key => $value) {
+            $arr[$key]['initial'] = self::initialPinyin($arr[$key][$field]);
+            if (preg_match($capitalPreg, $arr[$key]['initial'])) {
+                $initialPinyinArr[$arr[$key]['initial']][] = $arr[$key];
+            } else {
+                $arr[$key]['initial']   = '#';
+                $initialPinyinSpecial[] = $arr[$key];
+            }
+        }
+
+        $initialPinyinIndex = array_keys($initialPinyinArr);
+        sort($initialPinyinIndex);
+
+        if ($initialPinyinSpecial) {
+            $initialPinyinIndex[]  = '#';
+            $initialPinyinArr['#'] = $initialPinyinSpecial;
+        }
+
+        return [
+            'initialPinyinArr'   => $initialPinyinArr,
+            'initialPinyinIndex' => $initialPinyinIndex
+        ];
+    }
+
+    /**
+     * 获得字符串首字的拼音字母
+     * @param string $chinese
+     * @return string 首拼字母
+     */
+    public static function initialPinyin($chinese)
+    {
+        $chinese = \fast\Pinyin::get(mb_substr($chinese, 0, 1), true);
+        return ucfirst($chinese);
+    }
+
+    /**
+     * 格式化用户职业
+     * @param int $occupation
+     */
+    public static function formatOccupation($occupation)
+    {
+        $occupationLang = [
+            0  => '保密',
+            1  => '计算机/互联网/通信',
+            2  => '生产/工艺/制造',
+            3  => '医疗/护理/制药',
+            4  => '金融/银行/投资/保险',
+            5  => '商业/服务业/个体经营',
+            6  => '文化/广告/传媒',
+            7  => '娱乐/艺术/表演',
+            8  => '律师/法务',
+            9  => '教育/培训',
+            10 => '公务员/行政/事业单位',
+            11 => '模特',
+            12 => '空姐',
+            13 => '学生',
+            14 => '其他'
+        ];
+
+        if ($occupation === false) {
+            return $occupationLang;
+        } elseif (array_key_exists($occupation, $occupationLang)) {
+            return $occupationLang[$occupation];
+        } else {
+            return '';
+        }
+    }
+
+    /**
+     * 从知识库获取内容
+     * @param string $keywords 关键词
+     * @param int $id
+     * @return array|bool
+     */
+    public static function getKbs($keywords, $id = null)
+    {
+        if (!is_string($keywords)) {
+            return false;
+        }
+
+        if ($id) {
+            $kbs = Db::name('fastim_kbs')->where('id', $id)->where('status', '1')->where('deletetime', null)->find();
+            return $kbs;
+        }
+
+        $kbs = Db::name('fastim_kbs')
+            ->field('id,title')
+            ->where('title|keyword', 'like', '%' . $keywords . '%')
+            ->where('status', '1')
+            ->where('deletetime', null)
+            ->order('weigh desc')
+            ->limit(6)
+            ->select();
+        return $kbs;
+    }
+
+    /**
+     * 建立与指定用户的会话
+     * @param  [type] $type        会话类型:single=单聊,group=群聊,service=服务号
+     * @param  [type] $userId      用户
+     * @param  [type] $sessionUser 对话用户
+     * @return array|bool 会话资料
+     */
+    public static function imSession($type, $userId, $sessionUser)
+    {
+        if ($type == 'single') {
+            $imSession = Db::name('fastim_session')
+                ->where('type', 'single')
+                ->where('(user_one=:uid1 AND user_two=:sid1) OR (user_one=:sid2 AND user_two=:uid2)')
+                ->bind([
+                    'uid1' => $userId,
+                    'sid1' => $sessionUser,
+                    // 防止报错,tp-bug?
+                    'uid2' => $userId,
+                    'sid2' => $sessionUser
+                ])
+                ->find();
+        } else {
+            $imSession = Db::name('fastim_session')
+                ->where('type', $type)
+                ->where('user_one', $userId)
+                ->where('chat_id', $sessionUser)
+                ->find();
+        }
+
+        if (!$imSession) {
+            // 建立
+            $insert = [
+                'type'       => $type,
+                'user_one'   => $userId,
+                'user_two'   => ($type == 'single') ? $sessionUser : 0,
+                'chat_id'    => ($type == 'single') ? 0 : $sessionUser,
+                'createtime' => time()
+            ];
+            if (Db::name('fastim_session')->insert($insert)) {
+                $insert['id']     = Db::name('fastim_session')->getLastInsID();
+                $insert['create'] = true;// 标记建立
+                $imSession        = $insert;
+            } else {
+                return false;
+            }
+        } elseif ($imSession['deleteuser'] == $userId && $type != 'service') {
+            Db::name('fastim_session')->where('id', $imSession['id'])->update([
+                'deleteuser' => 0
+            ]);
+        }
+        return $imSession;
+    }
+
+
+    /**
+     * 服务号聊天记录
+     * @param array $sessionInfo 会话资料
+     * @param array $page 页码
+     * @param int $uid 加载用户
+     * @return array              聊天记录
+     */
+    public static function loadServiceRecord($sessionInfo, $page, $uid)
+    {
+        $resultArr = [
+            'agree'     => '已同意',
+            'refuse'    => '已拒绝',
+            'autoagree' => '已自动同意'
+        ];
+
+        // 好友申请
+        if ($sessionInfo['chat_id'] == 1) {
+            $message = Db::name('fastim_message')
+                ->where('type', 'friend_apply')
+                ->where('sender_id|recipient_id', $uid)
+                ->order('createtime desc')
+                ->limit($page['min'], $page['pageCount'])
+                ->select();
+            foreach ($message as $key => $value) {
+                $message[$key]['message']    = json_decode($value['message'], true);
+                $userId                      = ($value['sender_id'] == $uid) ? $value['recipient_id'] : $value['sender_id'];
+                $message[$key]['userInfo']   = self::getImUserInfo($userId);
+                $message[$key]['friendship'] = Db::name('fastim_friendship')
+                    ->where('user_id', $uid)
+                    ->where('friend_id', $userId)
+                    ->value('user_id');
+            }
+
+            $windowType = 'new_friends_message';
+        } elseif ($sessionInfo['chat_id'] == 2) {
+            // 群通知
+            $message = Db::name('fastim_message')
+                ->whereIn('type', 'group_apply,group_notice,group_invitation')
+                ->where('sender_id|recipient_id', $uid)
+                ->order('createtime desc')
+                ->limit($page['min'], $page['pageCount'])
+                ->select();
+            foreach ($message as $key => $value) {
+                $message[$key]['message'] = json_decode($value['message'], true);
+
+                // 群通知消息
+                $message[$key]['groupChatInfo'] = self::getImGroupChat($message[$key]['group_id']);
+
+                if ($value['type'] == 'group_invitation') {
+                    $message[$key]['invitationUser'] = self::getImUserInfo($message[$key]['message']['invitation_user']);
+                    $message[$key]['invitedUser']    = self::getImUserInfo($message[$key]['message']['invited_user']);
+
+                    $message[$key]['message'] = array_merge($message[$key]['message'], self::invitationMessageResult($message[$key]));
+
+                } elseif ($value['type'] == 'group_apply' && $value['recipient_id'] == $uid) {
+                    // x申请加入群聊y,获取发送人信息
+                    $message[$key]['userInfo'] = self::getImUserInfo($value['sender_id']);
+                } elseif ($value['type'] == 'group_notice') {
+                    // x退出了群聊
+                    if ($message[$key]['message']['action'] == 'quit') {
+                        $message[$key]['userInfo'] = self::getImUserInfo($message[$key]['message']['quit_user']);
+                    } elseif ($message[$key]['message']['action'] == 'refuse') {
+                        $message[$key]['userInfo'] = self::getImUserInfo($message[$key]['message']['refuse_user']);
+                    }
+                }
+            }
+
+            $windowType = 'group_notice';
+        } elseif ($sessionInfo['chat_id'] == 3) {
+            // 客服消息
+            $message = Db::name('fastim_message')
+                ->where('session_id', $sessionInfo['id'])
+                ->where(function ($query) use ($uid) {
+                    $query->where('recipient_id', $uid)->where('status', '<', 4);
+                    $query->whereOr('sender_id', $uid);
+                })
+                ->order('createtime desc,id desc')
+                ->limit($page['min'], $page['pageCount'])
+                ->select();
+
+            foreach ($message as $key => &$value) {
+                if ($value['sender_id']) {
+                    $value['csr'] = self::getImUserInfo($value['sender_id']);
+                }
+                $value['sender'] = ($value['recipient_id'] == $uid) ? 'you' : 'me';
+                if (in_array($value['type'], CommonCode::$messageJsonTypes)) {
+                    $value['message'] = json_decode($value['message'], true);
+                } else {
+                    $value['message'] = htmlspecialchars_decode($value['message']);
+                }
+
+                if ($value['type'] == 'voice') {
+                    // 获取语音播放状态
+                    $value['voice_playback'] = Db::name('fastim_reading_log')
+                        ->where('type', 1)
+                        ->where('user_id', $uid)
+                        ->where('data_id', $value['id'])
+                        ->value('id');
+                }
+            }
+
+            $windowType = 'csr_message';
+        }
+
+        return [
+            'message'    => $message,
+            'windowType' => $windowType
+        ];
+    }
+
+    /**
+     * 邀请好友入群消息的 result 处理
+     * result:0=未处理,1=同意,2=拒绝,3=失效
+     * @param array $message 消息
+     * @return array
+     */
+    public static function invitationMessageResult($message)
+    {
+        if ($message['message']['identity'] == 'admin') {
+            $leaderResult = $message['message']['result'];
+            $userResult   = 0;
+
+            $invitedUserSession = Common::imSession('service', $message['message']['invited_user'], 2);
+            $invitationMessage  = Db::name('fastim_message')
+                ->where('type', 'group_invitation')
+                ->where('session_id', $invitedUserSession['id'])
+                ->select();
+            foreach ($invitationMessage as $mkey => $mvalue) {
+                $mvalue['message'] = json_decode($mvalue['message'], true);
+                if (isset($mvalue['message']['admin_message_id']) && $mvalue['message']['admin_message_id'] == $message['id']) {
+                    $userResult = $mvalue['message']['result'];
+                    break;
+                }
+            }
+        } else {
+            $userResult   = $message['message']['result'];
+            $leaderResult = 0;
+
+            $invitationMessage = Db::name('fastim_message')
+                ->where('id', $message['message']['admin_message_id'])
+                ->find();
+            if ($invitationMessage) {
+                $invitationMessage['message'] = json_decode($invitationMessage['message'], true);
+                $leaderResult                 = $invitationMessage['message']['result'];
+            }
+        }
+
+        return [
+            'user_result'   => $userResult,
+            'leader_result' => $leaderResult
+        ];
+    }
+
+    /**
+     * 阅读服务号消息
+     * @param int $chatId 服务号ID
+     * @param int $uid 阅读用户
+     * @param int $sessionId 会话ID
+     */
+    public static function readServiceMessage($chatId, $uid, $sessionId)
+    {
+        if ($chatId == 1) {
+            // 好友申请
+            Db::name('fastim_message')
+                ->where('type', 'friend_apply')
+                ->where('recipient_id', $uid)
+                ->where('status', 0)
+                ->update(['status' => 1]);
+        } elseif ($chatId == 2) {
+            // 群通知
+            Db::name('fastim_message')
+                ->whereIn('type', 'group_apply,group_notice,group_invitation')
+                ->where('recipient_id', $uid)
+                ->where('status', 0)
+                ->update(['status' => 1]);
+        } elseif ($chatId == 3) {
+            Db::name('fastim_message')
+                ->where('session_id', $sessionId)
+                ->where('recipient_id', $uid)
+                ->where('status', 0)
+                ->update(['status' => 1]);
+        }
+    }
+
+    /**
+     * 群聊聊天记录
+     * @param array $sessionInfo 会话资料
+     * @param array $page 页码
+     * @param int $uid 加载用户
+     * @return array              聊天记录
+     */
+    public static function loadGroupChatRecord($sessionInfo, $page, $uid)
+    {
+        // 用户入群时间
+        $userJoinTime = Db::name('fastim_group_user')
+            ->where('group_id', $sessionInfo['chat_id'])
+            ->where('user_id', $uid)
+            ->value('jointime');
+        $userJoinTime = $userJoinTime ? $userJoinTime : 0;// 防null无法比较大小
+
+        $message = Db::name('fastim_message')
+            ->where('group_id', $sessionInfo['chat_id'])
+            ->where(function ($query) use ($uid) {
+                // 失败的消息不显示给其他群成员
+                $query->where('sender_id', $uid);
+                $query->whereOr('status', '<', 4);
+            })
+            ->where(function ($query) use ($sessionInfo, $userJoinTime, $uid) {
+                $typeSql = CommonCode::nonPrivilegedMessage(true);
+                if ($sessionInfo['sessionUser']['history_message'] == 0) {
+                    // 以下代码虽看起来重复判断了`createtime`,但没有逻辑问题,改动后部分情景下会计算错误
+                    $query->where("type {$typeSql} OR (type='system' AND (recipient_id={$uid} OR (recipient_id=0 AND createtime>={$userJoinTime})))")
+                        ->where('createtime', '>=', $userJoinTime);
+                } else {
+                    // 群通知:只显示发送时间大于用户加入时间的或直接发送给当前用户的群系统消息
+                    $query->where("type {$typeSql} OR (type='system' AND (recipient_id={$uid} OR (recipient_id=0 AND createtime>={$userJoinTime})))");
+                }
+            })
+            ->order('createtime desc,id desc')
+            ->limit($page['min'], $page['pageCount'])
+            ->select();
+
+        foreach ($message as $key => $value) {
+            $senderCollection[]      = $value['sender_id'];
+            $message[$key]['sender'] = ($value['sender_id'] == $uid) ? 'me' : 'you';
+            if (in_array($value['type'], CommonCode::$messageJsonTypes)) {
+                $message[$key]['message'] = json_decode($value['message'], true);
+            } else {
+                $message[$key]['message'] = htmlspecialchars_decode($value['message']);
+            }
+
+            if ($value['type'] == 'voice') {
+                // 获取语音播放状态
+                $message[$key]['voice_playback'] = Db::name('fastim_reading_log')
+                    ->where('type', 1)
+                    ->where('user_id', $uid)
+                    ->where('data_id', $value['id'])
+                    ->value('id');
+            }
+        }
+
+        // 取得所有发送人的资料
+        if (isset($senderCollection) && $senderCollection) {
+            $senderCollection = array_unique($senderCollection);
+            foreach ($senderCollection as $key => $value) {
+                $senderInfo[$value] = self::getImUserInfo($value, false, $uid);
+                // 取得用户群聊昵称
+                $groupUserNickname = Db::name('fastim_group_user')
+                    ->where('group_id', $sessionInfo['chat_id'])
+                    ->where('user_id', $value)
+                    ->value('nickname');
+                if ($groupUserNickname) {
+                    $senderInfo[$value]['nickname'] = $groupUserNickname;
+                    // 必要时重新生成头像
+                    $defaultAvatar                = preg_match('/^data:image/', $senderInfo[$value]['avatar']);
+                    $senderInfo[$value]['avatar'] = self::avatarSrc([
+                        $defaultAvatar ? '' : $senderInfo[$value]['avatar']
+                    ], $groupUserNickname);
+                }
+            }
+            foreach ($message as $key => $value) {
+                if (array_key_exists($value['sender_id'], $senderInfo)) {
+                    $message[$key]['userInfo'] = $senderInfo[$value['sender_id']];
+                } else {
+                    // 未查到用户资料
+                    $message[$key]['userInfo'] = [
+                        'id'     => 0,
+                        'avatar' => self::avatarSrc(false, '')
+                    ];
+                }
+            }
+        }
+
+        return $message;
+    }
+
+    /**
+     * 格式化群聊公告
+     * @param array $unreadFixedMsg 群聊公告资料
+     */
+    public static function formatGroupChatNotice($unreadFixedMsg, $userId, $record = false)
+    {
+        if ($unreadFixedMsg) {
+            $unreadFixedMsg['images']         = $unreadFixedMsg['images'] ? explode(',', trim($unreadFixedMsg['images'], ',')) : '';
+            $unreadFixedMsg['createtime']     = self::formatTimeAlong($unreadFixedMsg['createtime']);
+            $unreadFixedMsg['publisher']      = self::getImUserInfo($unreadFixedMsg['publisher'], false, $userId);
+            $unreadFixedMsg['reading_number'] = Db::name('fastim_reading_log')
+                ->where('type', 0)
+                ->where('group_id', $unreadFixedMsg['group_id'])
+                ->where('data_id', $unreadFixedMsg['id'])
+                ->count();
+
+            if (isset($unreadFixedMsg['receipt']) && $unreadFixedMsg['receipt']) {
+                // 是否已确认
+                $log = Db::name('fastim_reading_log')
+                    ->field('id, receipt')
+                    ->where('type', 0)
+                    ->where('group_id', $unreadFixedMsg['group_id'])
+                    ->where('user_id', $userId)
+                    ->where('data_id', $unreadFixedMsg['id'])
+                    ->find();
+                if ($log && $log['receipt']) {
+                    $unreadFixedMsg['receipted'] = true;
+                }
+
+                // 多少人已确认
+                $unreadFixedMsg['receiptedCount'] = Db::name('fastim_reading_log')
+                    ->where('type', 0)
+                    ->where('group_id', $unreadFixedMsg['group_id'])
+                    ->where('data_id', $unreadFixedMsg['id'])
+                    ->where('receipt', 1)
+                    ->count();
+            }
+
+            if ($record) {
+                // 记录阅读
+                self::readGroupChatNotice($unreadFixedMsg['group_id'], $userId, $unreadFixedMsg['id']);
+            }
+            return $unreadFixedMsg;
+        }
+        return false;
+    }
+
+    public static function readGroupChatNotice($groupId, $userId, $dataId, $type = '', $receipt = false)
+    {
+        if ($type == 'all') {
+            $unreadFixedMsg = Db::name('fastim_group_chat_notice')
+                ->alias('cn')
+                ->field('cn.*,r.id AS rid')
+                ->join('fastim_reading_log r', 'r.type=0 AND r.user_id=:user_id AND r.data_id=cn.id', 'LEFT')
+                ->where('cn.group_id', $groupId)
+                ->where('cn.deletetime', null)
+                ->where('r.id', null)
+                ->bind([
+                    'user_id' => $userId
+                ])
+                ->order('createtime desc')
+                ->select();
+            $insert         = false;
+            foreach ($unreadFixedMsg as $item) {
+                $insert[] = [
+                    'type'        => 0,
+                    'group_id'    => $groupId,
+                    'user_id'     => $userId,
+                    'data_id'     => $item['id'],
+                    'receipt'     => $receipt ? 1 : 0,
+                    'createtime'  => time(),
+                    'confirmtime' => $receipt ? time() : null
+                ];
+            }
+            if ($insert) {
+                Db::name('fastim_reading_log')->insertAll($insert);
+            }
+            return true;
+        }
+
+        $reading = Db::name('fastim_reading_log')
+            ->where('type', 0)
+            ->where('group_id', $groupId)
+            ->where('user_id', $userId)
+            ->where('data_id', $dataId)
+            ->value('id');
+        if ($reading) {
+            return true;
+        }
+        Db::name('fastim_reading_log')
+            ->insert([
+                'type'        => 0,
+                'group_id'    => $groupId,
+                'user_id'     => $userId,
+                'data_id'     => $dataId,
+                'receipt'     => $receipt ? 1 : 0,
+                'createtime'  => time(),
+                'confirmtime' => $receipt ? time() : null
+            ]);
+        return true;
+    }
+
+    /**
+     * 单聊聊天记录
+     * @param int $session_id 会话ID
+     * @param array $page 页码
+     * @param int $uid 加载记录的用户
+     * @return array|bool             聊天记录
+     */
+    public static function loadSingleChatRecord($session_id, $page, $uid)
+    {
+        $message = Db::name('fastim_message')
+            ->where('session_id', $session_id)
+            ->where('deleteuser IS NULL OR deleteuser!=:uid')
+            ->where(function ($query) use ($uid) {
+                $query->where('recipient_id', $uid)->where('status', '<', 4);
+                $query->whereOr('sender_id', $uid);
+            })
+            ->bind([
+                'uid' => $uid
+            ])
+            ->order('createtime desc,id desc')
+            ->limit($page['min'], $page['pageCount'])
+            ->select();
+
+        foreach ($message as $key => &$value) {
+            $value['sender'] = ($value['sender_id'] == $uid) ? 'me' : 'you';
+            if (in_array($value['type'], CommonCode::$messageJsonTypes)) {
+                $value['message'] = json_decode($value['message'], true);
+            } else {
+                $value['message'] = htmlspecialchars_decode($value['message']);
+            }
+
+            if ($value['type'] == 'voice') {
+                // 获取语音播放状态
+                $value['voice_playback'] = Db::name('fastim_reading_log')
+                    ->where('type', 1)
+                    ->where('user_id', $uid)
+                    ->where('data_id', $value['id'])
+                    ->value('id');
+            }
+        }
+        return $message;
+    }
+
+    /**
+     * 将消息记录按时间进行分组
+     * @param array $message 消息记录
+     * @return array
+     */
+    public static function groupByTime($message, $orderBy = 'asc')
+    {
+        if (!$message) {
+            return [];
+        }
+
+        $messageTemp = [];
+        $createtime  = $message[0]['createtime'];
+        foreach ($message as $key => $value) {
+
+            if ($orderBy == 'asc') {
+                $diff = $createtime - $value['createtime'];
+            } else {
+                $diff = $value['createtime'] - $createtime;
+            }
+
+            if ($diff < 3600) {
+                $messageTemp[$createtime][] = $value;
+            } else {
+                $createtime                 = $value['createtime'];
+                $messageTemp[$createtime][] = $value;
+            }
+        }
+        unset($message);
+        foreach ($messageTemp as $key => $value) {
+            $message[] = [
+                'datetime' => self::formatTime($key),
+                'data'     => $value
+            ];
+        }
+        unset($messageTemp);
+        return $message;
+    }
+
+    /**
+     * 获取分组详情
+     * @param int $id 分组ID
+     * @return array
+     */
+    public static function getGroupInfo($id)
+    {
+        // type分组类型:0=好友,1=群聊,2=收藏
+        $groupNames = [
+            'all_friends'    => [
+                'type' => 0,
+                'name' => '全部好友'
+            ],
+            'common'         => [
+                'type' => 0,
+                'name' => '常用联系人'
+            ],
+            'all_group'      => [
+                'type' => 1,
+                'name' => '全部群聊'
+            ],
+            'all_collection' => [
+                'type' => 2,
+                'name' => '全部收藏'
+            ],
+            'default'        => [
+                'type' => 2,
+                'name' => '笔记'
+            ],
+            'image'          => [
+                'type' => 2,
+                'name' => '图片'
+            ],
+            'audio'          => [
+                'type' => 2,
+                'name' => '音频',
+            ],
+            'video'          => [
+                'type' => 2,
+                'name' => '视频',
+            ],
+            'file'           => [
+                'type' => 2,
+                'name' => '文件'
+            ],
+            'link'           => [
+                'type' => 2,
+                'name' => '链接'
+            ],
+            'other'          => [
+                'type' => 2,
+                'name' => '其他'
+            ]
+        ];
+
+        if (array_key_exists($id, $groupNames)) {
+            $groupNames[$id]['id'] = $id;
+            return $groupNames[$id];
+        } else {
+            return Db::name('fastim_group')->where('id', $id)->find();
+        }
+    }
+
+    /**
+     * 获取分组列表
+     * @param int $uid 获取用户
+     * @param int $type 分组类型
+     * @return array       用户的分组
+     */
+    public static function getGroups($uid, $type)
+    {
+        if ($type == 0) {
+            // 好友分组
+            $default = [
+                [
+                    'id'   => 'common',
+                    'name' => '常用联系人'
+                ],
+                [
+                    'id'   => 'all_friends',
+                    'name' => '全部好友'
+                ]
+            ];
+        } elseif ($type == 1) {
+            // 群聊分组
+            $default = [
+                [
+                    'id'   => 'all_group',
+                    'name' => '全部群聊'
+                ]
+            ];
+        } elseif ($type == 2) {
+            // 收藏分组
+            $default = [
+                [
+                    'id'   => 'all_collection',
+                    'name' => '全部收藏'
+                ],
+                [
+                    'id'   => 'default',
+                    'name' => '笔记'
+                ],
+                [
+                    'id'   => 'image',
+                    'name' => '图片'
+                ],
+                [
+                    'id'   => 'audio',
+                    'name' => '音频'
+                ],
+                [
+                    'id'   => 'video',
+                    'name' => '视频'
+                ],
+                [
+                    'id'   => 'file',
+                    'name' => '文件'
+                ],
+                [
+                    'id'   => 'link',
+                    'name' => '链接'
+                ],
+                [
+                    'id'   => 'other',
+                    'name' => '其他'
+                ]
+            ];
+        }
+
+        $group = Db::name('fastim_group')->where('user_id', $uid)->where('type', $type)->order('weigh asc')->select();
+
+        $res = array_merge($default, $group);
+        return $res ? $res : [];
+    }
+
+    /**
+     * 获取最后一条未读消息
+     * @param int $getUserId 获取用户
+     * @param bool $initializeTime 初始化请求时间(用于标记只读取:用户已进入网站但未连接ws期间的消息)
+     * @return array|bool
+     */
+    public static function getUnreadMessage($getUserId, $initializeTime = false)
+    {
+        $message = Db::name('fastim_message')
+            ->alias('m')
+            ->field('m.*,gc.history_message,gu.last_read_id,gu.jointime')
+            ->join('fastim_group_chat gc', 'm.group_id=gc.id', 'LEFT')
+            ->join('fastim_group_user gu', "gu.group_id=gc.id AND gu.user_id='{$getUserId}'", 'LEFT')
+            ->where("m.group_id=0 AND m.recipient_id='{$getUserId}' AND m.status=0")
+            ->whereOr(function ($query) use ($getUserId) {
+                $query->where('gu.user_id', '>', 0)
+                    ->where('m.id>gu.last_read_id')
+                    ->where(function ($query) use ($getUserId) {
+                        $typeSql       = CommonCode::nonPrivilegedMessage(true);
+                        $systemTypeSql = CommonCode::nonPrivilegedMessage(true, ['system']);
+                        $query->where("gc.history_message=0 AND m.createtime>=gu.jointime AND m.type {$systemTypeSql} AND (m.recipient_id='{$getUserId}' OR m.recipient_id=0)");
+                        $query->whereOr("gc.history_message=1 AND (m.type {$typeSql} OR ((m.type='system' AND m.recipient_id='{$getUserId}') OR (m.recipient_id=0 AND m.createtime>=gu.jointime)))");
+                    })
+                    ->where('m.status', 0)
+                    ->where('m.sender_id', '<>', $getUserId);
+            })
+            ->where(function ($query) use ($initializeTime) {
+                if ($initializeTime) {
+                    $query->where('m.createtime', '>', $initializeTime);
+                }
+            })
+            ->order('m.createtime DESC')
+            ->find();
+
+        if (!$message) {
+            return false;
+        }
+
+        if ($message['session_id']) {
+            $sessionInfo = self::sessionInfo($message['session_id'], $getUserId);
+        } elseif ($message['group_id']) {
+            // 群系统消息无session_id
+            $sessionInfo = self::imSession('group', $getUserId, $message['group_id']);
+            $sessionInfo = self::sessionInfo($sessionInfo['id'], $getUserId);
+        }
+
+        $message = self::formatLastMessage($message, $getUserId, $sessionInfo['type']);
+        return [
+            'messageData' => $message,
+            'sessionInfo' => $sessionInfo,
+        ];
+    }
+
+    /**
+     * 获取未读消息数
+     * @param int $getUserId 获取用户
+     * @param array $sessionInfo 会话详情
+     * @param int $sessionId 会话ID
+     * @return int 未读消息数
+     */
+    public static function getUnreadMessagesNumber($getUserId, $sessionInfo = null, $sessionId = 0)
+    {
+        if (!$sessionInfo) {
+            $sessionInfo = self::sessionInfo($sessionId, $getUserId);
+        }
+
+        $number = 0;
+
+        if ($sessionInfo['type'] == 'single') {
+            $number = Db::name('fastim_message')
+                ->where('session_id', $sessionInfo['id'])
+                ->where('recipient_id', $getUserId)
+                ->where('status', 0)
+                ->count('id');
+        } elseif ($sessionInfo['type'] == 'group') {
+            $groupUserInfo = Db::name('fastim_group_user')
+                ->where('group_id', $sessionInfo['chat_id'])
+                ->where('user_id', $getUserId)
+                ->find();
+            $userJoinTime  = (isset($groupUserInfo['jointime']) && $groupUserInfo['jointime']) ? (int)$groupUserInfo['jointime'] : 0;
+
+            $historyMessage = 1;// 是否允许查看入群前的消息
+            if (isset($sessionInfo['sessionUser'])) {
+                $historyMessage = $sessionInfo['sessionUser']['history_message'];
+            } elseif (isset($sessionInfo['pushUser'])) {
+                $historyMessage = $sessionInfo['pushUser']['history_message'];
+            }
+
+            $number = Db::name('fastim_message')
+                ->where('group_id', $sessionInfo['chat_id'])
+                ->where(function ($query) use ($historyMessage, $userJoinTime, $getUserId) {
+                    $typeSql = CommonCode::nonPrivilegedMessage(true);
+                    if ($historyMessage == 0) {
+                        // 以下代码虽看起来重复判断了`createtime`,但没有逻辑问题,改动后部分情景下会计算错误
+                        $query->where("type {$typeSql} OR (type='system' AND (recipient_id={$getUserId} OR (recipient_id=0 AND createtime>={$userJoinTime})))")
+                            ->where('createtime', '>=', $userJoinTime);
+                    } else {
+                        // 群通知:只显示发送时间大于用户加入时间的或直接发送给当前用户的群系统消息
+                        $query->where("type {$typeSql} OR (type='system' AND (recipient_id={$getUserId} OR (recipient_id=0 AND createtime>={$userJoinTime})))");
+                    }
+                })
+                ->where('id', '>', $groupUserInfo['last_read_id'] ? $groupUserInfo['last_read_id'] : 0)
+                ->where('status', 0)
+                ->where('sender_id', '<>', $getUserId)
+                ->count('id');
+        } elseif ($sessionInfo['type'] == 'service') {
+
+            if ($sessionInfo['chat_id'] == 1) {
+                $number = Db::name('fastim_message')
+                    ->where('type', 'friend_apply')
+                    ->where('recipient_id', $getUserId)
+                    ->where('status', 0)
+                    ->count('id');
+            } elseif ($sessionInfo['chat_id'] == 2) {
+                $number = Db::name('fastim_message')
+                    ->whereIn('type', 'group_apply,group_notice,group_invitation')
+                    ->where('recipient_id', $getUserId)
+                    ->where('status', 0)
+                    ->count('id');
+            } elseif ($sessionInfo['chat_id'] == 3) {
+                $number = Db::name('fastim_message')
+                    ->where('session_id', $sessionInfo['id'])
+                    ->where('recipient_id', $getUserId)
+                    ->where('status', 0)
+                    ->count('id');
+            }
+        }
+
+        return ($number < 99) ? $number : '99+';
+    }
+
+    /**
+     * 获取一个会话的最后一条消息
+     * @param int $getUserId 获取用户
+     * @param array $sessionInfo 会话资料
+     * @param int $sessionId 会话ID
+     * @return array
+     */
+    public static function getLastMessage($getUserId = 0, $sessionInfo = null, $sessionId = 0)
+    {
+        if (!$sessionInfo) {
+            $sessionInfo = self::sessionInfo($sessionId, $getUserId);
+        }
+
+        $lastMessage = false;
+
+        if ($sessionInfo['type'] == 'single') {
+            $lastMessage = Db::name('fastim_message')
+                ->field('type, sender_id, message, createtime')
+                ->where('session_id', $sessionInfo['id'])
+                ->where('deleteuser IS NULL OR deleteuser!=:uid')
+                ->where(function ($query) use ($getUserId) {
+                    $query->where('recipient_id', $getUserId)->where('status', '<', 4);
+                    $query->whereOr('sender_id', $getUserId);
+                })
+                ->bind([
+                    'uid' => $getUserId
+                ])
+                ->order('createtime desc,id desc')
+                ->find();
+        } elseif ($sessionInfo['type'] == 'group') {
+            // 用户入群时间
+            $userJoinTime = Db::name('fastim_group_user')
+                ->where('group_id', $sessionInfo['chat_id'])
+                ->where('user_id', $getUserId)
+                ->value('jointime');
+            $userJoinTime = $userJoinTime ? $userJoinTime : 0;// 防null无法比较大小
+
+            $historyMessage = 1;// 是否允许查看入群前的消息
+            if (isset($sessionInfo['sessionUser'])) {
+                $historyMessage = $sessionInfo['sessionUser']['history_message'];
+            } elseif (isset($sessionInfo['pushUser'])) {
+                $historyMessage = $sessionInfo['pushUser']['history_message'];
+            }
+
+            $lastMessage = Db::name('fastim_message')
+                ->field('group_id, type, sender_id, message, createtime')
+                ->where('group_id', $sessionInfo['chat_id'])
+                ->where(function ($query) use ($getUserId) {
+                    $query->where('sender_id', $getUserId);
+                    $query->whereOr('status', '<', 4);
+                })
+                ->where(function ($query) use ($historyMessage, $userJoinTime, $getUserId) {
+
+                    $typeSql = CommonCode::nonPrivilegedMessage(true);
+                    if ($historyMessage == 0) {
+                        // 以下代码虽看起来重复判断了`createtime`,但没有逻辑问题,改动后部分情景下会计算错误
+                        $query->where("type {$typeSql} OR (type='system' AND (recipient_id={$getUserId} OR (recipient_id=0 AND createtime>={$userJoinTime})))")
+                            ->where('createtime', '>=', $userJoinTime);
+                    } else {
+                        // 群通知:只显示发送时间大于用户加入时间的或直接发送给当前用户的群系统消息
+                        $query->where("type {$typeSql} OR (type='system' AND (recipient_id={$getUserId} OR (recipient_id=0 AND createtime>={$userJoinTime})))");
+                    }
+                })
+                ->order('createtime desc,id desc')
+                ->find();
+
+            // 取得未读群通知
+            $unreadFixedMsg = Db::name('fastim_group_chat_notice')
+                ->alias('cn')
+                ->field('cn.*,r.id AS rid')
+                ->join('fastim_reading_log r', 'r.type=0 AND r.user_id=:user_id AND r.data_id=cn.id', 'LEFT')
+                ->where('cn.group_id', $sessionInfo['chat_id'])
+                ->where('cn.deletetime', null)
+                ->where('r.id', null)
+                ->bind([
+                    'user_id' => $getUserId
+                ])
+                ->count('*');
+            if ($unreadFixedMsg) {
+                $unreadFixedMsg = [
+                    'message' => '有新的公告',
+                    'count'   => $unreadFixedMsg
+                ];
+            } else {
+                // 取得未读@消息
+                $unreadFixedMsg = Db::name('fastim_reading_log')
+                    ->where('type', 2)
+                    ->where('group_id', $sessionInfo['chat_id'])
+                    ->where('user_id', $getUserId)
+                    ->where('receipt', 0)
+                    ->order('createtime asc')
+                    ->find();
+                if ($unreadFixedMsg) {
+                    $unreadFixedMsg = [
+                        'message' => '有人@你',
+                        'info'    => $unreadFixedMsg
+                    ];
+                }
+            }
+        } elseif ($sessionInfo['type'] == 'service') {
+            if ($sessionInfo['chat_id'] == 1) {
+                // 好友验证
+                $lastMessage = Db::name('fastim_message')
+                    ->field('type, sender_id, recipient_id, message, createtime')
+                    ->where('type', 'friend_apply')
+                    ->where('sender_id|recipient_id', $getUserId)
+                    ->order('createtime desc')
+                    ->find();
+            } elseif ($sessionInfo['chat_id'] == 2) {
+                // 群通知
+                $lastMessage = Db::name('fastim_message')
+                    ->whereIn('type', 'group_apply,group_notice,group_invitation')
+                    ->where('sender_id|recipient_id', $getUserId)
+                    ->order('createtime desc,id desc')
+                    ->find();
+            } elseif ($sessionInfo['chat_id'] == 3) {
+                // 客服消息
+                $lastMessage = Db::name('fastim_message')
+                    ->where('session_id', $sessionInfo['id'])
+                    ->where('sender_id|recipient_id', $getUserId)
+                    ->order('createtime desc,id desc')
+                    ->find();
+            }
+        }
+
+        if ($lastMessage) {
+            return [
+                'last_time'        => self::formatTimeAlong($lastMessage['createtime']),
+                'last_message'     => self::formatLastMessage($lastMessage, $getUserId, $sessionInfo['type']),
+                'unread_fixed_msg' => isset($unreadFixedMsg) ? $unreadFixedMsg : ''
+            ];
+        } else {
+            $bio = '';
+            if (isset($sessionInfo['sessionUser'])) {
+                $bio = $sessionInfo['sessionUser']['bio'];
+            } elseif (isset($sessionInfo['pushUser'])) {
+                $bio = $sessionInfo['pushUser']['bio'];
+            }
+
+            return [
+                'last_time'        => self::formatTimeAlong($sessionInfo['createtime']),
+                'last_message'     => $bio,
+                'unread_fixed_msg' => isset($unreadFixedMsg) ? $unreadFixedMsg : ''
+            ];
+        }
+    }
+
+    /**
+     * 格式化最后消息
+     * @param array $message 消息数据
+     * @param integer $getUserId 获取消息人,群通知和好友申请时传递
+     * @param string $type 会话类型,如果是群聊会话,则在消息前带上发信人昵称
+     * @return string
+     */
+    public static function formatLastMessage($message, $getUserId = 0, $type = 'single')
+    {
+        $types = [
+            'image' => '[图片]',
+            'audio' => '[音频]',
+            'video' => '[视频]',
+            'file'  => '[文件]',
+            'link'  => '[链接]',
+            'voice' => '[语音消息]'
+        ];
+        if ($message['type'] == 'default') {
+            // 匹配所有的img标签,去除所有其他标签
+            $message['message'] = htmlspecialchars_decode($message['message']);
+            $preg               = '/<img.*?title="(.+?)".*?>/is';
+            preg_match_all($preg, $message['message'], $result, PREG_PATTERN_ORDER);
+            if (isset($result[1][0])) {
+                foreach ($result[1] as $key => $value) {
+                    $result[1][$key] = "[{$value}]";
+                }
+                $message['message'] = strip_tags(str_replace($result[0], $result[1], $message['message']));
+            } else {
+                $preg = '/<img.*?src="(.+?)".*?>/is';
+                preg_match_all($preg, $message['message'], $result, PREG_PATTERN_ORDER);
+                $message['message'] = strip_tags(str_replace($result[0], '[图片]', $message['message']));
+            }
+
+            if ($type == 'group' && isset($message['sender_id'])) {
+                if (isset($message['group_id'])) {
+                    $groupUserNickname = Db::name('fastim_group_user')
+                        ->where('group_id', $message['group_id'])
+                        ->where('user_id', $message['sender_id'])
+                        ->value('nickname');
+                }
+                if (!isset($groupUserNickname) || !$groupUserNickname) {
+                    $senderInfo        = self::getImUserInfo($message['sender_id']);
+                    $groupUserNickname = $senderInfo['nickname'];
+                }
+                $message['message'] = $groupUserNickname . ':' . $message['message'];
+            }
+
+        } elseif (array_key_exists($message['type'], $types)) {
+            $message['message'] = $types[$message['type']];
+        } elseif ($message['type'] == 'system') {
+            // 系统消息
+            if (!is_array($message['message'])) {
+                $message['message'] = json_decode($message['message'], true);
+            }
+            $message['message'] = $message['message']['message'];
+        } elseif ($message['type'] == 'group_apply') {
+            // 入群申请
+            $info = self::getImGroupChat($message['group_id']);
+            if ($message['sender_id'] == $getUserId) {
+                $message['message'] = '申请加入群聊' . $info['nickname'];
+            } else {
+                $userInfo           = self::getImUserInfo($message['sender_id']);
+                $message['message'] = $userInfo['nickname'] . '申请加入群聊' . $info['nickname'];
+            }
+        } elseif ($message['type'] == 'group_notice') {
+            // 群通知
+            if (!is_array($message['message'])) {
+                $message['message'] = json_decode($message['message'], true);
+            }
+            $info = self::getImGroupChat($message['group_id']);
+
+            if ($message['message']['action'] == 'quit') {
+                $userInfo           = self::getImUserInfo($message['message']['quit_user']);
+                $message['message'] = $userInfo['nickname'] . '退出了群聊' . $info['nickname'];
+            } elseif ($message['message']['action'] == 'dismiss') {
+                $message['message'] = $message['message']['notice'] . $info['nickname'];
+            } elseif ($message['message']['action'] == 'refuse') {
+                $userInfo = self::getImUserInfo($message['message']['refuse_user']);
+
+                // getUserId 获取消息的用户:被邀请人、申请人、管理员、邀请人
+                // $message['message']['refuse_user'] 实际被拒绝的用户
+                // $message['sender_id'] 执行拒绝操作的用户
+                // $info['leader'] 群主
+
+                if ($getUserId == $message['message']['refuse_user']) {
+                    if ($message['sender_id'] == $info['leader']) {
+                        $message['message'] = '管理员拒绝你加入群聊' . $info['nickname'];
+                    } elseif ($message['sender_id'] == $getUserId) {
+                        $message['message'] = '你拒绝了加入群聊' . $info['nickname'];
+                    }
+                } elseif ($getUserId == $info['leader']) {
+                    if ($message['sender_id'] == $info['leader']) {
+                        $message['message'] = '你拒绝了' . $userInfo['nickname'] . '加入群聊' . $info['nickname'];
+                    } else {
+                        $message['message'] = $userInfo['nickname'] . '拒绝加入群聊' . $info['nickname'];
+                    }
+                } else {
+                    if ($message['sender_id'] == $info['leader']) {
+                        $message['message'] = '管理员拒绝了' . $userInfo['nickname'] . '加入群聊' . $info['nickname'];
+                    } else {
+                        $message['message'] = $userInfo['nickname'] . '拒绝加入群聊' . $info['nickname'];
+                    }
+                }
+            } elseif ($message['message']['action'] == 'propose') {
+                $message['message'] = '管理员已将你请出群聊' . $info['nickname'];
+            } else {
+                $message['message'] = $message['message']['notice'];
+            }
+        } elseif ($message['type'] == 'group_invitation') {
+            // 邀请好友入群
+            if (!is_array($message['message'])) {
+                $message['message'] = json_decode($message['message'], true);
+            }
+            $info           = self::getImGroupChat($message['group_id']);
+            $invitationUser = self::getImUserInfo($message['message']['invitation_user']);
+            if ($message['message']['identity'] == 'admin') {
+                $invitedUser        = self::getImUserInfo($message['message']['invited_user']);
+                $message['message'] = $invitationUser['nickname'] . '邀请' . $invitedUser['nickname'] . '加入群聊' . $info['nickname'];
+            } else {
+                $message['message'] = $invitationUser['nickname'] . '邀请你加入群聊' . $info['nickname'];
+            }
+        } elseif ($message['type'] == 'friend_apply') {
+            // 好友申请
+            if (!is_array($message['message'])) {
+                $message['message'] = json_decode($message['message'], true);
+            }
+            if ($message['sender_id'] == $getUserId) {
+                $info = self::getImUserInfo($message['recipient_id']);
+                if ($message['message']['method'] == 'notice') {
+                    if ($message['message']['result'] == 'refuse') {
+                        $message['message'] = '你已经拒绝了' . $info['nickname'] . '的好友申请。';
+                    } else {
+                        $message['message'] = $message['message']['msg'];
+                    }
+                } elseif ($message['message']['method'] == 'apply') {
+                    $message['message'] = '申请添加' . $info['nickname'] . '为好友';
+                }
+            } else {
+                $info = self::getImUserInfo($message['sender_id']);
+                if ($message['message']['method'] == 'notice') {
+                    if ($message['message']['result'] == 'refuse') {
+                        $message['message'] = $info['nickname'] . '拒绝了你的好友申请。';
+                    } else {
+                        $message['message'] = $message['message']['msg'];
+                    }
+                } elseif ($message['message']['method'] == 'apply') {
+                    $message['message'] = $info['nickname'] . '申请添加你为好友';
+                }
+            }
+        } elseif ($message['type'] == 'kbs_list') {
+            if (!is_array($message['message'])) {
+                $message['message'] = json_decode($message['message'], true);
+            }
+            return $message['message']['title'];
+        } elseif ($message['type'] == 'group_chat_notice') {
+            // 群公告
+            if (!is_array($message['message'])) {
+                $message['message'] = json_decode($message['message'], true);
+            }
+            $userInfo           = self::getImUserInfo($message['sender_id']);
+            $message['message'] = $userInfo['nickname'] . ':' . $message['message']['content'];
+        }
+        return $message['message'];
+    }
+
+    /**
+     * 格式化会话时间-按天顺时针格式化
+     * @param int time 时间戳
+     * @return string
+     */
+    public static function formatTimeAlong($time = null)
+    {
+        if (!$time) {
+            return date('H:i');
+        }
+
+        $nowDate  = getdate(time());
+        $timeDate = getdate($time);
+
+        if (($nowDate['year'] === $timeDate['year']) && ($nowDate['yday'] === $timeDate['yday'])) {
+            return date('H:i', $time);
+        } else {
+            return self::formatTime($time);
+        }
+    }
+
+    /**
+     * 格式化时间-按时间差逆时针格式化
+     * @param int time 时间戳
+     * @return string
+     */
+    public static function formatTime($time = null)
+    {
+        $nowTime = time();
+        $time    = ($time === null || $time > $nowTime || $time == $nowTime) ? $nowTime - 1 : intval($time);
+        $lang    = [
+            '%d second%s ago' => '%d秒前',
+            '%d minute%s ago' => '%d分钟前',
+            '%d hour%s ago'   => '%d小时前',
+            '%d day%s ago'    => '%d天前',
+            '%d week%s ago'   => '%d周前',
+            '%d month%s ago'  => '%d月前',
+            '%d year%s ago'   => '%d年前',
+        ];
+        \think\Lang::set($lang);
+        $date = \fast\Date::human($time);
+        return $date;
+    }
+
+    /**
+     * 群聊屏蔽状态
+     * @param  [type] $id  群聊
+     * @param  [type] $uid 用户
+     * @return [type]      屏蔽状态
+     */
+    public static function getGroupChatShield($id, $uid)
+    {
+        $shield = Db::name('fastim_group_user')
+            ->where('group_id', $id)
+            ->where('user_id', $uid)
+            ->value('block_messages');
+        return $shield ? true : false;
+    }
+
+    /**
+     * 获得用户资料
+     * @param int $id 用户ID
+     * @param bool $detail 获得详细的资料
+     * @param int $getUserId 获取人ID(用于验证权限)
+     * @return array|bool
+     */
+    public static function getImUserInfo($id, $detail = false, $getUserId = false)
+    {
+
+        if ($detail) {
+            $info = Db::name('fastim_user')
+                ->alias('u')
+                ->field('u.id,u.type,u.avatar,u.nickname,u.mobile,u.email,u.age,u.birthday,u.occupation,u.company,u.bio,u.gender,u.status,u.welcome_msg,fu.avatar as fu_avatar,fu.nickname as fu_nickname,fu.bio as fu_bio,fu.gender as fu_gender,fu.email as fu_email,fu.mobile as fu_mobile,fu.birthday as fu_birthday,a.avatar as a_avatar,a.nickname as a_nickname,a.email as a_email')
+                ->join('user fu', 'u.user_id=fu.id', 'LEFT')
+                ->join('admin a', 'u.admin_id=a.id', 'LEFT')
+                ->where('u.id', $id)
+                ->find();
+        } else {
+            $info = Db::name('fastim_user')
+                ->alias('u')
+                ->field('u.id,u.type,u.avatar,u.nickname,u.status,u.bio,u.gender,u.welcome_msg,fu.avatar as fu_avatar,fu.nickname as fu_nickname,fu.bio as fu_bio,fu.gender as fu_gender,a.avatar as a_avatar,a.nickname as a_nickname')
+                ->join('user fu', 'u.user_id=fu.id', 'LEFT')
+                ->join('admin a', 'u.admin_id=a.id', 'LEFT')
+                ->where('u.id', $id)
+                ->find();
+        }
+
+        if ($info) {
+
+            if ($getUserId) {
+                $friends = Db::name('fastim_friendship')->where('user_id', $getUserId)->where('friend_id', $id)->find();
+                if ($friends) {
+                    if ($friends['top_contacts']) {
+                        $info['group'] = 'common';
+                    } elseif ($friends['group'] == 0) {
+                        $info['group'] = 'all_friends';
+                    } else {
+                        $info['group'] = $friends['group'];
+                    }
+                }
+            }
+            $info['friend'] = ((isset($friends) && $friends) || $id == $getUserId) ? true : false;
+
+            if ($detail) {
+                $info['email']    = self::trueAttr([
+                    $info['email'],
+                    $info['fu_email'],
+                    $info['a_email']
+                ]);
+                $info['mobile']   = self::trueAttr([
+                    $info['mobile'],
+                    $info['fu_mobile'],
+                    ''
+                ]);
+                $info['birthday'] = self::trueAttr([
+                    $info['birthday'],
+                    $info['fu_birthday'],
+                    ''
+                ]);
+                unset($info['fu_email'], $info['a_email'], $info['fu_mobile'], $info['fu_birthday']);
+                // 资料隐私性检查
+                if ($getUserId != $id) {
+                    $userConfig = self::imUserConfig($id);
+                    $friendship = Db::name('fastim_friendship')
+                        ->where('user_id', $id)
+                        ->where('friend_id', $getUserId)
+                        ->find();
+
+                    // 隐私性设置:0=全部,1=好友,2=仅自己
+                    $privacy = [
+                        'mobile_privacy'     => 'mobile',
+                        'email_privacy'      => 'email',
+                        'occupation_privacy' => 'occupation',
+                        'company_privacy'    => 'company'
+                    ];
+                    foreach ($privacy as $key => $value) {
+                        if (($userConfig[$key] == 1 && !$friendship) || $userConfig[$key] == 2) {
+                            $info[$value] = '';
+                        }
+                    }
+                    if (($userConfig['age_privacy'] == 1 && !$friendship) || $userConfig['age_privacy'] == 2) {
+                        $info['age'] = $info['birthday'] = '';
+                    }
+                }
+            }
+            $info['remark']          = $friends['remark'] ?? '';
+            $info['nickname_origin'] = self::trueAttr(self::nicknameSort([
+                $info['nickname'],
+                $info['fu_nickname'],
+                $info['a_nickname']
+            ], $info['id']));
+            $info['nickname']        = self::trueAttr(self::nicknameSort([
+                $info['remark'],
+                $info['nickname_origin']
+            ], $info['id']));
+            $info['avatar']          = self::avatarSrc([
+                $info['avatar'],
+                $info['fu_avatar'],
+                $info['a_avatar']
+            ], $info['nickname']);
+            $info['bio']             = self::trueAttr([
+                $info['bio'],
+                $info['fu_bio'],
+                '这家伙很懒,什么也没写!'
+            ]);
+            $info['status']          = self::imUserStatus($info['status'], ($id == $getUserId ? false : true));
+            $info['gender']          = self::trueAttr([
+                $info['gender'],
+                $info['fu_gender'],
+                '0'
+            ]);
+            $info['gender']          = self::imUserGender($info['gender']);
+            unset($info['fu_avatar'], $info['a_avatar'], $info['fu_nickname'], $info['a_nickname'], $info['fu_bio'], $info['fu_gender']);
+            return $info;
+        }
+
+        return false;
+    }
+
+    /**
+     * IM用户状态转义
+     * @param int $status 数字状态
+     * @param int $desensitization 脱敏
+     * @return array
+     */
+    public static function imUserStatus($status, $desensitization = true)
+    {
+        $statusArr = [
+            0 => '离线',
+            1 => '在线',
+            2 => '忙碌',
+            3 => '隐身'
+        ];
+
+        if ($desensitization) {
+            $status       = ($status == 3) ? 0 : $status;
+            $statusArr[3] = '离线';
+        }
+
+        return [
+            'value'   => $status,
+            'chinese' => $statusArr[$status] ?? '未知'
+        ];
+    }
+
+    /**
+     * IM用户性别转义
+     * @param int $gender 数字性别
+     * @return string
+     */
+    public static function imUserGender($gender)
+    {
+        $genderArr = [
+            'secrecy' => [
+                'chinese' => '保密',
+                'value'   => 'secrecy'
+            ],
+            'male'    => [
+                'chinese' => '男',
+                'value'   => 'male'
+            ],
+            'female'  => [
+                'chinese' => '女',
+                'value'   => 'female'
+            ]
+        ];
+
+        return $genderArr[$gender] ?? $genderArr[0];
+    }
+
+    /**
+     * 获取群聊资料
+     * @param int $id 群聊ID
+     * @return array|bool
+     */
+    public static function getImGroupChat($id)
+    {
+        $info = Db::name('fastim_group_chat')->where('id', $id)->find();
+
+        if (!$info) {
+            return false;
+        }
+
+        $info['user_count'] = Db::name('fastim_group_user')->where('group_id', $id)->count('user_id');
+
+        $info['avatar'] = self::avatarSrc([
+            $info['avatar'],
+        ], $info['nickname']);
+
+        $info['bio'] = self::trueAttr([
+            $info['bio'],
+            '群主很懒,什么也没写~'
+        ]);
+        return $info;
+    }
+
+    /**
+     * 获取会话详情
+     * @param int $sessionId 会话ID
+     * @param int $uid 传递此参数则同时获取会话的用户信息
+     * @return array|bool
+     */
+    public static function sessionInfo($sessionId, $uid = null)
+    {
+        $imSession = Db::name('fastim_session')->where('id', $sessionId)->find();
+
+        if (!$imSession) {
+            return false;
+        } elseif (!$uid) {
+            return $imSession;
+        }
+
+        if ($imSession['type'] == 'single') {
+            $imSession['sessionUser'] = self::getImUserInfo(($imSession['user_one'] == $uid) ? $imSession['user_two'] : $imSession['user_one'], false, $uid);
+        } elseif ($imSession['type'] == 'group') {
+            $imSession['sessionUser'] = self::getImGroupChat($imSession['chat_id']);
+        } elseif ($imSession['type'] == 'service') {
+            $imSession['sessionUser'] = Db::name('fastim_service')->where('id', $imSession['chat_id'])->find();
+        }
+        $imSession['user'] = self::getImUserInfo($uid);
+        if (!$imSession['sessionUser'] || !$imSession['user']) {
+            return false;
+        }
+        $imSession['sessionUser']['avatar'] = self::avatarSrc($imSession['sessionUser']['avatar'], $imSession['sessionUser']['nickname']);
+
+        return $imSession;
+    }
+
+    /**
+     * 获得用户配置
+     * @param int $uid 用户
+     * @param string $name 配置项
+     * @return array
+     */
+    public static function imUserConfig($uid, $name = null)
+    {
+        if ($name) {
+            $imDefaultConfig = Db::name('fastim_config')
+                ->where('user_settings', 1)
+                ->where('name', $name)
+                ->value('value');
+            $userConfig      = Db::name('fastim_user_config')
+                ->where('user_id', $uid)
+                ->where('name', $name)
+                ->value('value');
+            return ($userConfig !== null) ? $userConfig : $imDefaultConfig;
+        } else {
+            $imDefaultConfig = Db::name('fastim_config')->where('user_settings', 1)->column('name,value');
+            $userConfig      = Db::name('fastim_user_config')->where('user_id', $uid)->column('name,value');
+            return array_merge($imDefaultConfig, $userConfig);
+        }
+    }
+
+    /**
+     * 绑定IM用户与FA管理员或FA用户的关系
+     * @return array|bool imUser完整资料
+     */
+    public static function bindFaUserToIMUser($imUser, $userInfo)
+    {
+        if (!$imUser) {
+            return false;
+        }
+
+        if ($userInfo) {
+            if ($userInfo['identity'] == 'admin' && $imUser['admin_id'] == 0) {
+                Db::name('fastim_user')->where('id', $imUser['id'])->update([
+                    'type'     => 'user',
+                    'admin_id' => $userInfo['id']
+                ]);
+                $imUser['type']     = 'user';
+                $imUser['admin_id'] = $userInfo['id'];
+            } elseif ($userInfo['identity'] == 'fauser' && $imUser['user_id'] == 0) {
+                Db::name('fastim_user')->where('id', $imUser['id'])->update([
+                    'type'    => 'user',
+                    'user_id' => $userInfo['id']
+                ]);
+                $imUser['type']    = 'user';
+                $imUser['user_id'] = $userInfo['id'];
+            }
+        }
+
+        $imUser['nickname'] = self::trueAttr(self::nicknameSort([
+            $imUser['nickname'],
+            $userInfo['nickname'] ?? ''
+        ], $imUser['id']));
+
+        $imUser['avatar'] = self::avatarSrc([
+            $imUser['avatar'],
+            $userInfo['avatar'] ?? ''
+        ], $imUser['nickname']);
+
+        $imUser['identity'] = $userInfo['identity'] ?? $imUser['identity'];
+        return $imUser;
+    }
+
+    /**
+     * 创建一个Im用户
+     * @return array|bool 用户资料
+     */
+    public static function createIMUser($referrer = '', $loginip = '')
+    {
+        $imUserId = Db::name('fastim_user')->max('id') + 1;
+        $token    = \fast\Random::uuid();
+
+        $imUser = [
+            'avatar'     => '',
+            'nickname'   => CommonCode::buildTouristNickname($imUserId),
+            'referrer'   => $referrer,
+            'token'      => $token,
+            'loginip'    => $loginip,
+            'createtime' => time(),
+        ];
+        if (Db::name('fastim_user')->insert($imUser)) {
+
+            $newImUserId = Db::name('fastim_user')->getLastInsID();
+            $keeptime    = 864000;
+            $expiretime  = time() + $keeptime;
+            $key         = md5(md5($newImUserId) . md5($keeptime) . md5($expiretime) . $token);
+            $userToken   = [$newImUserId, $keeptime, $expiretime, $key];
+
+            $imUser                = Db::name('fastim_user')
+                ->field('id,type,user_id,admin_id,avatar,nickname,email,status')
+                ->where('id', $newImUserId)
+                ->find();
+            $imUser['cache_token'] = implode('|', $userToken);// 缓存到客户端的token
+            $imUser['identity']    = 'imuser';
+
+            // 防止数据库id不对应时的昵称错误
+            if ((CommonCode::$imTouristNicknamePrefix . $imUserId != CommonCode::$imTouristNicknamePrefix . $newImUserId) && ($imUser['nickname'] == CommonCode::$imTouristNicknamePrefix . $imUserId)) {
+                Db::name('fastim_user')->where('id', $newImUserId)->update([
+                    'nickname' => CommonCode::$imTouristNicknamePrefix . $newImUserId
+                ]);
+                $imUser['nickname'] = CommonCode::$imTouristNicknamePrefix . $newImUserId;
+            }
+            return $imUser;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 检查为一个Im用户
+     * @param string token
+     * @param string userInfo 已登录的用户或管理员资料
+     * @return array|bool 用户资料
+     */
+    public static function checkIMUser($token = false, $userInfo = false)
+    {
+        // 通过绑定的用户获得imUser
+        if ($userInfo) {
+            if ($userInfo['identity'] == 'admin') {
+                $imUser = Db::name('fastim_user')
+                    ->alias('u')
+                    ->field('u.id,u.type,u.user_id,u.admin_id,u.avatar,u.nickname,u.email,u.status,a.avatar as a_avatar,a.nickname as a_nickname,a.email as a_email')
+                    ->join('admin a', 'u.admin_id=a.id', 'LEFT')
+                    ->where('u.admin_id', $userInfo['id'])
+                    ->find();
+
+                if ($imUser) {
+
+                    $imUser['nickname'] = self::trueAttr(self::nicknameSort([
+                        $imUser['nickname'],
+                        $imUser['a_nickname']
+                    ], $imUser['id']));
+
+                    $imUser['avatar'] = self::avatarSrc([
+                        $imUser['avatar'],
+                        $imUser['a_avatar']
+                    ], $imUser['nickname']);
+
+                    $imUser['email'] = self::trueAttr([
+                        $imUser['email'],
+                        $imUser['a_email']
+                    ]);
+
+                    unset($imUser['a_avatar'], $imUser['a_nickname'], $imUser['a_email']);
+                }
+            } elseif ($userInfo['identity'] == 'fauser') {
+                $imUser = Db::name('fastim_user')
+                    ->alias('u')
+                    ->field('u.id,u.type,u.user_id,u.admin_id,u.avatar,u.nickname,u.email,u.status,fu.avatar as fu_avatar,fu.nickname as fu_nickname,fu.email as fu_email')
+                    ->join('user fu', 'u.user_id=fu.id', 'LEFT')
+                    ->where('u.user_id', $userInfo['id'])
+                    ->find();
+
+                if ($imUser) {
+                    $imUser['nickname'] = self::trueAttr(self::nicknameSort([
+                        $imUser['nickname'],
+                        $imUser['fu_nickname']
+                    ], $imUser['id']));
+                    $imUser['avatar']   = self::avatarSrc([
+                        $imUser['avatar'],
+                        $imUser['fu_avatar']
+                    ], $imUser['nickname']);
+                    $imUser['email']    = self::trueAttr([
+                        $imUser['email'],
+                        $imUser['fu_email']
+                    ]);
+
+                    unset($imUser['fu_avatar'], $imUser['fu_nickname'], $imUser['fu_email']);
+                }
+            }
+        }
+
+        // 通过token获得imUser
+        if ($token && (!isset($imUser) || !$imUser)) {
+            // ios 网络传输特殊符号兼容
+            if (strstr($token, '~') !== false) {
+                $token = str_replace('~', '|', $token);
+            }
+
+            if (strstr($token, '|') === false) {
+                return false;
+            }
+
+            [$id, $keeptime, $expiretime, $key] = explode('|', $token);
+            if ($id && $keeptime && $expiretime && $key && $expiretime > time()) {
+                $imUser = Db::name('fastim_user')
+                    ->field('id,type,user_id,admin_id,avatar,nickname,email,token,status')
+                    ->where('id', $id)
+                    ->find();
+                if (!$imUser || !$imUser['token']) {
+                    return false;
+                }
+
+                // token有变更
+                if ($key != md5(md5($id) . md5($keeptime) . md5($expiretime) . $imUser['token'])) {
+                    return false;
+                }
+
+                $relationUser = false;
+                if ($imUser['user_id'] > 0) {
+                    $relationUser = Db::name('user')
+                        ->field('avatar,nickname,email')
+                        ->where('id', $imUser['user_id'])
+                        ->where('status', 'normal')
+                        ->find();
+                } elseif ($imUser['admin_id'] > 0) {
+                    $relationUser = Db::name('admin')
+                        ->field('avatar,nickname,email')
+                        ->where('id', $imUser['admin_id'])
+                        ->where('status', 'normal')
+                        ->find();
+                }
+
+                if ($relationUser) {
+                    $imUser['nickname'] = self::trueAttr(self::nicknameSort([
+                        $imUser['nickname'],
+                        $relationUser['nickname']
+                    ], $imUser['id']));
+                    $imUser['avatar']   = self::avatarSrc([
+                        $imUser['avatar'],
+                        $relationUser['avatar']
+                    ], $imUser['nickname']);
+
+                    $imUser['email'] = self::trueAttr([
+                        $imUser['email'],
+                        $relationUser['email']
+                    ]);
+                }
+            }
+        }
+
+        if (!isset($imUser)) {
+            return false;
+        }
+
+        $imUser['identity'] = 'imuser';
+        unset($imUser['token']);
+        return $imUser;
+    }
+
+    /**
+     * 生成鉴权token
+     * 一个额外于游客身份token的授权token
+     * uniapp编译的各端可以使用此 token 被鉴别为非游客用户
+     * @param string $token
+     */
+    public static function createAuthToken($uid)
+    {
+        $authToken = Db::name('fastim_user')->where('id', $uid)->value('auth_token');
+        if (!$authToken) {
+            $authToken = md5(\fast\Random::uuid()); // md5后短一点
+            Db::name('fastim_user')->where('id', $uid)->update([
+                'auth_token' => $authToken
+            ]);
+        }
+
+        $keeptime   = 864000;// token 10天后过期
+        $expiretime = time() + $keeptime;
+        $authKey    = md5(md5($uid) . md5($keeptime) . md5($expiretime) . $authToken);
+        $authToken  = [$uid, $keeptime, $expiretime, $authKey];
+        return implode('|', $authToken);
+    }
+
+    /**
+     * 检查鉴权token
+     * @param string $authToken 鉴权token
+     * @return bool
+     */
+    public static function checkAuthToken($authToken)
+    {
+        [$id, $keeptime, $expiretime, $key] = explode('|', $authToken);
+        if ($id && $keeptime && $expiretime && $key && $expiretime > time()) {
+            $userAuthToken = Db::name('fastim_user')->where('id', $id)->value('auth_token');
+            if (!$userAuthToken) {
+                return false;
+            }
+
+            // token有变更
+            if ($key == md5(md5($id) . md5($keeptime) . md5($expiretime) . $userAuthToken)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * 检查管理员身份
+     * @param string adminToken 管理员cookie
+     * @param string adminId    管理员ID直接通过检查
+     * @return array|bool
+     */
+    public static function checkAdmin($adminToken, $adminId = 0)
+    {
+        $readField = 'id,nickname,avatar,email,token'; // 要读取的字段
+
+        if ($adminId) {
+            $admin = Db::name('admin')->field($readField)->where('id', $adminId)->where('status', 'normal')->find();
+        } else {
+            [$id, $keeptime, $expiretime, $key] = explode('|', $adminToken);
+
+            if ($id && $keeptime && $expiretime && $key && $expiretime > time()) {
+
+                $admin = Db::name('admin')->field($readField)->where('id', $id)->where('status', 'normal')->find();
+
+                if (!$admin || !$admin['token']) {
+                    return false;
+                }
+
+                // 检查token是否有变更
+                $sign = $id . CommonCode::$imAuthSign;
+                if ($key != md5(md5($sign) . md5($keeptime) . md5($expiretime) . $admin['token'])) {
+                    return false;
+                }
+
+            } else {
+                return false;
+            }
+        }
+
+        if (!$admin) {
+            return false;
+        }
+
+        $admin['identity'] = 'admin';
+        return $admin;
+    }
+
+    /**
+     * 检查FastAdmin用户token
+     * @param string token 用户的token
+     * @param int id 用户ID直接登录
+     * @return array|bool Fa用户信息
+     */
+    public static function checkFaUser($token, $id = 0)
+    {
+        $user = false;
+
+        $readField = 'id,nickname,avatar,email,mobile';
+
+        if ($id) {
+            $user = Db::name('user')->field($readField)->where('id', $id)->where('status', 'normal')->find();
+        } else {
+            $userId = false;
+            if (!config('cookie.httponly')) {
+                $userId = Db::name('user_token')->where('token', self::getEncryptedToken($token))->value('user_id');
+            } else {
+                [$id, $key] = explode('|', $token);
+                $userTokenList = Db::name('user_token')
+                    ->where('user_id', $id)
+                    ->where('expiretime', '>', time())
+                    ->select();
+                foreach ($userTokenList as $userToken) {
+                    $sign    = $userToken['token'] . CommonCode::$imAuthSign;
+                    $userKey = md5(md5($id) . md5($sign));
+                    if ($userKey == $key) {
+                        $userId = $id;
+                        break;
+                    }
+                }
+            }
+
+            if ($userId) {
+                $user = Db::name('user')->field($readField)->where('id', $userId)->where('status', 'normal')->find();
+            }
+        }
+
+        if (!$user) {
+            return false;
+        }
+
+        $user['identity'] = 'fauser';
+        return $user;
+    }
+
+    /**
+     * 属性多选一
+     * @param array $attr 数组-按顺序多选一
+     */
+    public static function trueAttr($attr)
+    {
+        if ($attr) {
+            if (is_array($attr)) {
+                foreach ($attr as $key => $value) {
+                    if ($value) {
+                        return $value;
+                    }
+                }
+                return $attr[count($attr) - 1];// 返回数组中的最后一个值
+            } else {
+                return $attr;
+            }
+        } else {
+            return $attr;
+        }
+    }
+
+    /**
+     * 对昵称数组进行排序
+     * 默认昵称排到最后
+     * @return array
+     */
+    public static function nicknameSort($nickname, $uid)
+    {
+        if (is_array($nickname)) {
+            $defaultNickname    = CommonCode::buildTouristNickname($uid);
+            $defaultNicknameKey = array_search($defaultNickname, $nickname);
+            if ($defaultNicknameKey !== false) {
+                unset($nickname[$defaultNicknameKey]);// 删除
+                $nickname   = array_values($nickname);// 数组重排
+                $nickname[] = $defaultNickname;// 添加到最后
+            }
+            return $nickname;
+        } else {
+            return $nickname;
+        }
+    }
+
+    /**
+     * 用户头像url
+     * @param array|bool $avatar 原头像url数组
+     * @param string $nickname 用户昵称
+     */
+    public static function avatarSrc($avatar, $nickname)
+    {
+        $avatar = self::trueAttr($avatar);
+        if ($avatar) {
+            return $avatar;
+        }
+
+        $nickname = mb_strtoupper(mb_substr($nickname, 0, 1));
+        switch ($nickname) {
+            case '游':
+                $bg = "rgb(99,136,251)";
+                break;
+            case '新':
+                $bg = "rgb(243,89,168)";
+                break;
+            case '群':
+                $bg = "rgb(255,175,81)";
+                break;
+
+            default:
+                $bg = false;// 随机颜色
+                break;
+        }
+        return self::letterAvatar($nickname, $bg);
+    }
+
+    /*
+     * 检查和过滤变量
+     */
+    public static function checkVariable(&$variable)
+    {
+        $variable = self::removeXss($variable);
+        $variable = htmlspecialchars($variable);
+        $variable = stripslashes($variable); // 删除反斜杠
+        $variable = addslashes($variable); // 转义特殊符号
+        $variable = trim($variable); // 去除字符两边的空格
+    }
+
+    public static function removeXss($val)
+    {
+        if (function_exists('xss_clean')) {
+            return xss_clean($val);
+        }
+        $val = preg_replace('/([\x00-\x08,\x0b-\x0c,\x0e-\x19])/', '', $val);
+
+        $search = 'abcdefghijklmnopqrstuvwxyz';
+        $search .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+        $search .= '1234567890!@#$%^&*()';
+        $search .= '~`";:?+/={}[]-_|\'\\';
+        for ($i = 0; $i < strlen($search); $i++) {
+            $val = preg_replace('/(&#[xX]0{0,8}' . dechex(ord($search[$i])) . ';?)/i', $search[$i], $val); // with a ;
+            $val = preg_replace('/(&#0{0,8}' . ord($search[$i]) . ';?)/', $search[$i], $val); // with a ;
+        }
+
+        $ra1   = [
+            'javascript',
+            'vbscript',
+            'expression',
+            'applet',
+            'meta',
+            'xml',
+            'blink',
+            'link',
+            'style',
+            'script',
+            'embed',
+            'object',
+            'iframe',
+            'frame',
+            'frameset',
+            'ilayer',
+            'layer',
+            'bgsound',
+            'title',
+            'base'
+        ];
+        $ra2   = [
+            'onabort',
+            'onactivate',
+            'onafterprint',
+            'onafterupdate',
+            'onbeforeactivate',
+            'onbeforecopy',
+            'onbeforecut',
+            'onbeforedeactivate',
+            'onbeforeeditfocus',
+            'onbeforepaste',
+            'onbeforeprint',
+            'onbeforeunload',
+            'onbeforeupdate',
+            'onblur',
+            'onbounce',
+            'oncellchange',
+            'onchange',
+            'onclick',
+            'oncontextmenu',
+            'oncontrolselect',
+            'oncopy',
+            'oncut',
+            'ondataavailable',
+            'ondatasetchanged',
+            'ondatasetcomplete',
+            'ondblclick',
+            'ondeactivate',
+            'ondrag',
+            'ondragend',
+            'ondragenter',
+            'ondragleave',
+            'ondragover',
+            'ondragstart',
+            'ondrop',
+            'onerror',
+            'onerrorupdate',
+            'onfilterchange',
+            'onfinish',
+            'onfocus',
+            'onfocusin',
+            'onfocusout',
+            'onhelp',
+            'onkeydown',
+            'onkeypress',
+            'onkeyup',
+            'onlayoutcomplete',
+            'onload',
+            'onlosecapture',
+            'onmousedown',
+            'onmouseenter',
+            'onmouseleave',
+            'onmousemove',
+            'onmouseout',
+            'onmouseover',
+            'onmouseup',
+            'onmousewheel',
+            'onmove',
+            'onmoveend',
+            'onmovestart',
+            'onpaste',
+            'onpropertychange',
+            'onreadystatechange',
+            'onreset',
+            'onresize',
+            'onresizeend',
+            'onresizestart',
+            'onrowenter',
+            'onrowexit',
+            'onrowsdelete',
+            'onrowsinserted',
+            'onscroll',
+            'onselect',
+            'onselectionchange',
+            'onselectstart',
+            'onstart',
+            'onstop',
+            'onsubmit',
+            'onunload'
+        ];
+        $ra    = array_merge($ra1, $ra2);
+        $found = true;
+        while ($found == true) {
+
+            $val_before = $val;
+            for ($i = 0; $i < sizeof($ra); $i++) {
+                $pattern = '/';
+                for ($j = 0; $j < strlen($ra[$i]); $j++) {
+                    if ($j > 0) {
+                        $pattern .= '(';
+                        $pattern .= '(&#[xX]0{0,8}([9ab]);)';
+                        $pattern .= '|';
+                        $pattern .= '|(&#0{0,8}([9|10|13]);)';
+                        $pattern .= ')*';
+                    }
+                    $pattern .= $ra[$i][$j];
+                }
+                $pattern     .= '/i';
+                $replacement = substr($ra[$i], 0, 2) . '<k>' . substr($ra[$i], 2);
+                $val         = preg_replace($pattern, $replacement, $val);
+                if ($val_before == $val) {
+                    $found = false;
+                }
+            }
+        }
+        return $val;
+    }
+
+    /**
+     * 用户token加密
+     * @param string $token 待加密的token
+     */
+    public static function getEncryptedToken($token)
+    {
+        $tokenConfig = \think\Config::get('token');
+
+        $config = [
+            // 缓存前缀
+            'key'      => $tokenConfig['key'],
+            // 加密方式
+            'hashalgo' => $tokenConfig['hashalgo'],
+        ];
+
+        return hash_hmac($config['hashalgo'], $token, $config['key']);
+    }
+
+    /**
+     * 消息数据入库
+     * @param array $data 消息数据
+     * @return array|bool
+     */
+    public static function insertMessage($data)
+    {
+        $originalMessage = $data['message'];
+        if (is_array($data['message'])) {
+            $data['message'] = json_encode($data['message']);
+        }
+
+        $data['createtime'] = time();
+
+        if (Db::name('fastim_message')->insert($data)) {
+            $data['id'] = Db::name('fastim_message')->getLastInsID();
+
+            // 发送给前台的消息数据
+            if (is_array($originalMessage)) {
+                $data['message'] = $originalMessage;
+            } else {
+                $data['message'] = htmlspecialchars_decode($originalMessage);
+            }
+            return $data;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 富文本图片Url处理
+     * 此方法请勿在ws代码中使用
+     * @param string $html
+     * @return string 处理好图片Url的html
+     */
+    public static function htmlImgUrlHandle($html)
+    {
+        if ($html) {
+            // 匹配出img标签
+            $preg = '/<img.*?src="(.+?)".*?>/is';
+            preg_match_all($preg, $html, $result, PREG_PATTERN_ORDER);
+
+            if ($result[1]) {
+                $fullImgUrl = [];
+                foreach ($result[1] as $key => $value) {
+                    $fullImgUrl[$key] = cdnurl($value, true);
+                }
+                $html = str_replace($result[1], $fullImgUrl, $html);
+            }
+        }
+        return $html;
+    }
+
+    /**
+     * 生成首字头像
+     * @param string $text 文字
+     * @return string svg
+     */
+    public static function letterAvatar($text, $bg = false)
+    {
+        if (!$bg) {
+            $total = unpack('L', hash('adler32', $text, true))[1];
+            $hue   = $total % 360;
+            [$r, $g, $b] = hsv2rgb($hue / 360, 0.3, 0.9);
+            $bg = "rgb({$r},{$g},{$b})";
+        }
+
+        $color = "#ffffff";
+        $first = mb_strtoupper(mb_substr($text, 0, 1));
+        $src   = base64_encode('<svg xmlns="http://www.w3.org/2000/svg" version="1.1" height="100" width="100"><rect fill="' . $bg . '" x="0" y="0" width="100" height="100"></rect><text x="50" y="50" font-size="50" text-copy="fast" fill="' . $color . '" text-anchor="middle" text-rights="admin" dominant-baseline="central">' . $first . '</text></svg>');
+        $value = 'data:image/svg+xml;base64,' . $src;
+        return $value;
+    }
+
+    /**
+     * 根据文件后缀生成图片
+     * 来自FastAdmin的开源代码
+     * @param string $suffix 图片后缀
+     * @param string $background 背景颜色
+     * @return string             svg
+     */
+    public static function buildSuffixImage($suffix, $background = null)
+    {
+        $suffix = mb_substr(strtoupper($suffix), 0, 4);
+        $total  = unpack('L', hash('adler32', $suffix, true))[1];
+        $hue    = $total % 360;
+        [$r, $g, $b] = hsv2rgb($hue / 360, 0.3, 0.9);
+
+        $background = $background ? $background : "rgb({$r},{$g},{$b})";
+
+        $icon = '
+        <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+            <path style="fill:#E2E5E7;" d="M128,0c-17.6,0-32,14.4-32,32v448c0,17.6,14.4,32,32,32h320c17.6,0,32-14.4,32-32V128L352,0H128z"/>
+            <path style="fill:#B0B7BD;" d="M384,128h96L352,0v96C352,113.6,366.4,128,384,128z"/>
+            <polygon style="fill:#CAD1D8;" points="480,224 384,128 480,128 "/>
+            <path style="fill:' . $background . ';" d="M416,416c0,8.8-7.2,16-16,16H48c-8.8,0-16-7.2-16-16V256c0-8.8,7.2-16,16-16h352c8.8,0,16,7.2,16,16 V416z"/>
+            <path style="fill:#CAD1D8;" d="M400,432H96v16h304c8.8,0,16-7.2,16-16v-16C416,424.8,408.8,432,400,432z"/>
+            <g><text><tspan x="220" y="380" font-size="124" font-family="Verdana, Helvetica, Arial, sans-serif" fill="white" text-anchor="middle">' . $suffix . '</tspan></text></g>
+        </svg>';
+        return $icon;
+    }
+}

+ 96 - 0
addons/fastim/library/CommonCode.php

@@ -0,0 +1,96 @@
+<?php
+
+namespace addons\fastim\library;
+
+/**
+ * 一些变量
+ */
+class CommonCode
+{
+
+    /**
+     * FA用户转im用户的认证标志
+     * 结合此标志对已登录的FA用户或管理员的`token`加密稍作修改,供fastim-ws自动登录使用
+     * @var [type]
+     */
+    public static $imAuthSign = '117A5B966AE9828E0C5FCE69A5A29FF8';
+
+    // IM游客默认昵称前缀
+    public static $imTouristNicknamePrefix = '游客 ';
+
+    // 自动回复消息的前缀
+    public static $busyReplyPrefix = '[自动回复] ';
+
+    // 缺省/最大群成员数
+    public static $groupMaxUser = 500;
+
+    // 需要进行json编码和解码的消息类型
+    public static $messageJsonTypes = [
+        'file',
+        'link',
+        'system',
+        'group_apply',
+        'group_invitation',
+        'kbs_list',
+        'group_chat_notice',
+        'voice',
+        'group_notice',
+        'friend_apply'
+    ];
+
+    // 客服知识库收录提示
+    public static $kbsIncluded    = '看看这些内容对您有帮助么?';
+    public static $kbsNotIncluded = '非常抱歉,知识库尚未收录此问题~';
+    public static $kbsManualCSR   = '您可以点击以下按钮,联系人工客服代表~';
+
+    // 当用户向智能客服发送以下消息时,不经过知识库处理,直接进入人工客服转接流程
+    public static $manualCSRKeywords = [
+        '人工',
+        '人工客服',
+        '真人',
+        '机器人?'
+    ];
+
+    /**
+     * 构建IM游客昵称
+     * @param int $suffix 昵称后缀
+     * @return str 游客昵称
+     */
+    public static function buildTouristNickname($suffix)
+    {
+        return self::$imTouristNicknamePrefix . $suffix;
+    }
+
+    /**
+     * 非专有消息类型,如:`好友申请`专有于`新朋友`
+     * @param boolean $sqlSpeciallyUse SQL组装专用
+     * @param boolean $expand 额外要加入的消息类型
+     * @return str|array In-sql|非专有消息类型列表
+     */
+    public static function nonPrivilegedMessage($sqlSpeciallyUse = false, $expand = [])
+    {
+        $nonPrivilegedMessageArr = [
+            'default',
+            'image',
+            'audio',
+            'video',
+            'file',
+            'link',
+            'group_chat_notice',
+            'voice'
+        ];
+        if ($expand) {
+            $nonPrivilegedMessageArr = array_merge($nonPrivilegedMessageArr, $expand);
+        }
+
+        if ($sqlSpeciallyUse) {
+            foreach ($nonPrivilegedMessageArr as $key => &$value) {
+                $value = "'" . $value . "'";
+            }
+            $nonPrivilegedMessageArr = implode(',', $nonPrivilegedMessageArr);
+            $nonPrivilegedMessageArr = "IN (" . $nonPrivilegedMessageArr . ")";
+        }
+
+        return $nonPrivilegedMessageArr;
+    }
+}

+ 50 - 0
addons/fastim/library/controller/Base.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace addons\fastim\library\controller;
+
+/**
+ * IM控制器基类
+ */
+class Base
+{
+    public $userInfo = [];
+
+    protected $server;
+
+    protected $frame;
+
+    /**
+     * swoole Common类实例
+     */
+    protected $swooleCommon;
+
+    public $pageCount = 15;
+
+    public function __construct($base)
+    {
+        [$this->server, $this->frame, $this->swooleCommon] = $base;
+        $this->swooleCommon->setFrame($this->frame);
+        $userId = $this->swooleCommon->getUidByFd($this->frame->fd);
+        if ($userId) {
+            $this->userInfo['id'] = $userId;
+        }
+    }
+
+    /**
+     * 方法的登录权限检查
+     * @param string $funName 要访问的方法名
+     * @return bool
+     */
+    public function authCheck($funName)
+    {
+        $noNeedLogin = array_map('strtolower', $this->noNeedLogin);
+        if (!in_array(strtolower($funName), $noNeedLogin) && (!isset($this->userInfo['id']) || !$this->userInfo['id'])) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '请登录后再进行操作!'
+            ]));
+            return false;
+        }
+        return true;
+    }
+}

+ 829 - 0
addons/fastim/library/controller/ImBase.php

@@ -0,0 +1,829 @@
+<?php
+
+use think\Db;
+use addons\fastim\library\Common;
+use addons\fastim\library\controller\Base;
+
+/**
+ * im基础类
+ */
+class ImBase extends Base
+{
+    /**
+     * 无需登录的方法
+     * @var array
+     */
+    protected $noNeedLogin = ['login'];
+
+    /**
+     * 心跳
+     */
+    public function ping()
+    {
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'pong'
+        ]));
+    }
+
+    public function equipmentInspection($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['type'], $data, true)) {
+            return false;
+        }
+
+        if ($data['type'] == 'check') {
+
+            $recipientIdFds = $this->swooleCommon->getFdByUid($this->userInfo['id']);
+            if (!$recipientIdFds) {
+                return false;
+            }
+            foreach ($recipientIdFds as $key => $value) {
+                if ($this->server->isEstablished($value)) {
+                    $this->server->push($value, json_encode([
+                        'event' => 'equipment-inspection',
+                        'data'  => [
+                            'time' => $data['time'],
+                            'fd'   => $value
+                        ]
+                    ]));
+                }
+            }
+        } elseif ($data['type'] == 'close') {
+            $this->swooleCommon->unbindUid($data['fd'], $this->userInfo['id']);
+        }
+    }
+
+    /**
+     * 登录
+     */
+    public function login($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['identity', 'tokens', 'platform'], $data, true)) {
+            return false;
+        }
+
+        $tourist = false;
+        if (isset($data['tokens']['fastim_token']) && ($data['identity'] == 'admin' || $data['identity'] == 'fauser')) {
+            // 管理员或用户
+            if ($data['identity'] == 'fauser') {
+                $userInfo = Common::checkFaUser($data['tokens']['fastim_token']);
+                if ($userInfo) {
+                    $this->userInfo = Common::checkIMUser(false, $userInfo);
+                }
+            } else {
+                $userInfo = Common::checkAdmin($data['tokens']['fastim_token']);
+                if ($userInfo) {
+                    $this->userInfo = Common::checkIMUser(false, $userInfo);
+                }
+            }
+        } elseif (isset($data['tokens']['im_tourists_token']) && $data['identity'] == 'imuser') {
+            // imuser
+            $this->userInfo = Common::checkIMUser($data['tokens']['im_tourists_token']);
+            $tourist        = true;
+
+            if (isset($data['tokens']['auth_token']) && $this->userInfo) {
+                $tourist = Common::checkAuthToken($data['tokens']['auth_token']) ? false : true;
+            }
+        } else {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '登录失败,请重试!',
+                'close' => true
+            ]));
+            $this->server->close($this->frame->fd);
+            return false;
+        }
+
+        if ($this->userInfo) {
+
+            $this->swooleCommon->bindUid($this->frame->fd, $this->userInfo['id']);
+            $this->swooleCommon->bindFd($this->frame->fd, $this->userInfo['id'], $tourist, $data['platform']);
+
+            if ($this->userInfo['status'] === 0) {
+
+                $loginStatus = Common::imUserConfig($this->userInfo['id'], 'login_status');
+                $loginStatus++;
+
+                $this->userInfo['status'] = $loginStatus;
+                Db::name('fastim_user')->where('id', $this->userInfo['id'])->update([
+                    'status' => $loginStatus
+                ]);
+
+                $this->swooleCommon->radioStatusMessage($this->userInfo['id'], ($loginStatus == 3 ? 0 : $loginStatus));
+            }
+
+            // "我的客服"功能
+            $openCsr = Db::name('fastim_config')->where('name', 'open_csr')->value('value');
+            if ($openCsr && $this->userInfo['type'] != 'csr') {
+                // 为用户添加我的客服会话
+                $imSession         = Common::imSession('service', $this->userInfo['id'], 3);
+                $welcomeNewNserMsg = Db::name('fastim_config')->where('name', 'welcome_new_user_msg')->value('value');
+                if ($welcomeNewNserMsg && isset($imSession['create']) && $imSession['create']) {
+                    // 发送欢迎消息给用户
+                    $sessionInfo = Common::sessionInfo($imSession['id'], $this->userInfo['id']);
+                    // 消息入库
+                    $messageData = Common::insertMessage([
+                        'type'         => 'default',
+                        'session_id'   => $sessionInfo['id'],
+                        'sender_id'    => 0,
+                        'recipient_id' => $this->userInfo['id'],
+                        'message'      => $welcomeNewNserMsg
+                    ]);
+                }
+            }
+
+            $this->userInfo['unreadMessage'] = Common::getUnreadMessage($this->userInfo['id'], $data['initialize_time'] ?? false);
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'login_success',
+                'data'  => $this->userInfo
+            ]));
+            return $this->userInfo;
+        } else {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '用户身份识别失败,请重新登录!',
+                'close' => true
+            ]));
+            $this->server->close($this->frame->fd);
+            return false;
+        }
+    }
+
+    /**
+     * 用户修改状态
+     */
+    public function changeStatus($data)
+    {
+        $imUserStatus = Common::imUserStatus($data['status']);
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'change_status',
+            'data'  => [
+                'status_chinese' => $imUserStatus['chinese'],
+                'status'         => $data['status'],
+                'uid'            => $this->userInfo['id']
+            ]
+        ]));
+
+        if ($data['status'] == 0) {
+            // 用户主动离线
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '',
+                'close' => true
+            ]));
+            $this->server->close($this->frame->fd);
+        } else {
+
+            $this->swooleCommon->radioStatusMessage($this->userInfo['id'], ($data['status'] == 3 ? 0 : $data['status']));
+
+            Db::name('fastim_user')->where('id', $this->userInfo['id'])->update([
+                'status' => $data['status']
+            ]);
+        }
+    }
+
+    /**
+     * 待办
+     */
+    public function loadTODO($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['method'], $data)) {
+            return;
+        }
+
+        if ($data['method'] == 'save') {
+            if (!$this->swooleCommon->requiredParamCheck(['add_val'], $data, false, ['add_val' => '请输入正确的待办内容~'])) {
+                return;
+            }
+            if (isset($data['id']) && $data['id']) {
+                Db::name('fastim_todo')->where('id', $data['id'])->where('user_id', $this->userInfo['id'])->update([
+                    'title' => $data['add_val']
+                ]);
+            } else {
+                Db::name('fastim_todo')->insert([
+                    'user_id'    => $this->userInfo['id'],
+                    'title'      => $data['add_val'],
+                    'status'     => '0',
+                    'createtime' => time()
+                ]);
+            }
+        } elseif ($data['method'] == 'update') {
+            if (!$this->swooleCommon->requiredParamCheck(['id'], $data)) {
+                return;
+            }
+            Db::name('fastim_todo')->where('id', $data['id'])->where('user_id', $this->userInfo['id'])->update([
+                'status' => $data['status'] ? '1' : '0'
+            ]);
+            if ($data['status']) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => [
+                        'msg'  => '待办事项已完成~',
+                        'type' => 'success'
+                    ]
+                ]));
+            }
+        } elseif ($data['method'] == 'get-all-count') {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'load_to_do',
+                'data'  => [
+                    'count' => Db::name('fastim_todo')
+                        ->where('user_id', $this->userInfo['id'])
+                        ->where('status', '0')
+                        ->where('deletetime', null)
+                        ->count(),
+                    'data'  => $data
+                ]
+            ]));
+            return;
+        }
+
+        if (!isset($data['page']) || $data['page'] == 1) {
+            $min          = 0;
+            $data['page'] = 1;
+        } else {
+            $min = ($data['page'] - 1) * $this->pageCount;
+        }
+
+        // 未完成的-加载全部
+        $incomplete = Db::name('fastim_todo')
+            ->where('user_id', $this->userInfo['id'])
+            ->where('status', '0')
+            ->where('deletetime', null)
+            ->order('createtime desc')
+            ->select();
+
+        // 完成了的
+        $completed = Db::name('fastim_todo')
+            ->where('user_id', $this->userInfo['id'])
+            ->where('status', '1')
+            ->where('deletetime', null)
+            ->order('createtime desc')
+            ->limit($min, $this->pageCount)
+            ->select();
+
+        if ($data['page'] == 1) {
+            $data['incomplete'] = count($incomplete);
+            $data['completed']  = Db::name('fastim_todo')
+                ->where('user_id', $this->userInfo['id'])
+                ->where('status', '1')
+                ->where('deletetime', null)
+                ->order('createtime desc')
+                ->count('id');
+        }
+
+        $data['nextpage'] = (count($completed) < $this->pageCount) ? false : true;
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'load_to_do',
+            'data'  => [
+                'incomplete' => $incomplete,
+                'completed'  => $completed,
+                'data'       => $data
+            ]
+        ]));
+    }
+
+    /**
+     * 获得设置数据
+     */
+    public function config($data)
+    {
+        // 已屏蔽的联系人
+        $shieldUsers = Db::name('fastim_user_shield')
+            ->alias('s')
+            ->field('s.*,f.remark,fu.nickname as fu_nickname,a.nickname as a_nickname,u.nickname,u.id as uid,se.id as session_id')
+            ->join('fastim_session se', '(se.user_one=s.user_id AND se.user_two=s.shield_id) OR (se.user_one=s.shield_id AND se.user_two=s.user_id)')
+            ->join('fastim_user u', 's.shield_id=u.id')
+            ->join('user fu', 'u.user_id=fu.id', 'LEFT')
+            ->join('admin a', 'u.admin_id=a.id', 'LEFT')
+            ->join('fastim_friendship f', 's.user_id=f.user_id AND s.shield_id=f.friend_id', 'LEFT')
+            ->where('s.user_id', $this->userInfo['id'])
+            ->select();
+        $nicknameKey = ['nickname', 'fu_nickname', 'a_nickname', 'remark'];
+        foreach ($shieldUsers as $key => &$value) {
+            $value['nickname'] = Common::trueAttr(Common::nicknameSort([
+                $value['remark'],
+                $value['nickname'],
+                $value['fu_nickname'],
+                $value['a_nickname']
+            ], $value['uid']));
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'config',
+            'data'  => [
+                'shield_users' => $shieldUsers
+            ]
+        ]));
+    }
+
+    /**
+     * 更新用户配置
+     */
+    public function updateUserConfig($data)
+    {
+        $imDbConfig = Db::name('fastim_config')->where('user_settings', 1)->column('name,value');
+        $userConfig = Db::name('fastim_user_config')->where('user_id', $this->userInfo['id'])->column('name,value');
+        foreach ($data as $key => $value) {
+            $value = trim($value, ',');
+            // 是否为有效配置
+            if (array_key_exists($key, $imDbConfig)) {
+                // 是否已设置过
+                if (array_key_exists($key, $userConfig)) {
+                    $setUp = true;
+                } else {
+                    $setUp = false;
+                }
+
+                // 设置为默认值
+                if ($value == $imDbConfig[$key]) {
+                    if ($setUp) {
+                        Db::name('fastim_user_config')
+                            ->where('user_id', $this->userInfo['id'])
+                            ->where('name', $key)
+                            ->delete();
+                    }
+                    continue;
+                }
+
+                if ($setUp) {
+                    Db::name('fastim_user_config')
+                        ->where('user_id', $this->userInfo['id'])
+                        ->where('name', $key)
+                        ->update([
+                            'value' => $value
+                        ]);
+                } else {
+                    $insert = [
+                        'user_id' => $this->userInfo['id'],
+                        'name'    => $key,
+                        'value'   => $value
+                    ];
+                    Db::name('fastim_user_config')->insert($insert);
+                }
+            }
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'show_msg',
+            'data'  => [
+                'msg'  => '设置已保存,刷新生效~',
+                'type' => 'success'
+            ]
+        ]));
+    }
+
+    /**
+     * 屏蔽/解除屏蔽一个会话
+     */
+    public function shieldSession($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['session_id', 'method'], $data)) {
+            return;
+        }
+
+        $sessionInfo = Common::sessionInfo($data['session_id'], $this->userInfo['id']);
+
+        $exist = Db::name('fastim_user_shield')
+            ->where('user_id', $this->userInfo['id'])
+            ->where('shield_id', $sessionInfo['sessionUser']['id'])
+            ->value('id');
+
+        if ($data['method'] == 'shield' && !$exist) {
+            $shield = [
+                'user_id'   => $this->userInfo['id'],
+                'shield_id' => $sessionInfo['sessionUser']['id']
+            ];
+            Db::name('fastim_user_shield')->insert($shield);
+            if ($sessionInfo['deleteuser']) {
+                Db::name('fastim_session')->where('id', $sessionInfo['id'])->delete();
+            } else {
+                Db::name('fastim_session')->where('id', $sessionInfo['id'])->update([
+                    'deleteuser' => $this->userInfo['id']
+                ]);
+            }
+        } elseif ($data['method'] == 'relieve' && $exist) {
+            Db::name('fastim_user_shield')->where('id', $exist)->delete();
+            if ($sessionInfo['deleteuser'] == $this->userInfo['id']) {
+                Db::name('fastim_session')->where('id', $sessionInfo['id'])->update([
+                    'deleteuser' => 0
+                ]);
+            }
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'shield_session',
+            'data'  => [
+                'session_id' => $data['session_id'],
+                'method'     => $data['method']
+            ]
+        ]));
+    }
+
+    /**
+     * 一个用户的全部快捷回复
+     */
+    public function userAllFastReply()
+    {
+        $fastReply = Db::name('fastim_fast_reply')->field('id,title,content')->where(function ($query) {
+            $query->where('user_id', $this->userInfo['id'])->whereOr('user_id', 0);
+        })->where('status', '1')->where('deletetime', null)->order('user_id desc,createtime desc')->select();
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'user_all_fast_reply',
+            'data'  => $fastReply
+        ]));
+    }
+
+    /**
+     * 用户快捷回复
+     */
+    public function fastReply($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['method'], $data)) {
+            return;
+        }
+
+        $isUniApp = false;
+        if (isset($data['form']) && $data['form'] == 'uni-app') {
+            $isUniApp = true;
+        }
+
+        if ($data['method'] == 'del') {
+            if (!$this->swooleCommon->requiredParamCheck(['id'], $data)) {
+                return;
+            }
+
+            Db::name('fastim_fast_reply')->where('id', $data['id'])->update([
+                'deletetime' => time()
+            ]);
+
+            $data['page'] = 1;
+
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => [
+                    'msg'  => '快捷回复已删除~',
+                    'type' => 'success'
+                ]
+            ]));
+
+            if ($isUniApp) {
+                $this->userAllFastReply();
+            }
+        } elseif ($data['method'] == 'edit') {
+            $info = Db::name('fastim_fast_reply')->where('id', $data['id'])->where('deletetime', null)->find();
+
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'fast_reply',
+                'data'  => [
+                    'data' => $data,
+                    'info' => $info
+                ]
+            ]));
+            return;
+        } elseif ($data['method'] == 'update') {
+            $updateData     = $data['data'];
+            $data['method'] = 'get';
+            unset($data['data']);
+
+            if (!$this->swooleCommon->requiredParamCheck(['title', 'content'], $updateData, false, [
+                'title'   => '请输入标题~',
+                'content' => '请输入回复内容~'
+            ])) {
+                return;
+            }
+
+            if ($updateData['id'] > 0) {
+                // 更新
+                Db::name('fastim_fast_reply')
+                    ->where('id', $updateData['id'])
+                    ->where('user_id', $this->userInfo['id'])
+                    ->update($updateData);
+                $msg = '快捷回复已更新~';
+            } else {
+                // 添加
+                unset($updateData['id']);
+                $updateData['user_id']    = $this->userInfo['id'];
+                $updateData['createtime'] = time();
+                Db::name('fastim_fast_reply')->insert($updateData);
+                $msg = '快捷回复添加成功~';
+            }
+
+            if ($isUniApp) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'fast_reply',
+                    'data'  => [
+                        'data' => [
+                            'method' => 'opt-done',
+                            'msg'    => $msg
+                        ]
+                    ]
+                ]));
+
+                $this->userAllFastReply();
+                return;
+            } else {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => [
+                        'msg'  => $msg,
+                        'type' => 'success'
+                    ]
+                ]));
+            }
+        }
+
+        if (!isset($data['page']) || $data['page'] == 1) {
+            $min = 0;
+        } else {
+            $min = ($data['page'] - 1) * $this->pageCount;
+        }
+
+        $fastReplyList = Db::name('fastim_fast_reply')
+            ->where('user_id', $this->userInfo['id'])
+            ->where('deletetime', null)
+            ->limit($min, $this->pageCount)
+            ->select();
+
+        $data['nextpage'] = (count($fastReplyList) < $this->pageCount) ? false : true;
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'fast_reply',
+            'data'  => [
+                'data'            => $data,
+                'fast_reply_list' => $fastReplyList
+            ]
+        ]));
+    }
+
+    /**
+     * 举报和问题反馈提交
+     */
+    public function report($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['method'], $data)) {
+            return;
+        }
+
+        if ($data['type'] == 'user' || $data['type'] == 'group') {
+            if (!$this->swooleCommon->requiredParamCheck(['session_id'], $data, false, ['session_id' => '会话找不到啦~'])) {
+                return;
+            }
+            $sessionInfo = Common::sessionInfo($data['session_id'], $this->userInfo['id']);
+        }
+
+        if ($data['method'] == 'post') {
+
+            $title = ($data['type'] == 'user' || $data['type'] == 'group') ? '举报' : '反馈';
+            if (!$this->swooleCommon->requiredParamCheck(['describe'], $data, false, [
+                'describe' => '请输入' . $title . '详情~'
+            ])) {
+                return;
+            }
+
+            // 数据入库
+            $report = [
+                'type'        => $data['type'],
+                'session_id'  => $sessionInfo['id'] ?? 0,
+                'report_id'   => $this->userInfo['id'],
+                'user_id'     => $sessionInfo['sessionUser']['id'] ?? 0,
+                'describe'    => $data['describe'],
+                'mobile'      => $data['mobile'] ?? '',
+                'reportimage' => isset($data['reportimage']) ? trim($data['reportimage'], ',') : '',
+                'createtime'  => time()
+            ];
+            if (Db::name('fastim_report')->insert($report)) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => [
+                        'msg'  => $title . '提交成功!',
+                        'type' => 'success'
+                    ]
+                ]));
+            } else {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => [
+                        'msg'  => $title . '提交失败,请重试!',
+                        'type' => 'error'
+                    ]
+                ]));
+            }
+            return;
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => ($data['type'] == 'user' || $data['type'] == 'group') ? 'report' : 'feedback',
+            'data'  => $sessionInfo
+        ]));
+    }
+
+    /**
+     * 获取分组
+     * @return void
+     */
+    public function getGroups($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['type'], $data)) {
+            return;
+        }
+
+        // 获取用户的好友和群聊分组
+        if ($data['type'] == 'contacts' || $data['type'] == 'pick-contacts') {
+            $res = [
+                'friends' => Common::getGroups($this->userInfo['id'], 0),
+                'group'   => Common::getGroups($this->userInfo['id'], 1)
+            ];
+        } elseif ($data['type'] == 'collection') {
+            // 获取用户的收藏分组
+            $res = [
+                'collection' => Common::getGroups($this->userInfo['id'], 2)
+            ];
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'get_groups',
+            'data'  => [
+                'res'  => $res,
+                'data' => $data
+            ]
+        ]));
+    }
+
+    /**
+     * 好友和群聊分组管理
+     */
+    public function updateGroup($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['action'], $data)) {
+            return false;
+        }
+
+        if ($data['action'] != 'add-new-group') {
+            $group = Db::name('fastim_group')
+                ->where('id', $data['id'])
+                ->where('user_id', $this->userInfo['id'])
+                ->find();
+            if (!$group) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => '要操作的分组找不到啦~'
+                ]));
+                return false;
+            }
+
+            // 重新加载前台的分组需要的参数
+            if ($group['type'] == 0 || $group['type'] == 1) {
+                $reloadGroupData = [
+                    'type' => 'contacts'
+                ];
+            } elseif ($group['type'] == 2) {
+                $reloadGroupData = [
+                    'type' => 'collection'
+                ];
+            }
+        }
+
+        if ($data['action'] == 'add-new-group') {
+            if (!$this->swooleCommon->requiredParamCheck(['type', 'name'], $data, false, ['name' => '请输入正确的分组名称~'])) {
+                return false;
+            }
+
+            switch ($data['type']) {
+                case 'friends':
+                    $data['type'] = 0;
+                    break;
+                case 'group':
+                    $data['type'] = 1;
+                    break;
+                case 'collection':
+                    $data['type'] = 2;
+                    break;
+            }
+
+            $weigh  = Db::name('fastim_group')
+                ->where('user_id', $this->userInfo['id'])
+                ->where('type', $data['type'])
+                ->max('id');
+            $weigh  = $weigh ? ($weigh + 1) : 1;
+            $insert = [
+                'type'    => $data['type'],
+                'user_id' => $this->userInfo['id'],
+                'name'    => $data['name'],
+                'weigh'   => $weigh
+            ];
+            if (Db::name('fastim_group')->insert($insert)) {
+                if ($data['type'] == 0 || $data['type'] == 1) {
+                    $reloadGroupData = [
+                        'type' => 'contacts'
+                    ];
+                } else {
+                    $reloadGroupData = [
+                        'type' => 'collection'
+                    ];
+                }
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => [
+                        'msg'  => '添加分组成功~',
+                        'type' => 'success'
+                    ]
+                ]));
+            } else {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => '添加失败,请重试!'
+                ]));
+                return false;
+            }
+        } elseif ($data['action'] == 'update-group-name') {
+            if (!$this->swooleCommon->requiredParamCheck([
+                'id',
+                'newname'
+            ], $data, false, ['newname' => '请输入正确的分组名称~'])) {
+                return false;
+            }
+
+            if (Db::name('fastim_group')
+                ->where('id', $data['id'])
+                ->where('user_id', $this->userInfo['id'])
+                ->update(['name' => $data['newname']])) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => [
+                        'msg'  => '保存成功~',
+                        'type' => 'success'
+                    ]
+                ]));
+            } else {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => '未做任何修改~'
+                ]));
+                return false;
+            }
+        } elseif ($data['action'] == 'del' && Db::name('fastim_group')
+                ->where('id', $data['id'])
+                ->where('user_id', $this->userInfo['id'])
+                ->delete()) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => [
+                    'msg'  => '分组已删除~',
+                    'type' => 'success'
+                ]
+            ]));
+        } elseif ($data['action'] == 'moveup') {
+            $up = Db::name('fastim_group')
+                ->where('user_id', $this->userInfo['id'])
+                ->where('type', $group['type'])
+                ->where('weigh', '<', $group['weigh'])
+                ->order('weigh desc')
+                ->find();
+
+            Db::name('fastim_group')->update([
+                'id'    => $up['id'],
+                'weigh' => $group['weigh']
+            ]);
+            Db::name('fastim_group')->update([
+                'id'    => $group['id'],
+                'weigh' => $up['weigh']
+            ]);
+        } elseif ($data['action'] == 'movedown') {
+            $down = Db::name('fastim_group')
+                ->where('user_id', $this->userInfo['id'])
+                ->where('type', $group['type'])
+                ->where('weigh', '>', $group['weigh'])
+                ->order('weigh asc')
+                ->find();
+            Db::name('fastim_group')->update([
+                'id'    => $down['id'],
+                'weigh' => $group['weigh']
+            ]);
+            Db::name('fastim_group')->update([
+                'id'    => $group['id'],
+                'weigh' => $down['weigh']
+            ]);
+        }
+
+        return $this->getGroups($reloadGroupData);
+    }
+
+    /**
+     * 更新云存储 multipart
+     */
+    public function getUploadMultipart($data)
+    {
+        // 获取上传配置
+        $upload = \app\common\model\Config::upload();
+        \think\Hook::listen("upload_config_init", $upload);
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'upload_multipart',
+            'data'  => [
+                'multipart' => isset($upload['multipart']) ? $upload['multipart'] : []
+            ]
+        ]));
+    }
+}

+ 1868 - 0
addons/fastim/library/controller/Message.php

@@ -0,0 +1,1868 @@
+<?php
+
+use think\Db;
+use addons\fastim\library\Common;
+use addons\fastim\library\CommonCode;
+use addons\fastim\library\controller\Base;
+use think\exception\PDOException;
+
+/**
+ * im会话、消息类
+ */
+class Message extends Base
+{
+    protected $noNeedLogin = [];
+
+    /**
+     * 输入状态传递
+     */
+    public function inputStatusNotice($data)
+    {
+        if (isset($data['session_id'])) {
+            // 通知对方在输入
+            $sessionInfo = Common::sessionInfo($data['session_id'], $this->userInfo['id']);
+            if ($sessionInfo) {
+                $this->swooleCommon->pushMessageToUser($sessionInfo['sessionUser']['id'], [
+                    'event' => 'input_status',
+                    'data'  => [
+                        'session_id' => $data['session_id'],
+                        'status'     => 'start'
+                    ]
+                ]);
+            }
+        }
+
+        if (isset($data['end_input_session_id'])) {
+            // 通知一个会话输入结束
+            $sessionInfo = Common::sessionInfo($data['end_input_session_id'], $this->userInfo['id']);
+            if ($sessionInfo) {
+                $this->swooleCommon->pushMessageToUser($sessionInfo['sessionUser']['id'], [
+                    'event' => 'input_status',
+                    'data'  => [
+                        'session_id' => $data['end_input_session_id'],
+                        'status'     => 'end'
+                    ]
+                ]);
+            }
+        }
+    }
+
+
+    /**
+     * 删除群成员
+     */
+    public function delGroupMember($data)
+    {
+        $groupChatInfo = Common::getImGroupChat($data['id']);
+        if ($groupChatInfo['leader'] == $this->userInfo['id']) {
+            if (Db::name('fastim_group_user')
+                ->where('group_id', $data['id'])
+                ->where('user_id', $data['member_id'])
+                ->delete()) {
+
+                // 删除对应用户的会话
+                $imSession = Common::imSession('group', $data['member_id'], $data['id']);
+                Db::name('fastim_session')->where('id', $imSession['id'])->delete();
+
+                // 发送被踢通知给被删除用户
+                $groupImSession = Common::imSession('service', $data['member_id'], 2);
+                $messageData    = Common::insertMessage([
+                    'type'         => 'group_notice',
+                    'group_id'     => $data['id'],
+                    'session_id'   => $groupImSession['id'],
+                    'sender_id'    => 0,
+                    'recipient_id' => $data['member_id'],
+                    'message'      => [
+                        'action' => 'propose'
+                    ]
+                ]);
+                if ($messageData) {
+                    $sessionInfo                  = Common::sessionInfo($groupImSession['id'], $data['member_id']);
+                    $sessionInfo['userInfo']      = Common::getImUserInfo($data['member_id']);
+                    $sessionInfo['groupChatInfo'] = Common::getImGroupChat($data['id']);
+                    $this->swooleCommon->newMessagePush($messageData, $sessionInfo, $data['member_id']);
+
+                    $this->swooleCommon->pushMessageToUser($data['member_id'], [
+                        'event' => 'reload_session_list'
+                    ]);
+                }
+
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'del-group-member',
+                    'data'  => [
+                        'msg'       => '已删除群成员~',
+                        'member_id' => $data['member_id']
+                    ]
+                ]));
+            }
+        } else {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => [
+                    'msg'  => '您没有权限操作~',
+                    'type' => 'error'
+                ]
+            ]));
+            return;
+        }
+    }
+
+    public function loadNoticeInfo($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id'], $data)) {
+            return;
+        }
+
+        $message = Db::name('fastim_message')->where('id', $data['id'])->find();
+        if (!$message) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => [
+                    'msg'  => '消息找不到啦,请重试~',
+                    'type' => 'error'
+                ]
+            ]));
+            return;
+        }
+
+        $message['message'] = json_decode($message['message'], true);
+        if ($message['type'] == 'group_invitation') {
+            $message['message'] = array_merge($message['message'], Common::invitationMessageResult($message));
+        }
+
+        if ($message['group_id']) {
+            $message['groupChatInfo'] = Common::getImGroupChat($message['group_id']);
+        }
+
+        if ($message['type'] == 'friend_apply') {
+            if ($message['sender_id'] == $this->userInfo['id']) {
+                $message['userInfo'] = Common::getImUserInfo($message['recipient_id']);
+                $imSession           = Common::imSession('single', $this->userInfo['id'], $message['recipient_id']);
+            } else {
+                $message['userInfo'] = Common::getImUserInfo($message['sender_id']);
+                $imSession           = Common::imSession('single', $this->userInfo['id'], $message['sender_id']);
+            }
+            $message['user_session_id'] = $imSession['id'];
+            $message['shield']          = Db::name('fastim_user_shield')
+                ->where('user_id', $this->userInfo['id'])
+                ->where('shield_id', ($message['sender_id'] == $this->userInfo['id'] ? $message['recipient_id'] : $message['sender_id']))
+                ->value('id');
+
+            $message['friendship'] = Db::name('fastim_friendship')
+                ->where('user_id', $this->userInfo['id'])
+                ->where('friend_id', ($message['sender_id'] == $this->userInfo['id'] ? $message['recipient_id'] : $message['sender_id']))
+                ->value('user_id');
+        } elseif ($message['type'] == 'group_apply') {
+            // 入群申请
+            if ($message['sender_id'] != $this->userInfo['id']) {
+                $message['userInfo'] = Common::getImUserInfo($message['sender_id']);
+            }
+        } elseif ($message['type'] == 'group_notice') {
+            if ($message['message']['action'] == 'quit') {
+                $message['userInfo'] = Common::getImUserInfo($message['message']['quit_user']);
+            } elseif ($message['message']['action'] == 'refuse') {
+                $message['userInfo'] = Common::getImUserInfo($message['message']['refuse_user']);
+            }
+        } elseif ($message['type'] == 'group_invitation') {
+            $message['invitationUser'] = Common::getImUserInfo($message['message']['invitation_user']);
+            if ($message['message']['identity'] == 'admin') {
+                $message['invitedUser'] = Common::getImUserInfo($message['message']['invited_user']);
+            }
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'notice_info',
+            'data'  => [
+                'info' => $message
+            ]
+        ]));
+    }
+
+    /**
+     * 转发消息
+     */
+    public function forward($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['message_ids', 'pickuser', 'type'], $data)) {
+            return;
+        }
+
+        $pickuser = explode(',', trim($data['pickuser'], ','));
+        if (!$pickuser) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '请选择转发好友~'
+            ]));
+            return;
+        }
+
+        $message_ids = explode(',', trim($data['message_ids'], ','));
+
+        // 转发收藏
+        if ($data['type'] == 'collection') {
+            $message      = Db::name('fastim_user_collection')
+                ->whereIn('id', $message_ids)
+                ->where('user_id', $this->userInfo['id'])
+                ->select();
+            $messageField = 'value';
+        } elseif ($data['type'] == 'message') {
+            $message      = Db::name('fastim_message')->whereIn('id', $message_ids)->select();
+            $messageField = 'message';
+        }
+
+        if (!$message) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '你要转发的消息找不到啦~'
+            ]));
+            return;
+        }
+
+        foreach ($message as $key => $value) {
+            if (in_array($value['type'], CommonCode::$messageJsonTypes)) {
+                $message[$key]['message'] = json_decode($value[$messageField], true);
+            } else {
+                $message[$key]['message'] = htmlspecialchars_decode($value[$messageField]);
+            }
+        }
+
+        // 获取所有接受人的会话ID
+        $sessionIds = [];
+        foreach ($pickuser as $key => $value) {
+            $imSession    = Common::imSession('single', $this->userInfo['id'], $value);
+            $sessionIds[] = $imSession['id'];
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'forward_message',
+            'data'  => [
+                'data'        => $data,
+                'message'     => $message,
+                'session_ids' => $sessionIds
+            ]
+        ]));
+    }
+
+    /**
+     * 消息右击
+     */
+    public function messageOperation($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id', 'action'], $data)) {
+            return false;
+        }
+
+        $messageInfo = Db::name('fastim_message')->where('id', $data['id'])->find();
+        if (!$messageInfo) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '消息找不到啦~'
+            ]));
+            return false;
+        }
+
+        // 复制和引用
+        if ($data['action'] == 'message-copy' || $data['action'] == 'message-quote' || $data['action'] == 'message-to-do') {
+            if ($messageInfo['type'] == 'link' || $messageInfo['type'] == 'file') {
+                $messageInfo['message']     = json_decode($messageInfo['message'], true);
+                $messageInfo['messageText'] = ($messageInfo['type'] == 'link') ? $messageInfo['message']['link_url'] : $messageInfo['message']['url'];
+            } else {
+                $messageInfo['messageText'] = Common::formatLastMessage($messageInfo, $this->userInfo['id']);
+            }
+
+            // 引用
+            if ($data['action'] == 'message-quote') {
+                $userInfo                   = Common::getImUserInfo($messageInfo['sender_id']);
+                $messageInfo['messageText'] = '「' . $userInfo['nickname'] . ': ' . $messageInfo['messageText'] . '」';
+            }
+
+            // 待办
+            if ($data['action'] == 'message-to-do') {
+                Db::name('fastim_todo')->insert([
+                    'user_id'    => $this->userInfo['id'],
+                    'title'      => $messageInfo['messageText'],
+                    'status'     => '0',
+                    'createtime' => time()
+                ]);
+                $resMessage = [
+                    'type' => 'success',
+                    'msg'  => '已添加至待办~'
+                ];
+            }
+        } elseif ($data['action'] == 'message-collection') {
+            // 收藏
+            if ($messageInfo['type'] == 'kbs_list') {
+                $userInfo = Db::name('fastim_service')->where('id', 3)->find();
+            } else {
+                $userInfo = Common::getImUserInfo($messageInfo['sender_id']);
+            }
+
+            if (in_array($messageInfo['type'], CommonCode::$messageJsonTypes)) {
+                $messageInfo['message'] = json_decode($messageInfo['message'], true);
+                $messageInfo['message'] = json_encode($messageInfo['message'], JSON_UNESCAPED_UNICODE);
+            }
+
+            Db::name('fastim_user_collection')->insert([
+                'user_id'    => $this->userInfo['id'],
+                'group_id'   => 0,
+                'type'       => $messageInfo['type'],
+                'value'      => $messageInfo['message'],
+                'from'       => $userInfo['nickname'] ?? '',
+                'createtime' => time()
+            ]);
+            $resMessage = [
+                'type' => 'success',
+                'msg'  => '已添加至收藏~'
+            ];
+        } elseif ($data['action'] == 'message-delete') {
+            // 删除消息
+
+            $sessionInfo = Common::sessionInfo($messageInfo['session_id'], $this->userInfo['id']);
+            if ($sessionInfo['type'] != 'single') {
+                return;
+            }
+
+            Db::name('fastim_message')->where('id', $data['id'])->update([
+                'deleteuser' => $this->userInfo['id']
+            ]);
+            $messageInfo = array_merge($messageInfo, Common::getLastMessage($this->userInfo['id'], $sessionInfo));
+        } elseif ($data['action'] == 'message-new-window') {
+            // 新窗口打开
+            if ($messageInfo['type'] == 'link') {
+                $messageInfo['message']    = json_decode($messageInfo['message'], true);
+                $messageInfo['messageUrl'] = $messageInfo['message']['link_url'];
+            } else {
+                $messageInfo['messageUrl'] = $messageInfo['message'];
+            }
+        }
+
+        if (isset($resMessage)) {
+            $resMessage['method'] = 'show_msg';// 为uni端标记是showMsg
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => (isset($data['source']) && $data['source'] == 'uni-app') ? 'message_operation' : 'show_msg',
+                'data'  => $resMessage
+            ]));
+            return false;
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'message_operation',
+            'data'  => [
+                'method'      => 'operation',
+                'data'        => $data,
+                'messageInfo' => $messageInfo
+            ]
+        ]));
+    }
+
+    /**
+     * 获取群聊的屏蔽状态
+     */
+    public function getGroupChatShield($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['session_id'], $data)) {
+            return;
+        }
+        $sessionInfo = Common::sessionInfo($data['session_id'], $this->userInfo['id']);
+        $shield      = Common::getGroupChatShield($sessionInfo['sessionUser']['id'], $this->userInfo['id']);
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'group_chat_shield',
+            'data'  => [
+                'shield'     => $shield ? 'shield' : 'unshielded',
+                'session_id' => $data['session_id']
+            ]
+        ]));
+    }
+
+    /**
+     * 邀请加入群聊
+     */
+    public function invitationjoinGroupOption($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id', 'action'], $data, false, ['id' => '申请消息找不到啦~'])) {
+            return;
+        }
+
+        $message = Db::name('fastim_message')->where('id', $data['id'])->where('type', 'group_invitation')->find();
+        if (!$message) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '申请消息找不到啦~'
+            ]));
+            return;
+        }
+
+        $imGroupChatInfo    = Common::getImGroupChat($message['group_id']);
+        $message['message'] = json_decode($message['message'], true);
+
+        // 修改消息状态
+        if ($message['message']['identity'] == 'admin') {
+            if (($message['recipient_id'] != $imGroupChatInfo['leader']) || ($message['recipient_id'] != $this->userInfo['id'])) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => '你没有权限操作~'
+                ]));
+                return;
+            }
+        } else {
+            if ($message['recipient_id'] != $this->userInfo['id']) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => '你没有权限操作~'
+                ]));
+                return;
+            }
+        }
+
+        $message['message']['result'] = ($data['action'] == 'agree') ? 1 : 2;
+        $newMessage                   = json_encode($message['message']);
+        Db::name('fastim_message')->where('id', $data['id'])->update(['message' => $newMessage]);
+
+        $messageResultAll = Common::invitationMessageResult($message);
+        if ($data['action'] == 'agree') {
+            // 如果都同意了,用户入群
+            if ($messageResultAll['user_result'] == 1 && $messageResultAll['leader_result'] == 1) {
+                $imSession = Common::imSession('group', $message['message']['invited_user'], $message['group_id']);
+                $groupUser = [
+                    'group_id'      => $message['group_id'],
+                    'user_id'       => $message['message']['invited_user'],
+                    'session_id'    => $imSession['id'],
+                    'user_group_id' => 0,// 邀请入群需双方同意,不要求用户选择分组
+                    'jointime'      => time()
+                ];
+                Db::name('fastim_group_user')->insert($groupUser);
+
+                $this->swooleCommon->pushMessageToGroup('system', [
+                    'group_id'     => $message['group_id'],
+                    'session_id'   => $imSession['id'],
+                    'sender_id'    => $this->userInfo['id'],
+                    'recipient_id' => $message['message']['invited_user'],
+                    'message'      => [
+                        'message'      => '你已经是群成员了,和大家打个招呼吧~',
+                        'display_user' => $message['message']['invited_user']
+                    ]
+                ]);
+            }
+
+            $res = [
+                'msg'  => '已同意加群申请~',
+                'type' => 'success'
+            ];
+        } elseif ($data['action'] == 'refuse') {
+            $res = [
+                'msg'  => '已拒绝入群申请~',
+                'type' => 'tips'
+            ];
+
+            // 如果被邀请人已经处理过邀请,给被邀请人发送拒绝消息
+            $pushRefuseMessage = [];
+            if ($messageResultAll['user_result'] == 1) {
+                $pushRefuseMessage[] = $message['message']['invited_user'];
+            }
+            // 给邀请人推送拒绝消息
+            if ($message['message']['invitation_user']) {
+                $pushRefuseMessage[] = $message['message']['invitation_user'];
+            }
+
+            foreach ($pushRefuseMessage as $key => $recipientUserId) {
+                $imSession                    = Common::imSession('service', $recipientUserId, 2);
+                $sessionInfo                  = Common::sessionInfo($imSession['id'], $recipientUserId);
+                $messageData                  = Common::insertMessage([
+                    'type'         => 'group_notice',
+                    'group_id'     => $message['group_id'],
+                    'session_id'   => $sessionInfo['id'],
+                    'sender_id'    => $this->userInfo['id'],
+                    'recipient_id' => $recipientUserId,
+                    'message'      => [
+                        'action'      => 'refuse',
+                        'refuse_user' => $message['message']['invited_user']
+                    ]
+                ]);
+                $sessionInfo['userInfo']      = Common::getImUserInfo($message['message']['invited_user'], false, $message['message']['invited_user']);
+                $sessionInfo['groupChatInfo'] = Common::getImGroupChat($message['group_id']);
+                $this->swooleCommon->newMessagePush($messageData, $sessionInfo, $recipientUserId);
+            }
+        }
+
+        $imSession         = Common::imSession('service', $this->userInfo['id'], 2);
+        $res['session_id'] = $imSession['id'];
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'new_friends_option',
+            'data'  => $res
+        ]));
+
+        if ($this->userInfo['id'] == $imGroupChatInfo['leader']) {
+            $this->swooleCommon->pushMessageToUser($message['message']['invited_user'], [
+                'event' => 'reload_session_list'
+            ]);
+        } else {
+            $this->swooleCommon->pushMessageToUser($imGroupChatInfo['leader'], [
+                'event' => 'reload_session_list'
+            ]);
+        }
+    }
+
+    /**
+     * 邀请好友加入群聊
+     */
+    public function invitationGroupmember($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id', 'pickuser'], $data, false, ['pickuser' => '请选择需要邀请的好友~'])) {
+            return;
+        }
+
+        $pickuser = explode(',', trim($data['pickuser'], ','));
+        if (!$pickuser) {
+            $msg = '请选择需要邀请的好友~';
+        }
+
+        $info = Common::getImGroupChat($data['id']);
+        if (!$info) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '群聊找不到啦~'
+            ]));
+            return;
+        }
+
+        /**
+         * 以下三个判断分别是
+         * 群主本人邀请
+         * 群聊设置了入群免验证
+         * 群聊设置了成员邀请免验证
+         * leaderResult:0=待管理员审核,1=管理员已同意,2=管理员已拒绝
+         */
+        $leaderResult = 0;
+        if (($this->userInfo['id'] == $info['leader']) || ($info['add_mode'] == 1) || ($info['invite_join_group'] == 1)) {
+            $leaderResult = 1;
+        }
+        $adminMessage = [
+            'invitation_user' => $this->userInfo['id'],
+            'result'          => $leaderResult,// 群主意见
+            'identity'        => 'admin'
+        ];
+        $userMessage  = [
+            'invitation_user' => $this->userInfo['id'],
+            'result'          => 0,// 用户意见
+            'identity'        => 'user'
+        ];
+
+        $adminImSession   = Common::imSession('service', $info['leader'], 2);
+        $adminSessionInfo = Common::sessionInfo($adminImSession['id'], $this->userInfo['id']);
+        $invitationUser   = Common::getImUserInfo($this->userInfo['id']); // 邀请人
+
+        $sendInvitationNumber = 0;
+        foreach ($pickuser as $key => $value) {
+            // 检查是否已经是群成员
+            if (Db::name('fastim_group_user')
+                ->where('group_id', $data['id'])
+                ->where('user_id', $value)
+                ->value('user_id')) {
+                continue;
+            }
+
+            // 将以前给此用户和管理员发送过的邀请消息设置为失效
+            $historyMessage    = Db::name('fastim_message')
+                ->where('group_id', $data['id'])
+                ->where('type', 'group_invitation')
+                ->where('recipient_id=:invited_user OR recipient_id=:group_chat_leader')
+                ->bind([
+                    'invited_user'      => $value,
+                    'group_chat_leader' => $info['leader']
+                ])
+                ->select();
+            $reloadSessionList = false;// 若有消息失效,直接更新会话列表
+            foreach ($historyMessage as $hkey => $hvalue) {
+                $hvalue['message'] = json_decode($hvalue['message'], true);
+                if ($hvalue['message']['invited_user'] == $value && $hvalue['message']['result'] == 0) {
+                    $hvalue['message']['result'] = 3; // 失效
+                    $hvalue['message']           = json_encode($hvalue['message']);
+                    Db::name('fastim_message')->where('id', $hvalue['id'])->update(['message' => $hvalue['message']]);
+                    $reloadSessionList = true;
+                }
+            }
+
+            $invitedUser                 = Common::getImUserInfo($value); // 被邀请人
+            $userMessage['invited_user'] = $adminMessage['invited_user'] = $value;
+
+            // 管理员消息
+            $adminMessageData = Common::insertMessage([
+                'type'         => 'group_invitation',
+                'group_id'     => $data['id'],
+                'session_id'   => $adminSessionInfo['id'],
+                'sender_id'    => 0,
+                'recipient_id' => $info['leader'],
+                'message'      => $adminMessage
+            ]);
+            if ($adminMessageData) {
+                $adminMessageData['invitedUser']              = $invitedUser;
+                $adminMessageData['invitationUser']           = $invitationUser;
+                $adminMessageData['message']['user_result']   = 0;
+                $adminMessageData['message']['leader_result'] = $leaderResult;
+                $adminSessionInfo['groupChatInfo']            = $info;
+                $adminSessionInfo['reloadSessionList']        = $reloadSessionList;
+                $this->swooleCommon->newMessagePush($adminMessageData, $adminSessionInfo, $info['leader']);
+            }
+
+            // 被邀请人消息
+            $userMessage['admin_message_id'] = $adminMessageData['id'];
+            $invitationUserSession           = Common::imSession('service', $value, 2);
+            $invitationUserSessionInfo       = Common::sessionInfo($invitationUserSession['id'], $this->userInfo['id']);
+            $invitationUserMessageData       = Common::insertMessage([
+                'type'         => 'group_invitation',
+                'group_id'     => $data['id'],
+                'session_id'   => $invitationUserSessionInfo['id'],
+                'sender_id'    => 0,
+                'recipient_id' => $value,
+                'message'      => $userMessage
+            ]);
+            if ($invitationUserMessageData) {
+                $invitationUserMessageData['invitedUser']              = $invitedUser;
+                $invitationUserMessageData['invitationUser']           = $invitationUser;
+                $invitationUserMessageData['message']['user_result']   = 0;
+                $invitationUserMessageData['message']['leader_result'] = $leaderResult;
+                $invitationUserSessionInfo['groupChatInfo']            = $info;
+                $invitationUserSessionInfo['reloadSessionList']        = $reloadSessionList;
+                $this->swooleCommon->newMessagePush($invitationUserMessageData, $invitationUserSessionInfo, $value);
+            }
+
+            $sendInvitationNumber++;
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'show_msg',
+            'data'  => [
+                'type' => $sendInvitationNumber > 0 ? 'success' : 'tips',
+                'msg'  => $sendInvitationNumber > 0 ? '邀请已发送~' : '邀请发送失败或你邀请的好友已经是群成员了~'
+            ]
+        ]));
+    }
+
+    public function groupChatNoticeOpt($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id', 'type'], $data)) {
+            return false;
+        }
+
+        $info = Db::name('fastim_group_chat_notice')
+            ->where('id', $data['id'])
+            ->where('deletetime', null)
+            ->find();
+        if (!$info) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '公告找不到啦~'
+            ]));
+            return false;
+        }
+        $info = Common::formatGroupChatNotice($info, $this->userInfo['id'], false);
+
+        if ($data['type'] == 'del') {
+            Db::name('fastim_group_chat_notice')
+                ->where('id', $info['id'])
+                ->update([
+                    'deletetime' => time()
+                ]);
+            // 更新公告列表
+            $this->groupChatNotice([
+                'group_id' => $info['group_id'],
+                'method'   => 'list',
+                'page'     => 1
+            ]);
+            return false;
+        } elseif ($data['type'] == 'post-edit') {
+            $info = Common::getImGroupChat($data['group_id']);
+            if ($info && $info['leader'] == $this->userInfo['id']) {
+                $uni = (isset($data['source']) && $data['source'] == 'uni-app') ? true : false;
+                unset($data['type'], $data['method'], $data['source']);
+                Db::name('fastim_group_chat_notice')
+                    ->where('id', $data['id'])
+                    ->update($data);
+
+                if ($uni) {
+                    $this->server->push($this->frame->fd, json_encode([
+                        'event' => 'show_msg',
+                        'data'  => '更新公告成功~'
+                    ]));
+                } else {
+                    // 更新公告列表
+                    $this->groupChatNotice([
+                        'group_id' => $data['group_id'],
+                        'method'   => 'list',
+                        'page'     => 1
+                    ]);
+                }
+                return true;
+            } else {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => '您没有权限操作~'
+                ]));
+                return false;
+            }
+        } elseif ($data['type'] == 'get-users') {
+            $where['l.type'] = 0;
+            $orderStr        = 'l.createtime desc';
+            if ($data['method'] == 'receipted-number') {
+                $where['l.receipt'] = 1;
+                $orderStr           = 'l.confirmtime desc';
+            }
+
+            if (!isset($data['page']) || $data['page'] == 1) {
+                $min = 0;
+            } else {
+                $min = ($data['page'] - 1) * $this->pageCount;
+            }
+
+            $users = Db::name('fastim_reading_log')
+                ->alias('l')
+                ->field('l.user_id,l.receipt,l.createtime,l.confirmtime,gu.nickname,u.id,u.avatar,u.nickname as u_nickname,fu.avatar as fu_avatar,fu.nickname as fu_nickname,a.avatar as a_avatar,a.nickname as a_nickname,f.remark')
+                ->join('fastim_group_user gu', 'gu.group_id=l.group_id AND gu.user_id=l.user_id')
+                ->join('fastim_user u', 'u.id=l.user_id')
+                ->join('user fu', 'fu.id=l.user_id', 'LEFT')
+                ->join('admin a', 'a.id=u.admin_id', 'LEFT')
+                ->join('fastim_friendship f', 'f.friend_id=u.id AND f.user_id=:user_id', 'LEFT')
+                ->where($where)
+                ->where('l.data_id', $data['id'])
+                ->bind([
+                    'user_id' => $this->userInfo['id']
+                ])
+                ->order($orderStr)
+                ->limit($min, $this->pageCount)
+                ->select();
+            foreach ($users as $key => $value) {
+                $users[$key]['nickname'] = Common::trueAttr(Common::nicknameSort([
+                    $value['remark'],
+                    $value['u_nickname'],
+                    $value['fu_nickname'],
+                    $value['a_nickname']
+                ], $value['id']));
+
+                $users[$key]['avatar'] = Common::avatarSrc([
+                    $value['avatar'],
+                    $value['fu_avatar'],
+                    $value['a_avatar']
+                ], $users[$key]['nickname']);
+
+                if ($data['method'] == 'receipted-number') {
+                    $users[$key]['createtime'] = Common::formatTimeAlong($value['confirmtime']);
+                } else {
+                    $users[$key]['createtime'] = Common::formatTimeAlong($value['createtime']);
+                }
+
+                unset($users[$key]['remark'], $users[$key]['u_nickname'], $users[$key]['fu_nickname'], $users[$key]['a_nickname'], $users[$key]['fu_avatar'], $users[$key]['a_avatar']);
+            }
+
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'group-notice-users',
+                'data'  => [
+                    'users'    => $users,
+                    'data'     => $data,
+                    'nextpage' => (count($users) < $this->pageCount) ? false : true,
+                ]
+            ]));
+            return false;
+        } elseif ($data['type'] == 'get') {
+            $info['popup'] = 1;
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'group_chat_notice_info',
+            'data'  => [
+                'info' => $info,
+                'data' => $data
+            ]
+        ]));
+    }
+
+    public function groupChatNotice($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['group_id', 'method'], $data)) {
+            return false;
+        }
+
+        $info = Common::getImGroupChat($data['group_id']);
+        if (!$info) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '群聊找不到啦~'
+            ]));
+            return false;
+        }
+
+        if ($data['method'] == 'post') {
+
+            if (!$this->swooleCommon->requiredParamCheck(['content'], $data)) {
+                return false;
+            }
+
+            if ($info['leader'] != $this->userInfo['id']) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => '您没有权限操作~'
+                ]));
+                return false;
+            }
+
+            unset($data['method']);
+            $data['publisher']  = $this->userInfo['id'];
+            $data['createtime'] = time();
+
+            // 公告入库
+            Db::startTrans();
+            try {
+                Db::name('fastim_group_chat_notice')->insert($data);
+                $data['id'] = Db::name('fastim_group_chat_notice')->getLastInsID();
+                Db::commit();
+            } catch (PDOException $e) {
+                Db::rollback();
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => $e->getMessage()
+                ]));
+                return false;
+            }
+
+            // 推送群消息
+            $this->swooleCommon->pushMessageToGroup('group_chat_notice', [
+                'group_id'     => $data['group_id'],
+                'session_id'   => 0,
+                'sender_id'    => $this->userInfo['id'],
+                'recipient_id' => 0,
+                'message'      => $data
+            ], $this->userInfo['id']);
+
+            // 插入一条当前用户的群通知阅读记录
+            Common::readGroupChatNotice($data['group_id'], $this->userInfo['id'], $data['id'], '', true);
+
+            // 更新公告列表
+            $this->groupChatNotice([
+                'group_id' => $data['group_id'],
+                'method'   => 'list',
+                'page'     => 1
+            ]);
+
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => [
+                    'msg'  => '公告发布成功~',
+                    'type' => 'success'
+                ]
+            ]));
+        } elseif ($data['method'] == 'list') {
+            if (!isset($data['page']) || $data['page'] == 1) {
+                $min = 0;
+            } else {
+                $min = ($data['page'] - 1) * $this->pageCount;
+            }
+
+            Common::readGroupChatNotice($data['group_id'], $this->userInfo['id'], 0, 'all');
+
+            $list = Db::name('fastim_group_chat_notice')
+                ->where('group_id', $data['group_id'])
+                ->where('deletetime', null)
+                ->order('top desc,createtime desc')
+                ->limit($min, $this->pageCount)
+                ->select();
+            foreach ($list as $key => $value) {
+                $list[$key] = Common::formatGroupChatNotice($value, $this->userInfo['id'], false);
+            }
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'group_chat_notice',
+                'data'  => [
+                    'data'     => $data,
+                    'list'     => $list,
+                    'isLeader' => $info['leader'] == $this->userInfo['id'] ? true : false,
+                    'nextpage' => (count($list) < $this->pageCount) ? false : true,
+                ]
+            ]));
+        }
+    }
+
+    /**
+     * at群成员
+     */
+    public function groupChatAt($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['session_id', 'method'], $data)) {
+            return;
+        }
+
+        $sessionInfo = Common::sessionInfo($data['session_id']);
+        if (!$sessionInfo) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '会话找不到啦~'
+            ]));
+            return;
+        }
+
+        if ($data['method'] == 'get-all') {
+            $info = Common::getImGroupChat($sessionInfo['chat_id']);
+            if (!$info) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => '群聊找不到啦~'
+                ]));
+                return;
+            }
+
+            $groupMember = Db::name('fastim_group_user')
+                ->alias('gu')
+                ->field('gu.nickname,gu.speaktime,gu.jointime,u.id,u.avatar,u.nickname as u_nickname,u.status,fu.avatar as fu_avatar,fu.nickname as fu_nickname,a.avatar as a_avatar,a.nickname as a_nickname,f.remark')
+                ->join('fastim_user u', 'u.id=gu.user_id')
+                ->join('user fu', 'fu.id=u.user_id', 'LEFT')
+                ->join('admin a', 'a.id=u.admin_id', 'LEFT')
+                ->join('fastim_friendship f', 'f.friend_id=u.id AND f.user_id=:user_id', 'LEFT')
+                ->bind([
+                    'user_id' => $this->userInfo['id']
+                ]);
+
+            if (isset($data['keywords']) && $data['keywords']) {
+                $groupMember = $groupMember->where('gu.group_id', $info['id'])
+                    ->where("gu.nickname LIKE :keywords OR u.nickname LIKE :keywords OR fu.nickname LIKE :keywords OR a.nickname LIKE :keywords OR remark LIKE :keywords")
+                    ->bind([
+                        'keywords' => '%' . $data['keywords'] . '%'
+                    ])
+                    ->order('gu.speaktime desc,gu.jointime desc')
+                    ->select(false);
+                $groupMember = Db::query($groupMember);
+            } else {
+                $groupMember = $groupMember->where('gu.group_id', $info['id'])->order('gu.speaktime desc,gu.jointime desc');
+                $groupMember = $groupMember->select();
+            }
+
+            foreach ($groupMember as $key => $value) {
+                // 去除当前用户(不在查询中使用!=)
+                if ($value['id'] == $this->userInfo['id']) {
+                    unset($groupMember[$key]);
+                    continue;
+                }
+                // 原始昵称
+                $groupMember[$key]['nickname'] = Common::trueAttr(Common::nicknameSort([
+                    $value['nickname'],
+                    $value['u_nickname'],
+                    $value['fu_nickname'],
+                    $value['a_nickname']
+                ], $value['id']));
+
+                $groupMember[$key]['avatar'] = Common::avatarSrc([
+                    $value['avatar'],
+                    $value['fu_avatar'],
+                    $value['a_avatar']
+                ], $groupMember[$key]['nickname']);
+
+                unset($groupMember[$key]['u_nickname'], $groupMember[$key]['fu_nickname'], $groupMember[$key]['a_nickname'], $groupMember[$key]['fu_avatar'], $groupMember[$key]['a_avatar']);
+            }
+
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'group_chat_at',
+                'data'  => [
+                    'data'         => $data,
+                    'group_member' => array_values($groupMember)
+                ]
+            ]));
+        }
+    }
+
+    /**
+     * 群成员列表
+     */
+    public function groupChatMember($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id', 'method'], $data)) {
+            return;
+        }
+
+        $info = Common::getImGroupChat($data['id']);
+        if (!$info) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '群聊找不到啦~'
+            ]));
+            return;
+        }
+
+        // 保存群聊昵称
+        if ($data['method'] == 'save-groupmember-nickname') {
+            if (!$this->swooleCommon->requiredParamCheck(['new_nickname'], $data, false, ['new_nickname' => '请输入正确的昵称~'])) {
+                return;
+            }
+            Db::name('fastim_group_user')
+                ->where('group_id', $data['id'])
+                ->where('user_id', $this->userInfo['id'])
+                ->update([
+                    'nickname' => $data['new_nickname']
+                ]);
+        }
+
+        $groupMember = Db::name('fastim_group_user')
+            ->alias('gu')
+            ->field('gu.nickname,gu.speaktime,gu.jointime,u.id,u.avatar,u.nickname as u_nickname,u.status,fu.avatar as fu_avatar,fu.nickname as fu_nickname,a.avatar as a_avatar,a.nickname as a_nickname,f.remark,f.user_id as is_friend')
+            ->join('fastim_user u', 'u.id=gu.user_id')
+            ->join('user fu', 'fu.id=u.user_id', 'LEFT')
+            ->join('admin a', 'a.id=u.admin_id', 'LEFT')
+            ->join('fastim_friendship f', 'f.friend_id=u.id AND f.user_id=:user_id', 'LEFT')
+            ->bind([
+                'user_id' => $this->userInfo['id']
+            ]);
+
+        if (isset($data['keywords']) && $data['keywords']) {
+            $groupMember = $groupMember->where('gu.group_id', $data['id'])
+                ->where("gu.nickname LIKE :keywords OR u.nickname LIKE :keywords OR fu.nickname LIKE :keywords OR a.nickname LIKE :keywords OR remark LIKE :keywords")
+                ->bind([
+                    'keywords' => '%' . $data['keywords'] . '%'
+                ])
+                ->select(false);
+            $groupMember = Db::query($groupMember);
+        } else {
+            $groupMember = $groupMember->where('gu.group_id', $data['id'])->order('gu.jointime desc');
+            if (isset($data['limit'])) {
+                $groupMember->limit($data['limit']);
+            }
+            $groupMember = $groupMember->select();
+        }
+
+        foreach ($groupMember as $key => $value) {
+            // 原始昵称
+            $groupMember[$key]['nickname_origin'] = Common::trueAttr(Common::nicknameSort([
+                $value['remark'],
+                $value['u_nickname'],
+                $value['fu_nickname'],
+                $value['a_nickname']
+            ], $value['id']));
+
+            $groupMember[$key]['avatar'] = Common::avatarSrc([
+                $value['avatar'],
+                $value['fu_avatar'],
+                $value['a_avatar']
+            ], $groupMember[$key]['nickname_origin']);
+
+            if ($value['id'] == $this->userInfo['id']) {
+                $groupMember[$key]['is_friend'] = true;
+            }
+
+            $groupMember[$key]['speaktime'] = Common::formatTime($value['speaktime'] ? $value['speaktime'] : $value['jointime']);
+            unset($groupMember[$key]['remark'], $groupMember[$key]['u_nickname'], $groupMember[$key]['fu_nickname'], $groupMember[$key]['a_nickname'], $groupMember[$key]['fu_avatar'], $groupMember[$key]['a_avatar']);
+        }
+
+        if (isset($data['lettersort']) && (!isset($data['keywords']) || !$data['keywords'])) {
+            // 按字母排序
+
+            // 剔除群主
+            $leader = [];
+            foreach ($groupMember as $key => $value) {
+                if ($value['id'] == $info['leader']) {
+                    $leader = $value;
+                    unset($groupMember[$key]);
+                    break;
+                }
+            }
+            $groupMember           = Common::initialPinyinArrSort($groupMember, 'nickname_origin');
+            $groupMember['leader'] = $leader;
+        } elseif (!isset($data['keywords']) || !$data['keywords']) {
+            // 当前用户排第一,群主排第二
+            // 直接返回
+            foreach ($groupMember as $key => $value) {
+                if ($value['id'] == $this->userInfo['id'] && $key > 0) {
+                    $tempUser          = $groupMember[0];
+                    $groupMember[0]    = $groupMember[$key];
+                    $groupMember[$key] = $tempUser;
+                }
+
+                if (($value['id'] == $info['leader'] && $value['id'] != $this->userInfo['id']) && $key != 1) {
+                    $tempUser          = $groupMember[1];
+                    $groupMember[1]    = $groupMember[$key];
+                    $groupMember[$key] = $tempUser;
+                }
+            }
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'group_chat_member',
+            'data'  => [
+                'data'         => $data,
+                'group_member' => $groupMember,
+                'info'         => $info
+            ]
+        ]));
+    }
+
+    /**
+     * 标记一个会话的消息已读
+     */
+    public function readMessage($data)
+    {
+        if (!isset($data['session_id']) || !$data['session_id']) {
+            return;
+        }
+
+        $sessionInfo = Common::sessionInfo($data['session_id'], $this->userInfo['id']);
+
+        if ($sessionInfo['type'] == 'single') {
+            Db::name('fastim_message')
+                ->where('session_id', $data['session_id'])
+                ->where('recipient_id', $this->userInfo['id'])
+                ->where('status', 0)
+                ->update(['status' => 1]);
+
+            // 推送已读消息给发送人
+            $this->swooleCommon->pushMessageToUser($sessionInfo['sessionUser']['id'], [
+                'event' => 'read_message',
+                'data'  => [
+                    'sessionType' => 'single',
+                    'session_id'  => $data['session_id']
+                ]
+            ]);
+        } elseif (isset($data['type']) && $data['type'] == 'group_chat_notice') {
+            Common::readGroupChatNotice($sessionInfo['chat_id'], $this->userInfo['id'], $data['id'], $data['action']);
+        } elseif ($sessionInfo['type'] == 'group') {
+            // 群聊消息阅读
+            $this->swooleCommon->readGroupMessage($sessionInfo);
+        } elseif ($sessionInfo['type'] == 'service') {
+            // 服务号消息阅读
+            Common::readServiceMessage($sessionInfo['chat_id'], $this->userInfo['id'], $sessionInfo['id']);
+        }
+
+    }
+
+    public function receiptGroupChatNotice($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id', 'session_id'], $data)) {
+            return false;
+        }
+
+        $sessionInfo = Common::sessionInfo($data['session_id'], $this->userInfo['id']);
+
+        $log = Db::name('fastim_reading_log')
+            ->where('type', 0)
+            ->where('group_id', $sessionInfo['chat_id'])
+            ->where('user_id', $this->userInfo['id'])
+            ->where('data_id', $data['id'])
+            ->find();
+        if ($log) {
+            Db::name('fastim_reading_log')
+                ->where('id', $log['id'])
+                ->update([
+                    'receipt'     => 1,
+                    'confirmtime' => time()
+                ]);
+
+            $receiptedCount = Db::name('fastim_reading_log')
+                ->where('type', 0)
+                ->where('group_id', $sessionInfo['chat_id'])
+                ->where('data_id', $data['id'])
+                ->where('receipt', 1)
+                ->count();
+
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'receipt_group_chat_notice',
+                'data'  => [
+                    'msg'            => '确认成功~',
+                    'receiptedCount' => $receiptedCount,
+                    'data'           => $data
+                ]
+            ]));
+        }
+    }
+
+    /**
+     * 创建或打开与指定用户的会话窗口
+     */
+    public function openSession($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id', 'type'], $data, false, [
+            'id'   => '用户找不到啦~',
+            'type' => '会话创建失败,请重试~'
+        ])) {
+            return;
+        }
+
+        if ($data['type'] == 'user') {
+            // 是否是好友或对方是否允许临时会话
+            $friendship  = Db::name('fastim_friendship')
+                ->where('user_id', $data['id'])
+                ->where('friend_id', $this->userInfo['id'])
+                ->find();
+            $tempSession = Common::imUserConfig($data['id'], 'temp_session');
+
+            if (!$friendship && !$tempSession) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => [
+                        'msg'  => '对方拒绝会话,请添加对方为好友再试~',
+                        'type' => 'error'
+                    ]
+                ]));
+                return;
+            }
+
+            $imSession = Common::imSession('single', $this->userInfo['id'], $data['id']);
+        } elseif ($data['type'] == 'group') {
+            $groupMember = Db::name('fastim_group_user')
+                ->where('group_id', $data['id'])
+                ->where('user_id', $this->userInfo['id'])
+                ->value('group_id');
+            if (!$groupMember) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => [
+                        'msg'  => '请先加入群聊再发送消息~',
+                        'type' => 'error'
+                    ]
+                ]));
+                return;
+            }
+            $imSession = Common::imSession('group', $this->userInfo['id'], $data['id']);
+        } elseif ($data['type'] == 'service') {
+            $imSession = Common::imSession('service', $this->userInfo['id'], $data['id']);
+        }
+
+        if (!$imSession) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => [
+                    'msg'  => '会话创建失败,请重试~',
+                    'type' => 'error'
+                ]
+            ]));
+            return;
+        }
+
+        $sessionInfo = Common::sessionInfo($imSession['id'], $this->userInfo['id']);
+        if (!$sessionInfo) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => [
+                    'msg'  => '会话创建失败,请重试~',
+                    'type' => 'error'
+                ]
+            ]));
+            return;
+        }
+
+        // 客服代表的欢迎消息发送
+        if ($data['type'] == 'user' && $sessionInfo['sessionUser']['type'] == 'csr' && isset($imSession['create']) && $imSession['create']) {
+            $welcomeNewNserMsg = $sessionInfo['sessionUser']['welcome_msg'] ? $sessionInfo['sessionUser']['welcome_msg'] : Db::name('fastim_config')
+                ->where('name', 'welcome_new_user_msg')
+                ->value('value');
+
+            // 消息入库
+            $messageData = Common::insertMessage([
+                'type'         => 'default',
+                'session_id'   => $sessionInfo['id'],
+                'sender_id'    => $sessionInfo['sessionUser']['id'],
+                'recipient_id' => $this->userInfo['id'],
+                'message'      => $welcomeNewNserMsg
+            ]);
+        }
+
+        // 兼容前台imSession
+        $sessionInfo['pushUser'] = $sessionInfo['sessionUser'];
+        unset($sessionInfo['sessionUser']);
+
+        // 置顶状态
+        $sessionInfo['top'] = Db::name('fastim_session_top')
+            ->where('user_id', $this->userInfo['id'])
+            ->where('session_id', $imSession['id'])
+            ->value('top');
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'open_session',
+            'data'  => [
+                'sessionInfo'          => $sessionInfo,
+                'lastMessage'          => Common::getLastMessage($this->userInfo['id'], $sessionInfo),
+                'unreadMessagesNumber' => 0,
+                'shield'               => ($sessionInfo['type'] == 'group') ? Common::getGroupChatShield($sessionInfo['pushUser']['id'], $this->userInfo['id']) : false
+            ]
+        ]));
+    }
+
+    /**
+     * 创建群聊
+     */
+    public function createGroup($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['data', 'pickuser'], $data, false, [
+            'data'     => '资料错误,请重试~',
+            'pickuser' => '请选择群成员!'
+        ])) {
+            return;
+        }
+
+        if (!$this->swooleCommon->requiredParamCheck(['nickname'], $data['data'], false, ['nickname' => '请输入正确的群名称!'])) {
+            return;
+        }
+
+        $pickuser = explode(',', trim($data['pickuser'], ','));
+        if (!$pickuser) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '请选择群成员!'
+            ]));
+            return;
+        }
+
+        $pickuser[] = $this->userInfo['id'];
+        $pickuser   = array_unique($pickuser);
+
+        unset($data['data']['id']);
+        unset($data['data']['method']);
+        $data['data']['max_user_count'] = (isset($data['data']['max_user_count']) && $data['data']['max_user_count']) ? $data['data']['max_user_count'] : CommonCode::$groupMaxUser;
+        $data['data']['leader']         = $this->userInfo['id'];
+        $data['data']['createtime']     = time();
+
+        // 群聊数据入库
+        Db::startTrans();
+        try {
+            Db::name('fastim_group_chat')->insert($data['data']);
+            $groupId = Db::name('fastim_group_chat')->getLastInsID();
+            // 群成员入库
+            foreach ($pickuser as $key => $value) {
+                $imSession   = Common::imSession('group', $value, $groupId);
+                $groupUser[] = [
+                    'group_id'   => $groupId,
+                    'user_id'    => $value,
+                    'session_id' => $imSession['id'],
+                    'jointime'   => time()
+                ];
+            }
+            Db::name('fastim_group_user')->insertAll($groupUser);
+
+            // 推送群消息
+            $this->swooleCommon->pushMessageToGroup('system', [
+                'group_id'     => $groupId,
+                'session_id'   => 0,
+                'sender_id'    => $this->userInfo['id'],
+                'recipient_id' => 0,
+                'message'      => [
+                    'message'      => '你已经是群成员了,和大家打个招呼吧~',
+                    'display_user' => 'all'
+                ]
+            ]);
+
+            Db::commit();
+        } catch (PDOException $e) {
+            Db::rollback();
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => $e->getMessage()
+            ]));
+            return;
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'create_group',
+            'data'  => $groupId
+        ]));
+    }
+
+    public function loadKbs($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id'], $data)) {
+            return;
+        }
+        $kbs = Common::getKbs('', $data['id']);
+        if (!$kbs) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => [
+                    'msg'  => '知识点找不到啦,请重试~',
+                    'type' => 'error'
+                ]
+            ]));
+            return;
+        }
+
+        // 消息入库
+        $imSession   = Common::imSession('service', $this->userInfo['id'], 3);
+        $messageData = Common::insertMessage([
+            'type'         => 'default',
+            'session_id'   => $imSession['id'],
+            'sender_id'    => 0,
+            'recipient_id' => $this->userInfo['id'],
+            'message'      => $kbs['content']
+        ]);
+        if ($messageData) {
+            $sessionInfo = Common::sessionInfo($imSession['id'], $this->userInfo['id']);
+            $this->swooleCommon->newMessagePush($messageData, $sessionInfo, $this->userInfo['id']);
+        }
+    }
+
+    /**
+     * 客服分配
+     * @param array $array 待排序数组
+     * @return void
+     */
+    public function distributionCsr($data)
+    {
+        // 当前用户和`智能客服`的会话
+        $imSession   = Common::imSession('service', $this->userInfo['id'], 3);
+        $sessionInfo = Common::sessionInfo($imSession['id'], $this->userInfo['id']);
+
+        $where['u.type']   = 'csr';
+        $where['u.status'] = 1;// 只获取在线的客服代表
+        if (isset($data['group_id']) && $data['group_id']) {
+            $where['u.group_id'] = $data['group_id'];
+        }
+
+        $distributionType = Db::name('fastim_config')->where('name', 'distribution_type')->value('value');
+
+        $sysTempSession = Db::name('fastim_config')->where('name', 'temp_session')->value('value');
+
+        if ($distributionType == 0) {
+            // 轮训自动分配
+            $csr = Db::name('fastim_user')
+                ->alias('u')
+                ->field('u.*, uc.value')
+                ->join('fastim_user_config uc', "uc.user_id=u.id AND uc.name='temp_session'", 'LEFT')
+                ->where($where)
+                ->where(function ($query) use ($sysTempSession) {
+                    if ($sysTempSession == 1) {
+                        $query->where("uc.value IS NULL OR uc.value=1");
+                    } else {
+                        $query->where("uc.value=1");
+                    }
+                })
+                ->order('u.last_reception_time asc')
+                ->find();
+        } elseif ($distributionType == 1) {
+            // 按工作量分配
+            $receptionCount = Db::name('fastim_user')
+                ->alias('u')
+                ->field('u.*, uc.value')
+                ->join('fastim_user_config uc', "uc.user_id=u.id AND uc.name='temp_session'", 'LEFT')
+                ->where($where)
+                ->where(function ($query) use ($sysTempSession) {
+                    if ($sysTempSession == 1) {
+                        $query->where("uc.value IS NULL OR uc.value=1");
+                    } else {
+                        $query->where("uc.value=1");
+                    }
+                })
+                ->min('u.reception_count');
+
+            $csr = Db::name('fastim_user')
+                ->alias('u')
+                ->field('u.*, uc.value')
+                ->join('fastim_user_config uc', "uc.user_id=u.id AND uc.name='temp_session'", 'LEFT')
+                ->where($where)
+                ->where(function ($query) use ($sysTempSession) {
+                    if ($sysTempSession == 1) {
+                        $query->where("uc.value IS NULL OR uc.value=1");
+                    } else {
+                        $query->where("uc.value=1");
+                    }
+                })
+                ->where('u.reception_count', $receptionCount)
+                ->order('u.last_reception_time asc')
+                ->find();
+        } elseif ($distributionType == 2) {
+            // 用户自行选择
+            $csrs = Db::name('fastim_user')
+                ->alias('u')
+                ->field('u.id,u.nickname,u.status,fu.nickname as fu_nickname,a.nickname as a_nickname,g.name,g.id as gid, uc.value')
+                ->join('user fu', 'u.user_id=fu.id', 'LEFT')
+                ->join('admin a', 'u.admin_id=a.id', 'LEFT')
+                ->join('fastim_csr_group g', 'u.group_id=g.id', 'LEFT')
+                ->join('fastim_user_config uc', "uc.user_id=u.id AND uc.name='temp_session'", 'LEFT')
+                ->where($where)
+                ->where(function ($query) use ($sysTempSession) {
+                    if ($sysTempSession == 1) {
+                        $query->where("uc.value IS NULL OR uc.value=1");
+                    } else {
+                        $query->where("uc.value=1");
+                    }
+                })
+                ->order('u.last_reception_time asc')
+                ->select();
+
+            if (!$csrs && (isset($data['no_csr_tip']) && !$data['no_csr_tip'])) {
+                return;// 来自知识库的调用,由于无客服代表在线,不提示联系人工
+            }
+
+            $csrsGroup = [];
+            foreach ($csrs as $key => $value) {
+                $csrs[$key]['nickname'] = Common::trueAttr(Common::nicknameSort([
+                    $csrs[$key]['nickname'],
+                    $csrs[$key]['fu_nickname'],
+                    $csrs[$key]['a_nickname']
+                ], $csrs[$key]['id']));
+
+                unset($csrs[$key]['a_nickname'], $csrs[$key]['fu_nickname']);
+
+                if (array_key_exists($value['gid'], $csrsGroup)) {
+                    $csrsGroup[$value['gid']]['csrs'][] = $csrs[$key];
+                } else {
+                    $csrsGroup[$value['gid']]['name']   = $value['name'];
+                    $csrsGroup[$value['gid']]['csrs'][] = $csrs[$key];
+                }
+            }
+            $csrsGroup = array_values($csrsGroup);
+
+            $pushSendUserMessage = [
+                'type'  => 'csrs',
+                'csrs'  => $csrsGroup,
+                'title' => $csrsGroup ? CommonCode::$kbsManualCSR : Db::name('fastim_config')
+                    ->where('name', 'no_csr_tip')
+                    ->value('value')
+            ];
+
+            $kbsMessageData = Common::insertMessage([
+                'type'         => 'kbs_list',
+                'session_id'   => $sessionInfo['id'],
+                'sender_id'    => 0,
+                'recipient_id' => $this->userInfo['id'],
+                'message'      => $pushSendUserMessage
+            ]);
+            $this->swooleCommon->newMessagePush($kbsMessageData, $sessionInfo, $this->userInfo['id']);
+            return;
+        }
+
+        if (!$csr) {
+
+            $onlineCSRCount = Db::name('fastim_user')
+                ->alias('u')
+                ->field('u.id, uc.value')
+                ->join('fastim_user_config uc', "uc.user_id=u.id AND uc.name='temp_session'", 'LEFT')
+                ->where('u.type', 'csr')
+                ->where('u.status', 1)
+                ->where(function ($query) use ($sysTempSession) {
+                    if ($sysTempSession == 1) {
+                        $query->where("uc.value IS NULL OR uc.value=1");
+                    } else {
+                        $query->where("uc.value=1");
+                    }
+                })
+                ->count('u.id');
+
+            if (!$onlineCSRCount) {
+                // 一个在线客服也没有
+                $pushMessage = Db::name('fastim_config')->where('name', 'no_csr_tip')->value('value');
+            } elseif (isset($data['group_id']) && $data['group_id']) {
+                $pushMessage = '抱歉,您选择的分组内没有找到在线的客服代表~';
+            } else {
+                $pushMessage = '客服代表分配失败,请重试~';
+            }
+
+            $kbsMessageData = Common::insertMessage([
+                'type'         => 'default',
+                'session_id'   => $sessionInfo['id'],
+                'sender_id'    => 0,
+                'recipient_id' => $this->userInfo['id'],
+                'message'      => $pushMessage
+            ]);
+            $this->swooleCommon->newMessagePush($kbsMessageData, $sessionInfo, $this->userInfo['id']);
+            return;
+        }
+
+        $this->openSession([
+            'id'   => $csr['id'],
+            'type' => 'user'
+        ]);
+    }
+
+    /**
+     * 发送消息
+     */
+    public function sendMessage($data)
+    {
+        $paramCheck = $this->swooleCommon->requiredParamCheck([
+            'session_id',
+            'message',
+            'tokens',
+            'identity'
+        ], $data, false, [
+            'session_id' => '会话找不到啦~',
+            'message'    => '请输入消息内容~'
+        ]);
+        if (!$paramCheck) {
+            return false;
+        }
+
+        // 复检用户登录态是否注销
+        if (isset($data['tokens']['fastim_token']) && ($data['identity'] == 'admin' || $data['identity'] == 'fauser')) {
+            // 管理员或用户
+            $userInfo = $data['identity'] == 'fauser' ? Common::checkFaUser($data['tokens']['fastim_token']) : Common::checkAdmin($data['tokens']['fastim_token']);
+        } elseif (isset($data['tokens']['im_tourists_token']) && $data['identity'] == 'imuser') {
+            // imuser
+            $userInfo = Common::checkIMUser($data['tokens']['im_tourists_token']);
+        }
+        if (!$userInfo) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '登录已失效,请重新登录~'
+            ]));
+            return false;
+        }
+
+
+        $messageStatus = 0;
+        $data['type']  = $data['type'] ?? 'default';
+        $sessionInfo   = Common::sessionInfo($data['session_id'], $this->userInfo['id']);
+
+        if ($sessionInfo['type'] == 'single') {
+
+            // 通知输入状态结束
+            $this->swooleCommon->pushMessageToUser($sessionInfo['sessionUser']['id'], [
+                'event' => 'input_status',
+                'data'  => [
+                    'session_id' => $data['session_id'],
+                    'status'     => 'end'
+                ]
+            ]);
+
+            // 客服代表随时可以发送消息
+            if ($sessionInfo['user']['type'] != 'csr') {
+                // 检查接受人
+                $friendship    = Db::name('fastim_friendship')
+                    ->where('friend_id', $this->userInfo['id'])
+                    ->where('user_id', $sessionInfo['sessionUser']['id'])
+                    ->find();
+                $friendshipTwo = Db::name('fastim_friendship')
+                    ->where('user_id', $this->userInfo['id'])
+                    ->where('friend_id', $sessionInfo['sessionUser']['id'])
+                    ->find();
+                $tempSession   = Common::imUserConfig($sessionInfo['sessionUser']['id'], 'temp_session');
+                if ((!$friendship || !$friendshipTwo) && !$tempSession) {
+                    $this->server->push($this->frame->fd, json_encode([
+                        'event' => 'show_msg',
+                        'data'  => [
+                            'msg'  => '需要好友关系才能互相发送消息哦~',
+                            'type' => 'error'
+                        ]
+                    ]));
+
+                    $messageStatus = 4; // 消息发送失败
+                }
+            }
+
+            // 检查屏蔽状态
+            $shield = Db::name('fastim_user_shield')
+                ->where('user_id', $sessionInfo['sessionUser']['id'])
+                ->where('shield_id', $sessionInfo['user']['id'])
+                ->value('id');
+            if ($shield) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => [
+                        'msg'  => '消息被对方拒收了~',
+                        'type' => 'error'
+                    ]
+                ]));
+
+                $messageStatus = 4; // 消息发送失败
+            }
+
+            // 消息入库
+            $messageData = Common::insertMessage([
+                'type'         => $data['type'],
+                'session_id'   => $sessionInfo['id'],
+                'sender_id'    => $this->userInfo['id'],
+                'recipient_id' => $sessionInfo['sessionUser']['id'],
+                'message'      => $data['message'],
+                'status'       => $messageStatus
+            ]);
+
+            if ($messageData) {
+                if ($sessionInfo['sessionUser']['status'] == '忙碌') {
+                    // 取得忙碌时的自动回复
+                    $busyReply = Common::imUserConfig($sessionInfo['sessionUser']['id'], 'busy_reply');
+                    if ($busyReply) {
+                        $busyReply = CommonCode::$busyReplyPrefix . $busyReply;
+
+                        // 自动回复消息入库
+                        $busyReplyData = Common::insertMessage([
+                            'type'         => 'default',
+                            'session_id'   => $sessionInfo['id'],
+                            'sender_id'    => $sessionInfo['sessionUser']['id'],
+                            'recipient_id' => $this->userInfo['id'],
+                            'message'      => $busyReply
+                        ]);
+                        if ($busyReplyData) {
+                            $this->swooleCommon->newMessagePush($busyReplyData, $sessionInfo, $this->userInfo['id']);
+                        }
+
+                        // 通知发送自动回复的用户更新消息记录
+                        $this->swooleCommon->pushMessageToUser($sessionInfo['sessionUser']['id'], [
+                            'event' => 'reload_session',
+                            'data'  => [
+                                'session_id' => $data['session_id']
+                            ]
+                        ]);
+                    }
+                }
+
+                // 推送新消息给接受人
+                if ($messageData['status'] != 4) {
+                    $sessionInfo['pushUser'] = Common::getImUserInfo($this->userInfo['id'], true, $sessionInfo['sessionUser']['id']);
+                    $this->swooleCommon->newMessagePush($messageData, $sessionInfo, $sessionInfo['sessionUser']['id']);
+
+                    /**
+                     * sendToYourself:是否是发送给自己的
+                     * 发送给自己的任然需推送给自己的其他在线设备
+                     * 此处做标记以在前端删除可能存在的原消息:`message_id`
+                     */
+                    $this->server->push($this->frame->fd, json_encode([
+                        'event' => 'send_message',
+                        'data'  => [
+                            'message_id'     => $data['message_id'],
+                            'new_message_id' => $messageData['id'],
+                            'status'         => 0,
+                            'sendToYourself' => ($this->userInfo['id'] == $sessionInfo['sessionUser']['id']) ? true : false
+                        ]
+                    ]));
+                }
+            } else {
+                $messageStatus = 4;
+            }
+
+            if ($messageStatus == 4) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'send_message',
+                    'data'  => [
+                        'message_id' => $data['message_id'],
+                        'status'     => 4
+                    ]
+                ]));
+            }
+        }
+        elseif ($sessionInfo['type'] == 'group') {
+            if ($sessionInfo['sessionUser']['speak'] == 1 && $sessionInfo['sessionUser']['leader'] != $this->userInfo['id']) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => [
+                        'msg'  => '管理员已禁止发言~',
+                        'type' => 'error'
+                    ]
+                ]));
+            }
+
+            // 推送群消息
+            $messageData = $this->swooleCommon->pushMessageToGroup($data['type'], [
+                'group_id'   => $sessionInfo['chat_id'],
+                'session_id' => $sessionInfo['id'],
+                'sender_id'  => $this->userInfo['id'],
+                'message'    => $data['message']
+            ], $this->userInfo['id'], $data['message_id'], isset($data['at_users']) ? $data['at_users'] : []);
+
+            if (!$messageData) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'send_message',
+                    'data'  => [
+                        'message_id' => $data['message_id'],
+                        'status'     => 4
+                    ]
+                ]));
+            }
+        }
+        elseif ($sessionInfo['type'] == 'service' && $sessionInfo['chat_id'] == 3) {
+            // 用户给智能客服发送消息
+
+            // 消息入库
+            $messageData = Common::insertMessage([
+                'type'         => $data['type'],
+                'session_id'   => $sessionInfo['id'],
+                'sender_id'    => $this->userInfo['id'],
+                'recipient_id' => $sessionInfo['user_two'],
+                'message'      => $data['message'],
+                'status'       => 1
+            ]);
+
+            // 知识库阅读了此消息
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'send_message',
+                'data'  => [
+                    'message_id'     => $data['message_id'],
+                    'new_message_id' => $messageData['id'] ?? 0,
+                    'status'         => $messageData ? $messageData['status'] : 4
+                ]
+            ]));
+
+            // 搜索知识库并自动回复-区分有收录和无收录,供前端做不同的显示
+            if (in_array($data['message'], CommonCode::$manualCSRKeywords)) {
+                $kbs       = false;
+                $manualCSR = true;
+            } else {
+                $kbs       = Common::getKbs($data['message'], 0);
+                $manualCSR = false;
+            }
+
+            if ($kbs) {
+                $pushSendUserMessage = [
+                    'title' => CommonCode::$kbsIncluded,
+                    'type'  => 'included',
+                    'kbs'   => $kbs
+                ];
+            } else {
+                $distributionType = Db::name('fastim_config')->where('name', 'distribution_type')->value('value');
+
+                if ($distributionType == 2) {
+                    // 提示用户选择人工客服代表-带客服分组
+                    if (!$manualCSR) {
+                        $kbsMessageData = Common::insertMessage([
+                            'type'         => 'default',
+                            'session_id'   => $sessionInfo['id'],
+                            'sender_id'    => 0,
+                            'recipient_id' => $this->userInfo['id'],
+                            'message'      => CommonCode::$kbsNotIncluded
+                        ]);
+                        $this->swooleCommon->newMessagePush($kbsMessageData, $sessionInfo, $this->userInfo['id']);
+                    }
+
+                    return $this->distributionCsr([
+                        'no_csr_tip' => $manualCSR
+                    ]);
+                }
+
+                $distributionGroupSelect = Db::name('fastim_config')
+                    ->where('name', 'distribution_group_select')
+                    ->value('value');
+
+                $kbsNotIncludedTip = (!$manualCSR ? CommonCode::$kbsNotIncluded : '') . CommonCode::$kbsManualCSR;
+
+                if ($distributionGroupSelect == 0) {
+                    // 选择人工客服-先选择客服分组
+                    $dbprefix       = config('database.prefix');
+                    $sysTempSession = Db::name('fastim_config')->where('name', 'temp_session')->value('value');
+                    $tempSessionSql = 'AND (' . ($sysTempSession == 1 ? 'uc.value IS NULL OR uc.value=1' : 'uc.value=1') . ')';
+
+                    $groupCsrCountSql = "(SELECT count('u.id') FROM `{$dbprefix}fastim_user` `u` LEFT JOIN `{$dbprefix}fastim_user_config` `uc` ON `uc`.`user_id`=`u`.`id` AND `uc`.`name`='temp_session' WHERE u.type='csr' AND u.status=1 AND u.group_id=g.id {$tempSessionSql})";
+
+                    $csrGroup = Db::name('fastim_csr_group')
+                        ->alias('g')
+                        ->field('g.id,g.name,' . $groupCsrCountSql . ' as csr_count')
+                        ->where('g.status', '1')
+                        ->where('g.deletetime', null)
+                        ->order('g.weigh desc')
+                        ->select();
+
+                    $pushSendUserMessage = [
+                        'type'      => 'distribution-group-select',
+                        'title'     => $kbsNotIncludedTip,
+                        'csr_group' => $csrGroup
+                    ];
+                } elseif ($distributionGroupSelect == 1) {
+                    // 直接分配-提示用户是否接入人工
+                    $pushSendUserMessage = [
+                        'type'  => 'not-included',
+                        'title' => $kbsNotIncludedTip
+                    ];
+                }
+            }
+
+            $kbsMessageData = Common::insertMessage([
+                'type'         => 'kbs_list',
+                'session_id'   => $sessionInfo['id'],
+                'sender_id'    => 0,
+                'recipient_id' => $this->userInfo['id'],
+                'message'      => $pushSendUserMessage
+            ]);
+            if ($kbsMessageData) {
+                // 推送新消息给接受人
+                $this->swooleCommon->newMessagePush($kbsMessageData, $sessionInfo, $this->userInfo['id']);
+            }
+        }
+    }
+}

+ 2343 - 0
addons/fastim/library/controller/User.php

@@ -0,0 +1,2343 @@
+<?php
+
+use think\Db;
+use addons\fastim\library\Common;
+use addons\fastim\library\CommonCode;
+use addons\fastim\library\controller\Base;
+use think\exception\PDOException;
+
+/**
+ * im用户相关
+ */
+class User extends Base
+{
+    protected $noNeedLogin = [];
+
+
+    public function pushCid($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['type', 'clientid', 'platform'], $data)) {
+            return false;
+        }
+
+        if ($data['type'] == 'logout') {
+            Db::name('fastim_user_push_clientid')
+                ->where('user_id', $this->userInfo['id'])
+                ->where('clientid', $data['clientid'])
+                ->where('platform', $data['platform'])
+                ->delete();
+
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'logout',
+                'data'  => []
+            ]));
+            return false;
+        }
+
+        $userClientValueId = Db::name('fastim_user_push_clientid')
+            ->where('user_id', $this->userInfo['id'])
+            ->where('clientid', $data['clientid'])
+            ->where('platform', $data['platform'])
+            ->value('id');
+        if ($userClientValueId) {
+            Db::name('fastim_user_push_clientid')->where('id', $userClientValueId)->update([
+                'updatetime' => time()
+            ]);
+        } else {
+            Db::name('fastim_user_push_clientid')->insert([
+                'user_id'    => $this->userInfo['id'],
+                'clientid'   => $data['clientid'],
+                'platform'   => $data['platform'],
+                'updatetime' => time()
+            ]);
+        }
+    }
+
+    /**
+     * 标记语音消息已播放(非已读)
+     */
+    public function changeVoiceStatus($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id'], $data)) {
+            return;
+        }
+
+        // 只简单确认是否是真实语音的消息即可
+        $message = Db::name('fastim_message')
+            ->where('id', $data['id'])
+            ->where('type', 'voice')
+            ->value('id');
+        if ($message) {
+            Db::name('fastim_reading_log')
+                ->insert([
+                    'type'       => 1,
+                    'user_id'    => $this->userInfo['id'],
+                    'data_id'    => $data['id'],
+                    'createtime' => time()
+                ]);
+        }
+    }
+
+    public function userShield($data)
+    {
+        if (!isset($data['page']) || $data['page'] == 1) {
+            $min          = 0;
+            $data['page'] = 1;
+        } else {
+            $min = ($data['page'] - 1) * $this->pageCount;
+        }
+
+        $shieldUsers = Db::name('fastim_user_shield')
+            ->alias('s')
+            ->field('s.*,f.remark,fu.nickname as fu_nickname,fu.avatar as fu_avatar,a.nickname as a_nickname,a.avatar as a_avatar,u.nickname,u.avatar,u.id as uid,se.id as session_id')
+            ->join('fastim_session se', '(se.user_one=s.user_id AND se.user_two=s.shield_id) OR (se.user_one=s.shield_id AND se.user_two=s.user_id)')
+            ->join('fastim_user u', 's.shield_id=u.id')
+            ->join('user fu', 'u.user_id=fu.id', 'LEFT')
+            ->join('admin a', 'u.admin_id=a.id', 'LEFT')
+            ->join('fastim_friendship f', 's.user_id=f.user_id AND s.shield_id=f.friend_id', 'LEFT')
+            ->where('s.user_id', $this->userInfo['id'])
+            ->limit($min, $this->pageCount)
+            ->select();
+
+        foreach ($shieldUsers as $key => $value) {
+            $shieldUsers[$key]['nickname'] = Common::trueAttr(Common::nicknameSort([
+                $value['nickname'],
+                $value['fu_nickname'],
+                $value['a_nickname']
+            ], $value['uid']));
+
+            $nickname = Common::trueAttr(Common::nicknameSort([
+                $value['remark'],
+                $shieldUsers[$key]['nickname']
+            ], $value['uid']));
+
+            $shieldUsers[$key]['avatar'] = Common::avatarSrc([
+                $value['avatar'],
+                $value['fu_avatar'],
+                $value['a_avatar']
+            ], $nickname);
+
+            unset($shieldUsers[$key]['fu_nickname'], $shieldUsers[$key]['a_nickname'], $shieldUsers[$key]['fu_avatar'], $shieldUsers[$key]['a_avatar']);
+        }
+
+        $nextpage = (count($shieldUsers) < $this->pageCount) ? false : true;
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'user-shield',
+            'data'  => [
+                'data'         => $data,
+                'shield_users' => $shieldUsers,
+                'nextpage'     => $nextpage
+            ]
+        ]));
+    }
+
+    /**
+     * 用户收藏
+     * @param  [type] $data [description]
+     * @return [type]       [description]
+     */
+    public function collection($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id', 'action'], $data)) {
+            return;
+        }
+
+        if ($data['action'] == 'del') {
+            if (Db::name('fastim_user_collection')
+                ->where('id', $data['id'])
+                ->where('user_id', $this->userInfo['id'])
+                ->delete()) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'del-collection',
+                    'data'  => [
+                        'data' => $data,
+                        'msg'  => '收藏已删除~'
+                    ]
+                ]));
+            }
+        }
+    }
+
+    /**
+     * 聊天设置
+     */
+    public function sessionSetting($data)
+    {
+        $sessionInfo = Common::sessionInfo($data['session_id'], $this->userInfo['id']);
+        if ($sessionInfo['user_one'] != $this->userInfo['id'] && $sessionInfo['user_two'] != $this->userInfo['id']) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '您没有权限访问~'
+            ]));
+            return;
+        }
+
+        // 是否置顶
+        $sessionInfo['top'] = Db::name('fastim_session_top')
+            ->where('user_id', $this->userInfo['id'])
+            ->where('session_id', $data['session_id'])
+            ->value('top');
+        $sessionInfo['top'] = $sessionInfo['top'] ? true : false;
+
+        if ($sessionInfo['type'] == 'single') {
+            $friendId = ($sessionInfo['user_one'] == $this->userInfo['id']) ? $sessionInfo['user_two'] : $sessionInfo['user_one'];
+            $friend   = Db::name('fastim_friendship')
+                ->where('user_id', $this->userInfo['id'])
+                ->where('friend_id', $friendId)
+                ->find();
+
+            // 是否是好友
+            $sessionInfo['friend'] = $friend ? true : false;
+
+            // 是否屏蔽
+            $sessionInfo['shield'] = Db::name('fastim_user_shield')
+                ->where('user_id', $this->userInfo['id'])
+                ->where('shield_id', $friendId)
+                ->value('id');
+            $sessionInfo['shield'] = $sessionInfo['shield'] ? true : false;
+
+            // 是否是常用联系人
+            $sessionInfo['top_contacts'] = ($friend && $friend['top_contacts']) ? true : false;
+        } elseif ($sessionInfo['type'] == 'group') {
+            $groupUser = Db::name('fastim_group_user')
+                ->where('group_id', $sessionInfo['chat_id'])
+                ->where('user_id', $this->userInfo['id'])
+                ->find();
+
+            // 消息免打扰
+            $sessionInfo['block_messages'] = $groupUser['block_messages'] ? true : false;
+
+            // 我的群聊备注
+            $sessionInfo['nickname'] = $groupUser['nickname'];
+
+            // 是否是群主
+            $sessionInfo['isLeader'] = ($sessionInfo['sessionUser']['leader'] == $this->userInfo['id']) ? true : false;
+
+            // 群公告条数-未读群公告数
+            $unreadNoticeNumber = Db::name('fastim_group_chat_notice')
+                ->alias('cn')
+                ->field('cn.*,r.id AS rid')
+                ->join('fastim_reading_log r', 'r.type=0 AND r.user_id=:user_id AND r.data_id=cn.id', 'LEFT')
+                ->where('cn.group_id', $sessionInfo['chat_id'])
+                ->where('cn.deletetime', null)
+                ->where('r.id', null)
+                ->bind([
+                    'user_id' => $this->userInfo['id']
+                ])
+                ->count('*');
+            if ($unreadNoticeNumber) {
+                $sessionInfo['notice'] = $unreadNoticeNumber . '条未读';
+            } else {
+                $number                = Db::name('fastim_group_chat_notice')
+                    ->where('group_id', $sessionInfo['chat_id'])
+                    ->where('deletetime', null)
+                    ->count('*');
+                $sessionInfo['notice'] = $number . '条';
+            }
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'session-setting',
+            'data'  => [
+                'session_info' => $sessionInfo
+            ]
+        ]));
+    }
+
+    /**
+     * 获得用户与指定服务号的会话ID
+     * @param  [type] $data [description]
+     * @return [type]       [description]
+     */
+    public function getServiceSession($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['service_id'], $data)) {
+            return;
+        }
+
+        $userImSession = Common::imSession('service', $this->userInfo['id'], $data['service_id']);
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'service_session',
+            'data'  => [
+                'data'         => $data,
+                'service_info' => $userImSession
+            ]
+        ]));
+    }
+
+    /**
+     * 删除联系人/群
+     */
+    public function delContact($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id', 'type'], $data)) {
+            return;
+        }
+
+        if ($data['type'] == 'user') {
+            // 删除好友
+            Db::name('fastim_friendship')
+                ->where('user_id', $this->userInfo['id'])
+                ->where('friend_id', $data['id'])
+                ->delete();
+            $msg = '好友删除成功~';
+        } elseif ($data['type'] == 'group') {
+            // 退出群聊
+            $groupChat = Common::getImGroupChat($data['id']);
+            if ($groupChat['leader'] == $this->userInfo['id']) {
+
+                $groupUser = Db::name('fastim_group_user')->where('group_id', $data['id'])->select();
+
+                Db::startTrans();
+                try {
+                    Db::name('fastim_session')->where('type', 'group')->where('chat_id', $data['id'])->delete();
+                    Db::name('fastim_group_user')->where('group_id', $data['id'])->delete();
+                    Db::name('fastim_group_chat')
+                        ->where('id', $data['id'])
+                        ->where('leader', $this->userInfo['id'])
+                        ->update([
+                            'deletetime' => time()
+                        ]);
+                    Db::commit();
+                } catch (PDOException $e) {
+                    Db::rollback();
+                    $this->server->push($this->frame->fd, json_encode([
+                        'event' => 'show_msg',
+                        'data'  => $e->getMessage()
+                    ]));
+                    return;
+                }
+
+                $notice = [
+                    'action' => 'dismiss'
+                ];
+
+                // 通知所有群成员重载会话列表
+                foreach ($groupUser as $key => $value) {
+                    if ($value['user_id'] == $this->userInfo['id']) {
+                        $notice['notice'] = '你已经解散了群聊';
+                    } else {
+                        $notice['notice'] = '群主已经解散了群聊';
+                    }
+
+                    // 发送一个群通知给所有群成员
+                    $groupImSession = Common::imSession('service', $value['user_id'], 2);
+                    $messageData    = Common::insertMessage([
+                        'type'         => 'group_notice',
+                        'group_id'     => $data['id'],
+                        'session_id'   => $groupImSession['id'],
+                        'sender_id'    => 0,
+                        'recipient_id' => $value['user_id'],
+                        'message'      => $notice
+                    ]);
+
+                    // 消息无需推送,直接重装会话列表
+                    $this->swooleCommon->pushMessageToUser($value['user_id'], [
+                        'event' => 'reload_session_list'
+                    ]);
+                }
+
+                $msg = '已解散群聊~';
+
+            } else {
+
+                $notice = [
+                    'action'    => 'quit',
+                    'quit_user' => $this->userInfo['id']
+                ];
+
+                Db::startTrans();
+                try {
+                    Db::name('fastim_group_user')
+                        ->where('group_id', $data['id'])
+                        ->where('user_id', $this->userInfo['id'])
+                        ->delete();
+                    Db::name('fastim_session')
+                        ->where('type', 'group')
+                        ->where('user_one', $this->userInfo['id'])
+                        ->where('chat_id', $data['id'])
+                        ->delete();
+
+                    // 给群主发送一个通知
+                    $groupImSession = Common::imSession('service', $groupChat['leader'], 2);
+                    $messageData    = Common::insertMessage([
+                        'type'         => 'group_notice',
+                        'group_id'     => $data['id'],
+                        'session_id'   => $groupImSession['id'],
+                        'sender_id'    => 0,
+                        'recipient_id' => $groupChat['leader'],
+                        'message'      => $notice
+                    ]);
+                    if ($messageData) {
+                        $sessionInfo                  = Common::sessionInfo($groupImSession['id'], $groupChat['leader']);
+                        $sessionInfo['userInfo']      = Common::getImUserInfo($groupChat['leader']);
+                        $sessionInfo['groupChatInfo'] = $groupChat;
+                        $this->swooleCommon->newMessagePush($messageData, $sessionInfo, $groupChat['leader']);
+                    }
+
+                    Db::commit();
+                } catch (PDOException $e) {
+                    Db::rollback();
+                    $this->server->push($this->frame->fd, json_encode([
+                        'event' => 'show_msg',
+                        'data'  => $e->getMessage()
+                    ]));
+                    return false;
+                }
+
+                $msg = '已退出群聊~';
+            }
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'del-contact',
+            'data'  => [
+                'data' => $data,
+                'msg'  => $msg
+            ]
+        ]));
+    }
+
+    /**
+     * 加入群聊
+     */
+    public function joinGroupOption($data, $directAgree = false)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id', 'action'], $data, false, ['id' => '申请消息找不到啦~'])) {
+            return false;
+        }
+        $message = Db::name('fastim_message')->where('id', $data['id'])->where('type', 'group_apply')->find();
+        if (!$message) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '申请消息找不到啦~'
+            ]));
+            return false;
+        }
+
+        if (!$directAgree) {
+            $groupChat = Common::getImGroupChat($message['group_id']);
+            if (!$groupChat || ($groupChat['leader'] != $this->userInfo['id'])) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => '您没有权限操作~'
+                ]));
+                return false;
+            }
+        }
+
+        $message['message'] = json_decode($message['message'], true);
+        if ($data['action'] == 'agree' || $data['action'] == 'autoagree') {
+            // 同意入群
+            Db::startTrans();
+            try {
+                $imSession = Common::imSession('group', $message['sender_id'], $message['group_id']);
+                $groupUser = [
+                    'group_id'      => $message['group_id'],
+                    'user_id'       => $message['sender_id'],
+                    'session_id'    => $imSession['id'],
+                    'user_group_id' => (int)$message['message']['group'],
+                    'jointime'      => time()
+                ];
+                Db::name('fastim_group_user')->insert($groupUser);
+
+                // 推送群消息
+                $this->swooleCommon->pushMessageToGroup('system', [
+                    'group_id'     => $message['group_id'],
+                    'session_id'   => $imSession['id'],
+                    'sender_id'    => $this->userInfo['id'],
+                    'recipient_id' => $message['sender_id'],
+                    'message'      => [
+                        'message'      => '你已经是群成员了,和大家打个招呼吧~',
+                        'display_user' => $message['sender_id']
+                    ]
+                ]);
+
+                Db::commit();
+            } catch (PDOException $e) {
+                Db::rollback();
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => $e->getMessage()
+                ]));
+                return false;
+            }
+
+            if ($data['action'] == 'agree') {
+                $res = [
+                    'msg'        => '已同意加群申请~',
+                    'type'       => 'success',
+                    'session_id' => $message['session_id']
+                ];
+            }
+        } else {
+            $res = [
+                'msg'        => '已拒绝加群申请~',
+                'type'       => 'tips',
+                'session_id' => $message['session_id']
+            ];
+
+            // 给被拒绝用户推送消息
+            $imSession                    = Common::imSession('service', $message['sender_id'], 2);
+            $sessionInfo                  = Common::sessionInfo($imSession['id'], $message['sender_id']);
+            $messageData                  = Common::insertMessage([
+                'type'         => 'group_notice',
+                'group_id'     => $message['group_id'],
+                'session_id'   => $sessionInfo['id'],
+                'sender_id'    => $this->userInfo['id'],
+                'recipient_id' => $message['sender_id'],
+                'message'      => [
+                    'action'      => 'refuse',
+                    'refuse_user' => $message['sender_id']
+                ]
+            ]);
+            $sessionInfo['userInfo']      = Common::getImUserInfo($message['sender_id'], false, $message['sender_id']);
+            $sessionInfo['groupChatInfo'] = Common::getImGroupChat($message['group_id']);
+            $this->swooleCommon->newMessagePush($messageData, $sessionInfo, $message['sender_id']);
+        }
+
+        // 同类申请
+        $applyMessage = Db::name('fastim_message')
+            ->where('group_id', $message['group_id'])
+            ->where('type', 'group_apply')
+            ->where('sender_id', $message['sender_id'])
+            ->where('recipient_id', $message['recipient_id'])
+            ->select();
+
+        foreach ($applyMessage as $key => $value) {
+            $msg = json_decode($value['message'], true);
+            if (!isset($msg['result']) || !$msg['result'] || $msg['result'] == 'pending') {
+                $msg['result'] = $data['action'];
+                $msg           = json_encode($msg);
+                Db::name('fastim_message')->where('id', $value['id'])->update(['message' => $msg]);
+            }
+        }
+
+        if (isset($res)) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'new_friends_option',
+                'data'  => $res
+            ]));
+        }
+        return true;
+    }
+
+    /**
+     * 新朋友申请消息处理
+     */
+    public function newFriendsOption($data, $agreeUserId = 0)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id', 'action'], $data, false, ['id' => '好友申请消息找不到啦~'])) {
+            return;
+        }
+
+        $message = Db::name('fastim_message')->where('id', $data['id'])->where('type', 'friend_apply')->find();
+
+        if (!$message) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '好友申请消息找不到啦~'
+            ]));
+            return;
+        }
+        $message['message'] = json_decode($message['message'], true);
+
+        $uid           = $agreeUserId ? $agreeUserId : $this->userInfo['id'];
+        $userImSession = Common::imSession('service', $uid, 1);
+
+        if ($data['action'] == 'agree' || $data['action'] == 'autoagree') {
+            // 同意-插入两条好友记录
+            $friendship = Db::name('fastim_friendship')
+                ->where('user_id', $uid)
+                ->where('friend_id', $message['sender_id'])
+                ->value('user_id');
+            if (!$friendship) {
+                $friend[] = [
+                    'user_id'    => $uid,
+                    'friend_id'  => $message['sender_id'],
+                    'group'      => isset($data['data']['new_contact_user_group']) ? (int)$data['data']['new_contact_user_group'] : 0,
+                    'remark'     => $data['data']['new_contact_remark'] ?? '',
+                    'createtime' => time()
+                ];
+            }
+
+            $friendship = Db::name('fastim_friendship')
+                ->where('user_id', $message['sender_id'])
+                ->where('friend_id', $uid)
+                ->value('friend_id');
+            if (!$friendship) {
+                $friend[] = [
+                    'user_id'    => $message['sender_id'],
+                    'friend_id'  => $uid,
+                    'group'      => isset($message['message']['group']) ? (int)$message['message']['group'] : 0,
+                    'remark'     => $message['message']['remark'] ?? '',
+                    'createtime' => time()
+                ];
+            }
+
+            if (isset($friend) && $friend) {
+                Db::name('fastim_friendship')->insertAll($friend);
+            } else {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => '你们已经是好友了哦~'
+                ]));
+                return;
+            }
+
+            // 给双方发送通知消息
+            $imSession = Common::imSession('single', $uid, $message['sender_id']);
+
+            $sessionInfo[] = Common::sessionInfo($imSession['id'], $message['sender_id']);
+            $sessionInfo[] = Common::sessionInfo($imSession['id'], $uid);
+
+            foreach ($sessionInfo as $key => $value) {
+                // 消息入库
+                $messageData = Common::insertMessage([
+                    'type'         => 'system',
+                    'session_id'   => $value['id'],
+                    'sender_id'    => $value['user']['id'],
+                    'recipient_id' => $value['sessionUser']['id'],
+                    'message'      => [
+                        'message'      => '我们已经是好友了~',
+                        'display_user' => $value['sessionUser']['id']
+                    ]
+                ]);
+                if ($messageData) {
+                    $value['pushUser'] = Common::getImUserInfo($value['user']['id'], false, $value['sessionUser']['id']);
+                    $this->swooleCommon->newMessagePush($messageData, $value, $value['sessionUser']['id']);
+                }
+            }
+
+            if ($data['action'] == 'agree') {
+                $res = [
+                    'msg'        => '已同意好友申请~',
+                    'type'       => 'success',
+                    'session_id' => $message['session_id']
+                ];
+            }
+
+        } elseif ($data['action'] == 'refuse') {
+
+            $res = [
+                'msg'        => '已拒绝好友申请~',
+                'type'       => 'tips',
+                'session_id' => $userImSession['id']
+            ];
+
+            // 给被拒绝的用户推送消息
+            $imSession               = Common::imSession('service', $message['sender_id'], 1);
+            $sessionInfo             = Common::sessionInfo($imSession['id'], $message['sender_id']);
+            $messageData             = Common::insertMessage([
+                'type'         => 'friend_apply',
+                'session_id'   => $sessionInfo['id'],
+                'sender_id'    => $uid,
+                'recipient_id' => $message['sender_id'],
+                'message'      => [
+                    'method' => 'notice',
+                    'result' => 'refuse'
+                ]
+            ]);
+            $sessionInfo['userInfo'] = Common::getImUserInfo($uid, false, $message['sender_id']);
+            $this->swooleCommon->newMessagePush($messageData, $sessionInfo, $message['sender_id']);
+        }
+
+        // 同类申请消息
+        $similarMessage = Db::name('fastim_message')
+            ->where('type', 'friend_apply')
+            ->where('sender_id', $message['sender_id'])
+            ->where('recipient_id', $message['recipient_id'])
+            ->select();
+
+        // 对所有同类申进行标记
+        foreach ($similarMessage as $key => $value) {
+            $msg = json_decode($value['message'], true);
+            if ($msg['method'] == 'apply' && (!isset($msg['result']) || !$msg['result'] || $msg['result'] == 'pending')) {
+                $msg['result'] = $data['action'];
+                $msg           = json_encode($msg);
+                Db::name('fastim_message')->where('id', $value['id'])->update(['message' => $msg]);
+            }
+        }
+
+        if (isset($res)) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'new_friends_option',
+                'data'  => $res
+            ]));
+        }
+    }
+
+    /**
+     * 申请添加新联系人/群
+     */
+    public function createNewContact($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id', 'type', 'method', 'action'], $data)) {
+            return false;
+        }
+
+        if ($data['type'] == 'friends') {
+            // 查得被添加用户基本信息
+            $info = Common::getImUserInfo($data['id']);
+            if ($info['id'] == $this->userInfo['id']) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => '无需添加自己为好友~'
+                ]));
+                return false;
+            }
+
+            $friendship = Db::name('fastim_friendship')
+                ->where('user_id', $this->userInfo['id'])
+                ->where('friend_id', $info['id'])
+                ->find();
+
+            if ($data['action'] == 'pre-agree' && isset($data['message_id'])) {
+                // 查得消息资料
+                $info['message'] = Db::name('fastim_message')
+                    ->where('id', $data['message_id'])
+                    ->where('type', 'friend_apply')
+                    ->find();
+                if ($info['message']) {
+                    $info['message']['message'] = json_decode($info['message']['message'], true);
+                    $info['add_note']           = isset($info['message']['message']['note']) ? $info['message']['message']['note'] : '';
+                    unset($info['message']);
+                }
+            }
+
+            if ($friendship) {
+                if ($data['action'] == 'pre-agree') {
+                    // 预同意申请请求,直接同意
+                    $this->newFriendsOption(['id' => $data['message_id'], 'action' => 'agree'], $this->userInfo['id']);
+                    return false;
+                } else {
+                    $this->server->push($this->frame->fd, json_encode([
+                        'event' => 'show_msg',
+                        'data'  => 'TA已经是您的好友了哦~'
+                    ]));
+                    return false;
+                }
+            }
+
+            // 查得用户的分组设置
+            $group = Common::getGroups($this->userInfo['id'], 0);
+            array_splice($group, 0, 1);// 删除常用联系人分组
+
+            // 用户验证要求配置
+            $imUserConfig          = Common::imUserConfig($info['id']);
+            $info['verify_method'] = $imUserConfig['verify_method'];
+            $info['temp_session']  = $imUserConfig['temp_session'];
+
+            if ($data['method'] == 'post') {
+                // 获得被添加用户与“新朋友”服务号的会话
+                $imSession = Common::imSession('service', $data['id'], 1);
+                if (!$imSession) {
+                    $this->server->push($this->frame->fd, json_encode([
+                        'event' => 'show_msg',
+                        'data'  => '申请发送失败,请稍后重试~'
+                    ]));
+                    return false;
+                }
+                // 发送好友申请
+                $message     = [
+                    'remark' => $data['new_contact_remark'],
+                    'group'  => isset($data['new_contact_user_group']) ? (int)$data['new_contact_user_group'] : 0,
+                    'note'   => $data['new_contact_note'] ?? '',
+                    'source' => '搜索好友',
+                    'result' => 'pending',
+                    'method' => 'apply'
+                ];
+                $sessionInfo = Common::sessionInfo($imSession['id'], $this->userInfo['id']);
+
+                // 消息入库
+                $messageData = Common::insertMessage([
+                    'type'         => 'friend_apply',
+                    'session_id'   => $sessionInfo['id'],
+                    'sender_id'    => $this->userInfo['id'],
+                    'recipient_id' => $data['id'],
+                    'message'      => $message
+                ]);
+
+                if ($messageData) {
+                    // 推送申请消息给被添加人
+                    $sessionInfo['pushUser'] = $sessionInfo['sessionUser'];
+                    $sessionInfo['userInfo'] = $sessionInfo['user'];
+                    $this->swooleCommon->newMessagePush($messageData, $sessionInfo, $data['id']);
+
+                    // 如果被添加人无需验证-直接同意
+                    if ($info['verify_method'] == 1) {
+                        $this->newFriendsOption(['id' => $messageData['id'], 'action' => 'autoagree'], $info['id']);
+                        $res = [
+                            'msg'  => '成功添加好友~',
+                            'type' => 'success'
+                        ];
+                    } else {
+                        $imSession               = Common::imSession('service', $this->userInfo['id'], 1);
+                        $sessionInfo             = Common::sessionInfo($imSession['id'], $this->userInfo['id']);
+                        $sessionInfo['userInfo'] = $info;
+                        $this->swooleCommon->newMessagePush($messageData, $sessionInfo, $this->userInfo['id']);
+                        $res = [
+                            'msg'  => '好友申请已发送~',
+                            'type' => 'success'
+                        ];
+                    }
+
+                    $this->server->push($this->frame->fd, json_encode([
+                        'event' => 'show_msg',
+                        'data'  => $res
+                    ]));
+                } else {
+                    $this->server->push($this->frame->fd, json_encode([
+                        'event' => 'show_msg',
+                        'data'  => '申请发送失败,请稍后重试~'
+                    ]));
+                }
+                return false;
+            }
+
+        } elseif ($data['type'] == 'group') {
+            // 查得被添加群聊基本信息
+            $info = Common::getImGroupChat($data['id']);
+
+            if (!$info) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => '你查找的群聊找不到啦~'
+                ]));
+                return false;
+            }
+
+            $groupMember = Db::name('fastim_group_user')
+                ->where('group_id', $data['id'])
+                ->where('user_id', $this->userInfo['id'])
+                ->value('group_id');
+
+            if ($groupMember) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => '您已经是群聊成员了~'
+                ]));
+                return false;
+            }
+
+            // 查得用户的分组设置
+            $group = Common::getGroups($this->userInfo['id'], 1);
+            unset($info['invite_join_group']);
+
+            if ($data['method'] == 'post') {
+                // 建立群主和“群通知”的会话
+                $imSessionLeader = $info['leader'] ? Common::imSession('service', $info['leader'], 2) : false;
+                if (!$imSessionLeader) {
+                    $this->server->push($this->frame->fd, json_encode([
+                        'event' => 'show_msg',
+                        'data'  => '申请发送失败,请稍后重试~'
+                    ]));
+                    return false;
+                }
+
+                // 发送加群申请-申请人为发送人,群主为接受人
+                $message     = [
+                    'group'  => isset($data['new_contact_user_group']) ? (int)$data['new_contact_user_group'] : 0,
+                    'note'   => $data['new_contact_note'] ?? '',
+                    'source' => '搜索群聊',
+                    'result' => 'pending'
+                ];
+                $sessionInfo = Common::sessionInfo($imSessionLeader['id'], $this->userInfo['id']);
+
+                // 消息入库
+                $messageData = Common::insertMessage([
+                    'type'         => 'group_apply',
+                    'session_id'   => $imSessionLeader['id'],
+                    'sender_id'    => $this->userInfo['id'],
+                    'recipient_id' => $info['leader'],
+                    'message'      => $message,
+                    'group_id'     => $data['id']
+                ]);
+
+                if ($messageData) {
+                    $sessionInfo['userInfo']      = Common::getImUserInfo($this->userInfo['id']);
+                    $sessionInfo['groupChatInfo'] = $info;
+                    $this->swooleCommon->newMessagePush($messageData, $sessionInfo, $info['leader']);
+                    if ($info['add_mode'] == 1) {
+                        // 无需验证-直接入群
+                        if ($this->joinGroupOption(['id' => $messageData['id'], 'action' => 'autoagree'], true)) {
+                            $res = [
+                                'msg'  => '成功加入群聊~',
+                                'type' => 'success'
+                            ];
+                        } else {
+                            $res = [
+                                'msg'  => '加群失败,请重试~',
+                                'type' => 'error'
+                            ];
+                        }
+                    } else {
+                        // 建立加群人和“群通知”的会话
+                        Common::imSession('service', $this->userInfo['id'], 2);
+                        $this->server->push($this->frame->fd, json_encode([
+                            'event' => 'reload_session_list'
+                        ]));
+                        $res = [
+                            'msg'  => '加群申请已发送~',
+                            'type' => 'success'
+                        ];
+                    }
+                    $this->server->push($this->frame->fd, json_encode([
+                        'event' => 'show_msg',
+                        'data'  => $res
+                    ]));
+                } else {
+                    $this->server->push($this->frame->fd, json_encode([
+                        'event' => 'show_msg',
+                        'data'  => '申请发送失败,请稍后重试~'
+                    ]));
+                }
+                return false;
+            }
+
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'create_new_contact',
+            'data'  => [
+                'info'  => $info,
+                'data'  => $data,
+                'group' => $group
+            ]
+        ]));
+    }
+
+    /**
+     * 屏蔽一个会话
+     */
+    public function shieldSession($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['session_id', 'method'], $data)) {
+            return;
+        }
+
+        $sessionInfo = Common::sessionInfo($data['session_id'], $this->userInfo['id']);
+
+        $exist = Db::name('fastim_user_shield')
+            ->where('user_id', $this->userInfo['id'])
+            ->where('shield_id', $sessionInfo['sessionUser']['id'])
+            ->value('id');
+
+        if ($data['method'] == 'shield' && !$exist) {
+            $shield = [
+                'user_id'   => $this->userInfo['id'],
+                'shield_id' => $sessionInfo['sessionUser']['id']
+            ];
+            Db::name('fastim_user_shield')->insert($shield);
+            if ($sessionInfo['deleteuser']) {
+                Db::name('fastim_session')->where('id', $sessionInfo['id'])->delete();
+            } else {
+                Db::name('fastim_session')->where('id', $sessionInfo['id'])->update([
+                    'deleteuser' => $this->userInfo['id']
+                ]);
+            }
+        } elseif ($data['method'] == 'relieve' && $exist) {
+            Db::name('fastim_user_shield')->where('id', $exist)->delete();
+            if ($sessionInfo['deleteuser'] == $this->userInfo['id']) {
+                Db::name('fastim_session')->where('id', $sessionInfo['id'])->update([
+                    'deleteuser' => 0
+                ]);
+            }
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'shield_session',
+            'data'  => [
+                'session_id' => $data['session_id'],
+                'method'     => $data['method']
+            ]
+        ]));
+    }
+
+    /**
+     * 更新好友备注/分组等
+     */
+    public function updateFriendInfo($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id', 'method'], $data)) {
+            return;
+        }
+
+        if ($data['method'] == 'update_remark') {
+            if (!$this->swooleCommon->requiredParamCheck(['new_remark'], $data, false, ['new_remark' => '请输入正确的好友备注~'])) {
+                return;
+            }
+
+            Db::name('fastim_friendship')
+                ->where('user_id', $this->userInfo['id'])
+                ->where('friend_id', $data['id'])
+                ->update([
+                    'remark' => $data['new_remark']
+                ]);
+
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => [
+                    'msg'  => '好友备注修改成功~',
+                    'type' => 'success'
+                ]
+            ]));
+
+            $this->infoDetail([
+                'id'     => $data['id'],
+                'method' => 'get-new-info',
+                'type'   => 'user'
+            ]);
+        } elseif ($data['method'] == 'update_group') {
+            $update = true;
+            if ($data['new_group'] == 'common') {
+                Db::name('fastim_friendship')
+                    ->where('user_id', $this->userInfo['id'])
+                    ->where('friend_id', $data['id'])
+                    ->update([
+                        'top_contacts' => 1
+                    ]);
+                $update = false;
+            } elseif ($data['new_group'] == 'all_friends') {
+                $data['new_group'] = 0;
+            }
+
+            if ($update) {
+                Db::name('fastim_friendship')
+                    ->where('user_id', $this->userInfo['id'])
+                    ->where('friend_id', $data['id'])
+                    ->update([
+                        'group'        => (int)$data['new_group'],
+                        'top_contacts' => 0
+                    ]);
+            }
+            if (isset($data['source']) && $data['source'] == 'uni-app') {
+                $msg = ($data['new_group'] === 'common') ? '已添加好友至常用联系人~' : '已取消常用联系人~';
+            } else {
+                $msg = '好友分组修改成功~';
+            }
+
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => [
+                    'msg'  => $msg,
+                    'type' => 'success'
+                ]
+            ]));
+        } elseif ($data['method'] == 'update_groupchat_group') {
+            if ($data['new_group'] == 'all_group') {
+                $data['new_group'] = 0;
+            }
+
+            Db::name('fastim_group_user')
+                ->where('group_id', $data['id'])
+                ->where('user_id', $this->userInfo['id'])
+                ->update([
+                    'user_group_id' => (int)$data['new_group']
+                ]);
+
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => [
+                    'msg'  => '群聊分组修改成功~',
+                    'type' => 'success'
+                ]
+            ]));
+        }
+    }
+
+    /**
+     * 用户和群聊资料
+     */
+    public function infoDetail($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id', 'type', 'method'], $data)) {
+            return false;
+        }
+
+        if ($data['type'] == 'user') {
+            $info = Common::getImUserInfo($data['id'], true, $this->userInfo['id']);
+        } elseif ($data['type'] == 'group') {
+            $info = Common::getImGroupChat($data['id']);
+        } elseif ($data['type'] == 'service') {
+            $info = Db::name('fastim_service')->where('id', $data['id'])->find();
+        }
+
+        if (!$info) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '获取资料失败,请重试~'
+            ]));
+            return false;
+        }
+
+        if ($data['type'] == 'user') {
+            $info['occupation'] = ($data['method'] == 'user-edit') ? $info['occupation'] : Common::formatOccupation($info['occupation']);
+
+            // 取得用户好友分组
+            $info['groups'] = Common::getGroups($this->userInfo['id'], 0);
+        } elseif ($data['type'] == 'group') {
+            $info['createtime'] = Common::formatTime($info['createtime']);
+            $info['leader']     = Common::getImUserInfo($info['leader'], true, $this->userInfo['id']);
+            $info['isLeader']   = true;
+            if (!$info['leader'] || ($info['leader']['id'] != $this->userInfo['id'])) {
+                // 不是leader,去除群的隐私资料
+                $info['isLeader'] = false;
+                unset($info['add_mode'], $info['invite_join_group'], $info['history_message'], $info['retrieval_settings']);
+            }
+
+            $groupUser = Db::name('fastim_group_user')
+                ->where('group_id', $data['id'])
+                ->where('user_id', $this->userInfo['id'])
+                ->find();
+
+            if ($groupUser) {
+                $info['groupMember'] = $groupUser['group_id'];
+                $info['group']       = $groupUser['user_group_id'] ? $groupUser['user_group_id'] : 'all_group';
+            } else {
+                $info['groupMember'] = false;
+                $info['group']       = 'all_group';
+            }
+
+            // 取得用户群聊分组
+            $info['groups'] = Common::getGroups($this->userInfo['id'], 1);
+        } elseif ($data['type'] == 'service') {
+            $info['avatar'] = Common::avatarSrc([
+                $info['avatar']
+            ], $info['nickname']);
+        }
+
+        if ($data['method'] == 'user-edit') {
+            // 编辑用户资料
+            if ($data['id'] != $this->userInfo['id']) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => '您没有权限操作~'
+                ]));
+                return false;
+            }
+            $info['birthday']       = $info['birthday'] ? explode('-', $info['birthday']) : false;
+            $info['occupationData'] = Common::formatOccupation(false);
+        } elseif ($data['method'] == 'group-edit') {
+            // 编辑群资料
+            if (!$info['isLeader']) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => '您没有权限操作~'
+                ]));
+                return false;
+            }
+        } elseif ($data['method'] == 'post-user-edit' && $data['id'] == $this->userInfo['id']) {
+            // 保存用户资料
+            unset($data['method'], $data['type']);
+
+            if (!$this->swooleCommon->requiredParamCheck(['nickname'], $data, false, ['nickname' => '请输入正确的昵称~'])) {
+                return false;
+            }
+
+            $data['birthday'] = (isset($data['birthday']) && $data['birthday']) ? $data['birthday'] : date('Y-m-d');
+            Db::name('fastim_user')->where('id', $this->userInfo['id'])->update($data);
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => [
+                    'msg'  => '资料保存成功~',
+                    'type' => 'success'
+                ]
+            ]));
+            return false;
+        } elseif ($data['method'] == 'post-group-edit') {
+            if (!$info['isLeader']) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => '您没有权限操作~'
+                ]));
+                return false;
+            }
+
+            if (!$this->swooleCommon->requiredParamCheck(['nickname'], $data, false, ['nickname' => '请输入正确的群名称~'])) {
+                return false;
+            }
+            // 保存群聊资料
+            unset($data['method'], $data['type'], $data['action']);
+            Db::name('fastim_group_chat')->update($data);
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => [
+                    'msg'  => '资料保存成功~',
+                    'type' => 'success'
+                ]
+            ]));
+            return false;
+        } elseif ($data['method'] == 'post-avatar') {
+            // 修改头像
+            if ($data['type'] == 'user') {
+                Db::name('fastim_user')->where('id', $this->userInfo['id'])->update([
+                    'avatar' => $data['url']
+                ]);
+            } elseif ($data['type'] == 'group' && $info['isLeader']) {
+                Db::name('fastim_group_chat')->where('id', $info['id'])->update([
+                    'avatar' => $data['url']
+                ]);
+            }
+            $info['avatar'] = $data['url'];
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'info-detail',
+            'data'  => [
+                'info' => $info,
+                'data' => $data
+            ]
+        ]));
+    }
+
+    /**
+     * 加载会话
+     */
+    public function loadSession($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['session_id'], $data, false, ['session_id' => '会话找不到啦~'])) {
+            return;
+        }
+
+        // 获取会话资料
+        $sessionInfo = Common::sessionInfo($data['session_id'], $this->userInfo['id']);
+        if (!$sessionInfo) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '会话找不到啦~'
+            ]));
+            return;
+        }
+        // 置顶状态
+        $sessionInfo['top'] = Db::name('fastim_session_top')
+            ->where('user_id', $this->userInfo['id'])
+            ->where('session_id', $sessionInfo['id'])
+            ->value('top');
+
+        // 是否为已绑定用户的游客
+        $tourist        = $this->swooleCommon->getUserIsTourist($this->frame->fd);
+        $imUserBindUser = Db::name('fastim_user')
+            ->field('user_id, admin_id')
+            ->where('id', $this->userInfo['id'])
+            ->find();
+        if (($tourist == 1) && $imUserBindUser && ($imUserBindUser['admin_id'] || $imUserBindUser['user_id'])) {
+            $sessionInfo['windowType'] = 'fake_tourist';
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'load_session',
+                'data'  => [
+                    'info'  => $sessionInfo,
+                    'admin' => $imUserBindUser['admin_id']
+                ]
+            ]));
+            return;
+        }
+
+        if (!isset($data['page']) || $data['page'] == 1) {
+            $min          = 0;
+            $data['page'] = 1;
+        } else {
+            $min = ($data['page'] - 1) * $this->pageCount;
+        }
+
+        if ($sessionInfo['type'] == 'single') {
+            // 单聊消息
+
+            // 标记已读
+            Db::name('fastim_message')
+                ->where('session_id', $data['session_id'])
+                ->where('recipient_id', $this->userInfo['id'])
+                ->where('status', 0)
+                ->update(['status' => 1]);
+
+            // 发送已读通知
+            $this->swooleCommon->pushMessageToUser($sessionInfo['sessionUser']['id'], [
+                'event' => 'read_message',
+                'data'  => [
+                    'sessionType' => 0,
+                    'session_id'  => $data['session_id']
+                ]
+            ]);
+
+            $message = Common::loadSingleChatRecord($data['session_id'], [
+                'min'       => $min,
+                'pageCount' => $this->pageCount
+            ], $this->userInfo['id']);
+
+            $nextpage = (count($message) < $this->pageCount) ? false : true;
+
+            if ($data['page'] == 1) {
+                $message = array_reverse($message, false);
+                $message = Common::groupByTime($message, 'desc');
+            } else {
+                $message = Common::groupByTime($message);
+            }
+
+            $sessionInfo['shield'] = Db::name('fastim_user_shield')
+                ->where('user_id', $this->userInfo['id'])
+                ->where('shield_id', $sessionInfo['sessionUser']['id'])
+                ->value('id');
+
+            $sessionInfo['windowType'] = 'message';
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'load_session',
+                'data'  => [
+                    'message'  => $message,
+                    'info'     => $sessionInfo,
+                    'nextpage' => $nextpage,
+                    'data'     => $data
+                ]
+            ]));
+        } elseif ($sessionInfo['type'] == 'group') {
+            // 群聊消息
+            // 取得未读公告
+            $unreadFixedMsg = Db::name('fastim_group_chat_notice')
+                ->alias('cn')
+                ->field('cn.*,r.id AS rid')
+                ->join('fastim_reading_log r', 'r.type=0 AND r.user_id=:user_id AND r.data_id=cn.id', 'LEFT')
+                ->where('cn.group_id', $sessionInfo['chat_id'])
+                ->where('cn.popup', 1)
+                ->where('cn.deletetime', null)
+                ->where('r.id', null)
+                ->bind([
+                    'user_id' => $this->userInfo['id']
+                ])
+                ->order('createtime desc')
+                ->find();
+            $unreadFixedMsg = Common::formatGroupChatNotice($unreadFixedMsg, $this->userInfo['id'], false);
+
+            $this->swooleCommon->readGroupMessage($sessionInfo);
+
+            $message = Common::loadGroupChatRecord($sessionInfo, [
+                'min'       => $min,
+                'pageCount' => $this->pageCount
+            ], $this->userInfo['id']);
+
+            $nextpage = (count($message) < $this->pageCount) ? false : true;
+
+            if ($data['page'] == 1) {
+                $message = array_reverse($message, false);
+                $message = Common::groupByTime($message, 'desc');
+            } else {
+                $message = Common::groupByTime($message);
+            }
+
+            $sessionInfo['windowType']           = 'message';
+            $sessionInfo['sessionUser']['speak'] = ($sessionInfo['sessionUser']['speak'] == 1 && $sessionInfo['sessionUser']['leader'] == $this->userInfo['id']) ? 0 : $sessionInfo['sessionUser']['speak'];
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'load_session',
+                'data'  => [
+                    'message'        => $message,
+                    'info'           => $sessionInfo,
+                    'nextpage'       => $nextpage,
+                    'data'           => $data,
+                    'lastMessage'    => Common::getLastMessage($this->userInfo['id'], $sessionInfo),
+                    'unreadFixedMsg' => $unreadFixedMsg
+                ]
+            ]));
+        } elseif ($sessionInfo['type'] == 'service') {
+            // 服务号
+
+            // 群通知或好友申请消息
+            if ($sessionInfo['chat_id'] == 1 || $sessionInfo['chat_id'] == 2 || $sessionInfo['chat_id'] == 3) {
+
+                Common::readServiceMessage($sessionInfo['chat_id'], $this->userInfo['id'], $sessionInfo['id']);
+
+                $message = Common::loadServiceRecord($sessionInfo, [
+                    'min'       => $min,
+                    'pageCount' => $this->pageCount
+                ], $this->userInfo['id']);
+                if ($message) {
+                    $sessionInfo['windowType'] = $message['windowType'];
+                    $message                   = $message['message'];
+                }
+
+                $nextpage = (count($message) < $this->pageCount) ? false : true;
+
+                if ($data['page'] == 1 && $sessionInfo['chat_id'] == 3) {
+                    $message = array_reverse($message, false);
+                    $message = Common::groupByTime($message, 'desc');
+                } else {
+                    $message = Common::groupByTime($message);
+                }
+            }
+
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'load_session',
+                'data'  => [
+                    'message'     => $message,
+                    'info'        => $sessionInfo,
+                    'nextpage'    => $nextpage,
+                    'data'        => $data,
+                    'lastMessage' => Common::getLastMessage($this->userInfo['id'], $sessionInfo)
+                ]
+            ]));
+        }
+    }
+
+    /**
+     * 会话列表
+     */
+    public function loadSessionList($data)
+    {
+        $data['method'] = $data['method'] ?? 'load';
+        if ($data['method'] == 'pickuser') {
+            $where['s.type'] = 'single';
+        }
+        $where['s.deleteuser'] = ['<>', $this->userInfo['id']];
+
+        /*Db::name('fastim_session')->where('user_one',$this->userInfo['id'])->update(['deleteuser'=>0]);
+        $hasId=Db::name('fastim_session')->where('user_one',$this->userInfo['id'])->column('user_two')?:[0];
+        $userIds=Db::name('fastim_user')->where('id','<>',$this->userInfo['id'])->whereNotIn('id',$hasId)->column('id');
+        $data=[];
+        foreach ($userIds as $id){
+            $data[]=[
+                'type'=>'single',
+                'user_one'=>$this->userInfo['id'],
+                'user_two'=>$id,
+                'createtime'=>time(),
+            ];
+        }
+        Db::name('fastim_session')->insertAll($data);*/
+        $hasId=Db::name('fastim_friendship')->where('user_id',$this->userInfo['id'])->column('friend_id')?:[0];
+        $userIds=Db::name('fastim_user')->where('id','<>',$this->userInfo['id'])->whereNotIn('id',$hasId)->column('id');
+        $userData=[];
+        foreach ($userIds as $id){
+            $userData[]=[
+                'user_id'=>$this->userInfo['id'],
+                'friend_id'=>$id,
+                'createtime'=>time(),
+            ];
+        }
+        Db::name('fastim_friendship')->insertAll($userData);
+
+        /**
+         * 会话列表
+         * 直接关联到了服务号和群聊表的资料、单聊资料需单独获取
+         * 通过 message 表的 createtime 排序、会话创建时间辅助排序
+         * 涉及群组在会话列表的排序,所以显得复杂,请不要随意改动
+         */
+        $dbprefix        = config('database.prefix');
+        $typeSql         = CommonCode::nonPrivilegedMessage(true);
+        $serviceTypeSql  = "IN ('group_invitation', 'group_notice', 'group_apply')";
+        $groupMessageExp = "m.type {$typeSql} OR (m.type='system' AND (m.recipient_id=:user_id OR (m.recipient_id=0 AND m.createtime>=gu.jointime)))";
+        $orderSubSelect  = "(SELECT createtime FROM `{$dbprefix}fastim_message` `m` WHERE (s.type='single' AND m.session_id=s.id) OR (s.type='service' AND (m.sender_id=:user_id OR m.recipient_id=:user_id) AND ((s.chat_id=1 AND m.type='friend_apply') OR (s.chat_id=2 AND m.type {$serviceTypeSql})) OR (s.chat_id=3)) OR (s.type='group' AND s.chat_id=m.group_id AND ((gc.history_message=0 AND {$groupMessageExp} AND m.createtime>=gu.jointime) OR (gc.history_message=1 AND {$groupMessageExp}))) ORDER BY m.createtime DESC LIMIT 1)";
+
+        $session = Db::name('fastim_session')
+            ->alias('s')
+            ->field('s.*, se.avatar as se_avatar, se.nickname as se_nickname, se.bio as se_bio, gc.avatar as gc_avatar, gc.nickname as gc_nickname, gc.bio as gc_bio, gc.history_message as gc_history_message, gu.jointime,' . $orderSubSelect . ' as m_createtime, t.top')
+            ->join('fastim_group_chat gc', 's.chat_id=gc.id AND s.type="group" AND gc.deletetime IS NULL', 'LEFT')
+            ->join('fastim_service se', 's.chat_id=se.id AND s.type="service"', 'LEFT')
+            ->join('fastim_group_user gu', 'gu.group_id=gc.id AND gu.user_id=:user_id', 'LEFT')
+            ->join('fastim_session_top t', 't.user_id=:user_id AND t.session_id=s.id', 'LEFT') // 会话置顶表
+            ->where('s.user_one|s.user_two', $this->userInfo['id'])
+            ->where($where)
+            ->bind([
+                'user_id' => $this->userInfo['id']
+            ])
+            ->order('t.top desc, m_createtime desc, s.createtime desc')
+            ->limit(40)
+            ->select(false);
+        $session = Db::query($session);
+
+        // 删除这些key以保持干净
+        $delKey = [
+            'se_avatar'   => '',
+            'se_nickname' => '',
+            'se_bio'      => '',
+            'gc_avatar'   => '',
+            'gc_nickname' => '',
+            'gc_bio'      => ''
+        ];
+
+        // pushUser=会话对象
+
+        foreach ($session as $key => $value) {
+            if ($value['type'] == 'single') {
+                // 单聊
+                $userInfo                        = Common::getImUserInfo(($value['user_one'] == $this->userInfo['id']) ? $value['user_two'] : $value['user_one'], false, $this->userInfo['id']);
+                $session[$key]['pushUser']       = $userInfo;
+                $session[$key]['searchContinue'] = $userInfo['friend'] ? true : false;
+            } elseif ($value['type'] == 'group') {
+                // 群聊
+                $session[$key]['searchContinue']       = true;
+                $session[$key]['pushUser']['nickname'] = $value['gc_nickname'];
+                $session[$key]['pushUser']['avatar']   = Common::avatarSrc([
+                    $value['gc_avatar']
+                ], $session[$key]['pushUser']['nickname']);
+
+                $session[$key]['pushUser']['id']              = $value['chat_id'];
+                $session[$key]['pushUser']['bio']             = $value['gc_bio'];
+                $session[$key]['pushUser']['history_message'] = $value['gc_history_message'];
+                $session[$key]['shield']                      = Common::getGroupChatShield($value['chat_id'], $this->userInfo['id']);
+            } elseif ($value['type'] == 'service') {
+                // 服务号
+                $session[$key]['searchContinue']       = false;
+                $session[$key]['pushUser']['nickname'] = $value['se_nickname'];
+                $session[$key]['pushUser']['avatar']   = Common::avatarSrc([
+                    $value['se_avatar']
+                ], $session[$key]['pushUser']['nickname']);
+
+                $session[$key]['pushUser']['id']  = $value['chat_id'];
+                $session[$key]['pushUser']['bio'] = $value['se_bio'];
+            }
+
+            if (isset($data['keyword'])) {
+                if (mb_strpos($session[$key]['pushUser']['nickname'], $data['keyword']) === false || ($session[$key]['searchContinue'])) {
+                    unset($session[$key]);
+                    continue;
+                }
+            }
+
+            $session[$key] = array_diff_key($session[$key], $delKey);
+
+            $session[$key] = [
+                'sessionInfo'          => $session[$key],
+                'lastMessage'          => Common::getLastMessage($this->userInfo['id'], $session[$key]),
+                'unreadMessagesNumber' => Common::getUnreadMessagesNumber($this->userInfo['id'], $session[$key])
+            ];
+        }
+
+        // 搜索来的,不直接返回给前台
+        if (isset($data['keyword'])) {
+            return $session;
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'load_session_list',
+            'data'  => [
+                'session' => $session,
+                'data'    => $data
+            ]
+        ]));
+    }
+
+    /**
+     * 会话右击
+     */
+    public function sessionOperation($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id', 'action'], $data)) {
+            return false;
+        }
+
+        $sessionInfo = Db::name('fastim_session')
+            ->where('id', $data['id'])
+            ->where('user_one|user_two', $this->userInfo['id'])
+            ->find();
+        if (!$sessionInfo) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '会话找不到啦~'
+            ]));
+            return false;
+        }
+
+        if ($data['action'] == 'session-top') {
+            // 置顶会话
+            $exist = Db::name('fastim_session_top')
+                ->where('user_id', $this->userInfo['id'])
+                ->where('session_id', $data['id'])
+                ->value('top');
+            if ($exist) {
+                // 取消置顶
+                Db::name('fastim_session_top')
+                    ->where('user_id', $this->userInfo['id'])
+                    ->where('session_id', $data['id'])
+                    ->delete();
+            } else {
+                // 插入置顶
+                /*$maxTop = Db::name('fastim_session_top')
+                ->where('user_id', $this->userInfo['id'])
+                ->max('top');*/
+                Db::name('fastim_session_top')->insert([
+                    'user_id'    => $this->userInfo['id'],
+                    'session_id' => $data['id'],
+                    'top'        => 1
+                ]);
+            }
+            if (!isset($data['source']) || $data['source'] != 'uni-app') {
+                $this->loadSessionList([]);
+            } else {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'session-operation-tips',
+                    'data'  => [
+                        'type' => 'success',
+                        'msg'  => '会话已' . ($exist ? '取消' : '') . '置顶~'
+                    ]
+                ]));
+            }
+        } elseif ($data['action'] == 'session-info') {
+            // 会话资料
+            if ($sessionInfo['type'] == 'single' || $sessionInfo['type'] == 'group') {
+                $info = [
+                    'id'   => ($sessionInfo['type'] == 'single') ? ($sessionInfo['user_one'] == $this->userInfo['id']) ? $sessionInfo['user_two'] : $sessionInfo['user_one'] : $sessionInfo['chat_id'],
+                    'type' => ($sessionInfo['type'] == 'single') ? 'user' : 'group'
+                ];
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'session_info',
+                    'data'  => $info
+                ]));
+                return false;
+            }
+        } elseif ($data['action'] == 'session-remove') {
+            // 移除会话
+            Db::name('fastim_session')->where('id', $data['id'])->update([
+                'deleteuser' => $this->userInfo['id']
+            ]);
+            if (!isset($data['source']) || $data['source'] != 'uni-app') {
+                $this->loadSessionList([]);
+            } else {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'session-operation-tips',
+                    'data'  => [
+                        'type' => 'success',
+                        'msg'  => '会话已移除~'
+                    ]
+                ]));
+            }
+        } elseif ($data['action'] == 'session-block-groupmessage') {
+            // 屏蔽群消息
+            $blockMessages = Db::name('fastim_group_user')
+                ->where('group_id', $sessionInfo['chat_id'])
+                ->where('user_id', $this->userInfo['id'])
+                ->value('block_messages');
+            Db::name('fastim_group_user')
+                ->where('group_id', $sessionInfo['chat_id'])
+                ->where('user_id', $this->userInfo['id'])
+                ->update([
+                    'block_messages' => $blockMessages ? '0' : '1'
+                ]);
+
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => (!isset($data['source']) || $data['source'] != 'uni-app') ? 'show_msg' : 'session-operation-tips',
+                'data'  => [
+                    'type' => 'success',
+                    'msg'  => '群消息已' . ($blockMessages ? '解除' : '设置为') . '免打扰~'
+                ]
+            ]));
+        }
+    }
+
+    /**
+     * 搜索用户
+     */
+    public function search($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['keyword', 'method'], $data, false, ['keyword' => '请输入关键词~'])) {
+            return;
+        }
+
+        $method = explode('|', $data['method']);// 要搜索的数据类型
+
+        // 搜索好友
+        if (in_array('friends', $method)) {
+            $friendSql = Db::name('fastim_friendship')
+                ->alias('f')
+                ->field('f.*,u.avatar,u.nickname,u.status,u.bio,u.gender,fu.avatar as fu_avatar,fu.nickname as fu_nickname,a.avatar as a_avatar,a.nickname as a_nickname,g.name as groupname')
+                ->join('fastim_user u', 'f.friend_id=u.id')
+                ->join('user fu', 'u.user_id=fu.id', 'LEFT')
+                ->join('admin a', 'u.admin_id=a.id', 'LEFT')
+                ->join('fastim_group g', 'g.id=f.group AND g.type=0 AND g.user_id=:user_id', 'LEFT')
+                ->where('f.user_id', $this->userInfo['id'])
+                ->where("f.friend_id=:keyword OR f.remark LIKE :likeyword OR u.nickname LIKE :likeyword OR fu.nickname LIKE :likeyword OR a.nickname LIKE :likeyword")
+                ->bind([
+                    'keyword'   => $data['keyword'],
+                    'likeyword' => '%' . $data['keyword'] . '%',
+                    'user_id'   => $this->userInfo['id']
+                ])
+                ->order('f.createtime desc')
+                ->select(false);
+            $friends   = Db::query($friendSql);
+            foreach ($friends as $key => $info) {
+                $friends[$key]['id']              = $info['friend_id'];
+                $friends[$key]['nickname_origin'] = Common::trueAttr(Common::nicknameSort([
+                    $friends[$key]['nickname'],
+                    $info['fu_nickname'],
+                    $info['a_nickname']
+                ], $friends[$key]['id']));
+
+                $friends[$key]['nickname'] = Common::trueAttr(Common::nicknameSort([
+                    $info['remark'],
+                    $friends[$key]['nickname_origin']
+                ], $friends[$key]['id']));
+                $friends[$key]['avatar']   = Common::avatarSrc([
+                    $info['avatar'],
+                    $info['fu_avatar'],
+                    $info['a_avatar']
+                ], $friends[$key]['nickname']);
+
+                $friends[$key]['groupname'] = $friends[$key]['groupname'] ? $friends[$key]['groupname'] : '全部好友';
+
+                unset($friends[$key]['fu_avatar'], $friends[$key]['a_avatar'], $friends[$key]['fu_nickname'], $friends[$key]['a_nickname']);
+            }
+        }
+
+        // 从会话中搜索好友
+        if (in_array('lately-session-user', $method)) {
+            $session = $this->loadSessionList([
+                'method'  => 'pickuser',
+                'keyword' => $data['keyword']
+            ]);
+
+            $latelySessionUser = array_values($session?:[]);
+        }
+
+        // 搜索群聊
+        if (in_array('group-chat', $method)) {
+            $groupChat = Db::name('fastim_group_user')
+                ->alias('gu')
+                ->field('gu.group_id, gu.user_id, gu.user_group_id, gu.jointime, gc.id, gc.avatar, gc.nickname, g.name as groupname')
+                ->join('fastim_group_chat gc', 'gu.group_id=gc.id')
+                ->join('fastim_group g', 'gu.user_group_id=g.id AND g.type=1 AND g.user_id=:user_id', 'LEFT')
+                ->where('gu.user_id', $this->userInfo['id'])
+                ->where('gc.id=:keyword OR gc.nickname LIKE :likeyword')
+                ->bind([
+                    'user_id'   => $this->userInfo['id'],
+                    'keyword'   => $data['keyword'],
+                    'likeyword' => '%' . $data['keyword'] . '%'
+                ])
+                ->order('gu.jointime desc')
+                ->select();
+            foreach ($groupChat as $key => $value) {
+                $groupChat[$key]['avatar']    = Common::avatarSrc([
+                    $value['avatar']
+                ], $groupChat[$key]['nickname']);
+                $groupChat[$key]['groupname'] = $value['groupname'] ? $value['groupname'] : '全部群聊';
+            }
+        }
+
+        // 搜索全部会话
+        if (in_array('lately-session', $method)) {
+            $session       = $this->loadSessionList([
+                'keyword' => $data['keyword']
+            ]);
+            $latelySession = array_values($session);
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'user_search',
+            'data'  => [
+                'friends'             => $friends ?? [],
+                'lately_session_user' => $latelySessionUser ?? [],
+                'group_chat'          => $groupChat ?? [],
+                'lately_session'      => $latelySession ?? [],
+                'data'                => $data
+            ]
+        ]));
+    }
+
+    /**
+     * uni-app端加载好友和群聊
+     * @param  [type] $data [description]
+     * @return [type]       [description]
+     */
+    public function loadContact($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['method'], $data)) {
+            return;
+        }
+
+        if ($data['method'] == 'all-friend' || $data['method'] == 'top-friend') {
+            $where['f.user_id'] = $this->userInfo['id'];
+            if ($data['method'] == 'top-friend') {
+                $where['f.top_contacts'] = 1;
+            }
+
+            $res = Db::name('fastim_friendship')
+                ->alias('f')
+                ->field('f.*,u.avatar,u.nickname,u.status,fu.avatar as fu_avatar,fu.nickname as fu_nickname,a.avatar as a_avatar,a.nickname as a_nickname')
+                ->join('fastim_user u', 'f.friend_id=u.id')
+                ->join('user fu', 'u.user_id=fu.id', 'LEFT')
+                ->join('admin a', 'u.admin_id=a.id', 'LEFT')
+                ->where($where)
+                ->order('f.createtime asc')
+                ->select();
+            foreach ($res as $key => $info) {
+                $res[$key]['nickname'] = Common::trueAttr(Common::nicknameSort([
+                    $info['remark'],
+                    $res[$key]['nickname'],
+                    $info['fu_nickname'],
+                    $info['a_nickname']
+                ], $info['friend_id']));
+
+                $res[$key]['avatar'] = Common::avatarSrc([
+                    $info['avatar'],
+                    $info['fu_avatar'],
+                    $info['a_avatar']
+                ], $res[$key]['nickname']);
+
+                unset($res[$key]['fu_avatar'], $res[$key]['a_avatar'], $res[$key]['fu_nickname'], $res[$key]['a_nickname']);
+            }
+
+            $initialPinyinArr = Common::initialPinyinArrSort($res, 'nickname');
+        } elseif ($data['method'] == 'all-group-chat') {
+            $res = Db::name('fastim_group_user')
+                ->alias('gu')
+                ->field('gc.*,gu.jointime')
+                ->where('gu.user_id', $this->userInfo['id'])
+                ->where('gc.deletetime', null)
+                ->join('fastim_group_chat gc', 'gu.group_id=gc.id')
+                ->order('gu.jointime asc')
+                ->select();
+
+            foreach ($res as $key => $value) {
+                $res[$key]['avatar'] = Common::avatarSrc([
+                    $value['avatar']
+                ], $res[$key]['nickname']);
+            }
+
+            $initialPinyinArr = Common::initialPinyinArrSort($res, 'nickname');
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'load_contact',
+            'data'  => [
+                'res'  => $initialPinyinArr,
+                'data' => $data
+            ]
+        ]));
+    }
+
+    /**
+     * 加载分组内的好友或群聊
+     */
+    public function loadGroup($data)
+    {
+        if (!$this->swooleCommon->requiredParamCheck(['id', 'method'], $data, false, ['id' => '分组找不到啦~'])) {
+            return;
+        }
+
+        $group = Common::getGroupInfo($data['id']);
+        if (!$group) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '分组找不到啦~'
+            ]));
+            return;
+        }
+
+        if (!isset($data['page']) || $data['page'] == 1) {
+            $min = 0;
+        } else {
+            $min = ($data['page'] - 1) * $this->pageCount;
+        }
+
+        if ($group['type'] == 0) {
+            // 好友
+            $where['f.user_id'] = $this->userInfo['id'];
+            if ($data['id'] == 'common') {
+                // 获取常用联系人
+                $where['f.top_contacts'] = 1;
+            } elseif ($data['id'] != 'all_friends') {
+                $where['f.group'] = $data['id'];
+            }
+
+            $res = Db::name('fastim_friendship')
+                ->alias('f')
+                ->field('f.*,u.avatar,u.nickname,u.status,u.bio,u.gender,fu.avatar as fu_avatar,fu.nickname as fu_nickname,fu.bio as fu_bio,fu.gender as fu_gender,a.avatar as a_avatar,a.nickname as a_nickname')
+                ->join('fastim_user u', 'f.friend_id=u.id')
+                ->join('user fu', 'u.user_id=fu.id', 'LEFT')
+                ->join('admin a', 'u.admin_id=a.id', 'LEFT')
+                ->where($where);
+
+            if ($data['method'] == 'pick-group-user') {
+                $res = $res->order('f.createtime asc')->select();
+            } else {
+                $res = $res->order('f.createtime desc')->limit($min, $this->pageCount)->select();
+            }
+
+            foreach ($res as $key => $info) {
+                $res[$key]['nickname_origin'] = Common::trueAttr(Common::nicknameSort([
+                    $res[$key]['nickname'],
+                    $info['fu_nickname'],
+                    $info['a_nickname']
+                ], $info['friend_id']));
+
+                $res[$key]['nickname'] = Common::trueAttr(Common::nicknameSort([
+                    $info['remark'],
+                    $res[$key]['nickname_origin']
+                ], $info['friend_id']));
+                $res[$key]['avatar']   = Common::avatarSrc([
+                    $info['avatar'],
+                    $info['fu_avatar'],
+                    $info['a_avatar']
+                ], $res[$key]['nickname']);
+                $res[$key]['bio']      = Common::trueAttr([
+                    $info['bio'],
+                    $info['fu_bio'],
+                    '这家伙很懒,什么也没写!'
+                ]);
+                $res[$key]['gender']   = Common::trueAttr([
+                    $info['gender'],
+                    $info['fu_gender'],
+                    '0'
+                ]);
+                $res[$key]['gender']   = Common::imUserGender($info['gender']);
+                $res[$key]['status']   = Common::imUserStatus($info['status']);
+                unset($res[$key]['fu_avatar'], $res[$key]['a_avatar'], $res[$key]['fu_nickname'], $res[$key]['a_nickname'], $res[$key]['fu_bio'], $res[$key]['fu_gender']);
+            }
+            $data['type'] = 'user';
+        } elseif ($group['type'] == 1) {
+            // 群聊-取得分组中的群聊
+            if ($data['id'] == 'all_group') {
+                // 全部分组
+                $join = [
+                    ['fastim_group g', 'gu.user_group_id=g.id', 'LEFT']
+                ];
+            } else {
+                $join = [
+                    ['fastim_group g', 'gu.user_group_id=g.id AND g.id=' . (int)$data['id']]
+                ];
+            }
+
+            $dbprefix = config('database.prefix');
+            $res      = Db::name('fastim_group_user')
+                ->alias('gu')
+                ->field('gc.*, gu.user_group_id, gu.jointime, g.name as groupname,(SELECT count(user_id) FROM `' . $dbprefix . 'fastim_group_user` `guc` WHERE guc.group_id=gu.group_id) as user_count')
+                ->join('fastim_group_chat gc', 'gu.group_id=gc.id')
+                ->join($join)
+                ->where('gu.user_id', $this->userInfo['id']);
+
+            if ($data['method'] == 'pick-group-user') {
+                $res = $res->order('gu.jointime asc')->select();
+            } else {
+                $res = $res->order('gu.jointime desc')->limit($min, $this->pageCount)->select();
+            }
+
+            foreach ($res as $key => $value) {
+                $res[$key]['avatar'] = Common::avatarSrc([
+                    $value['avatar']
+                ], $res[$key]['nickname']);
+                $res[$key]['bio']    = Common::trueAttr([
+                    $value['bio'],
+                    '群主很懒,什么也没写~'
+                ]);
+            }
+
+            $data['type'] = 'group_chat';
+        } elseif ($group['type'] == 2) {
+            // 收藏
+            $where['user_id'] = $this->userInfo['id'];
+
+            $cType = CommonCode::nonPrivilegedMessage(false, ['other']);
+            if (in_array($data['id'], $cType)) {
+                $where['type'] = $data['id'];
+            } elseif ($data['id'] == 'all_collection') {
+                // 
+            }
+
+            if (isset($data['keywords']) && $data['keywords']) {
+                $where['value'] = ['LIKE', '%' . $data['keywords'] . '%'];
+            }
+
+            $res = Db::name('fastim_user_collection')
+                ->where($where)
+                ->order('createtime desc')
+                ->limit($min, $this->pageCount)
+                ->select();
+
+            foreach ($res as $key => $value) {
+                if (in_array($value['type'], CommonCode::$messageJsonTypes)) {
+                    $res[$key]['message'] = json_decode($value['value'], true);
+                } else {
+                    $res[$key]['message'] = htmlspecialchars_decode($value['value']);
+                }
+                $res[$key]['createtime'] = Common::formatTime($value['createtime']);
+                unset($res[$key]['value']);
+            }
+        }
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'load_group',
+            'data'  => [
+                'res'      => $res,
+                'data'     => $data,
+                'group'    => $group,
+                'nextpage' => (count($res) < $this->pageCount) ? false : true,
+            ]
+        ]));
+    }
+
+    /**
+     * 搜索新联系人/群
+     */
+    public function searchNewContact($data)
+    {
+        if (!isset($data['keyword']) || !trim($data['keyword'])) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '请输入搜索关键词~'
+            ]));
+            return false;
+        }
+
+        if (!isset($data['page']) || $data['page'] == 1) {
+            $min = 0;
+        } else {
+            $min = ($data['page'] - 1) * $this->pageCount;
+        }
+
+        if ($data['type'] == 'friends') {
+
+            $searchSql = Db::name('fastim_user')
+                ->alias('u')
+                ->field('u.id,u.avatar,u.nickname,u.status,u.bio,u.gender,u.mobile,u.email,fu.avatar as fu_avatar,fu.nickname as fu_nickname,fu.bio as fu_bio,fu.gender as fu_gender,fu.mobile as fu_mobile,fu.email as fu_email,a.avatar as a_avatar,a.nickname as a_nickname,a.email as a_email,fr.friend_id as is_friend,fr.remark')
+                ->join('user fu', 'u.user_id=fu.id', 'LEFT')
+                ->join('admin a', 'u.admin_id=a.id', 'LEFT')
+                ->join('fastim_friendship fr', 'fr.user_id=:uid AND fr.friend_id=u.id', 'LEFT')
+                ->where("u.id=:keyword OR u.mobile=:keyword OR u.email=:keyword OR fu.mobile=:keyword OR fu.email=:keyword OR a.email=:keyword OR fu.nickname LIKE :likeyword OR a.nickname LIKE :likeyword")
+                ->where('u.id', '<>', $this->userInfo['id'])
+                ->bind([
+                    'keyword'   => $data['keyword'],
+                    'likeyword' => '%' . $data['keyword'] . '%',
+                    'uid'       => $this->userInfo['id']
+                ])
+                ->limit($min, $this->pageCount)
+                ->select(false);
+
+            $res = Db::query($searchSql);
+
+            $addMyWayField = [
+                0 => ['eq', ['id']],
+                1 => ['like', ['nickname', 'fu_nickname', 'a_nickname']],
+                2 => ['eq', ['mobile', 'fu_mobile']],
+                3 => ['eq', ['email', 'fu_email', 'a_email']],
+            ];
+            foreach ($res as $key => $value) {
+                // 检查添加方式
+                $retain   = false;
+               /* $addMyWay = Common::imUserConfig($value['id'], 'add_my_way');
+                if ($addMyWay !== '') {
+                    $addMyWay = explode(',', $addMyWay);
+                    foreach ($addMyWay as $item) {
+                        foreach ($addMyWayField[$item][1] as $itemField) {
+                            if ($value[$itemField]) {
+                                if ($addMyWayField[$item][0] == 'eq') {
+                                    if ($data['keyword'] == $value[$itemField]) {
+                                        $retain = true;
+                                    }
+                                } else {
+                                    if (mb_stristr($value[$itemField], $data['keyword']) !== false) {
+                                        $retain = true;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+                if (!$retain) {
+                    unset($res[$key]);
+                    continue;
+                }*/
+
+                $res[$key]['nickname'] = Common::trueAttr(Common::nicknameSort([
+                    $value['remark'],
+                    $value['nickname'],
+                    $value['fu_nickname'],
+                    $value['a_nickname']
+                ], $value['id']));
+
+                $res[$key]['avatar'] = Common::avatarSrc([
+                    $value['avatar'],
+                    $value['fu_avatar'],
+                    $value['a_avatar']
+                ], $res[$key]['nickname']);
+
+                $res[$key]['bio'] = Common::trueAttr([
+                    $value['bio'],
+                    $value['fu_bio'],
+                    '这家伙很懒,什么也没写!'
+                ]);
+
+                $res[$key]['gender'] = Common::trueAttr([
+                    $value['gender'],
+                    $value['fu_gender'],
+                    '0'
+                ]);
+                $res[$key]['gender'] = Common::imUserGender($res[$key]['gender']);
+                $res[$key]['status'] = Common::imUserStatus($value['status']);
+
+                unset($res[$key]['fu_avatar'], $res[$key]['a_avatar'], $res[$key]['fu_nickname'], $res[$key]['a_nickname'], $res[$key]['fu_bio'], $res[$key]['fu_gender'], $res[$key]['mobile'], $res[$key]['fu_mobile'], $res[$key]['email'], $res[$key]['fu_email'], $res[$key]['a_email']);
+            }
+        } elseif ($data['type'] == 'group') {
+            $dbprefix = config('database.prefix');
+            $res      = Db::name('fastim_group_chat')
+                ->alias('g')
+                ->field('g.*, (SELECT count(user_id) FROM `' . $dbprefix . 'fastim_group_user` `gu` WHERE gu.group_id=g.id) as user_count, gru.user_id as is_group_user')
+                ->join('fastim_group_user gru', 'gru.group_id=g.id AND gru.user_id=:uid', 'LEFT')
+                ->where(function ($query) use ($data) {
+                    $query->where('g.id', $data['keyword']);
+                    $query->whereOr('g.nickname', 'like', '%' . $data['keyword'] . '%');
+                })
+                ->where('g.deletetime', null)
+                ->bind([
+                    'uid' => $this->userInfo['id']
+                ])
+                ->order('g.createtime desc')
+                ->limit($min, $this->pageCount)
+                ->select();
+            foreach ($res as $key => $value) {
+
+                if ($value['retrieval_settings'] == 0) {
+                    unset($res[$key]);
+                    continue;
+                }
+
+                $res[$key]['avatar'] = Common::avatarSrc([
+                    $value['avatar'],
+                ], $value['nickname']);
+
+                $res[$key]['bio'] = Common::trueAttr([
+                    $value['bio'],
+                    '群主很懒,什么也没写~'
+                ]);
+            }
+        }
+
+        $res = array_values($res);
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'search_new_contact',
+            'data'  => [
+                'res'      => $res,
+                'data'     => $data,
+                'nextpage' => (count($res) < $this->pageCount) ? false : true,
+            ]
+        ]));
+    }
+
+    /**
+     * uni-app用户中心-待办和收藏数量
+     */
+    public function center($data)
+    {
+        $info            = Common::getImUserInfo($this->userInfo['id'], true, $this->userInfo['id']);
+        $collectionCount = Db::name('fastim_user_collection')->where('user_id', $this->userInfo['id'])->count();
+
+        $TODOCount = Db::name('fastim_todo')
+            ->where('user_id', $this->userInfo['id'])
+            ->where('status', '0')
+            ->where('deletetime', null)
+            ->count();
+
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'center',
+            'data'  => [
+                'info'            => $info,
+                'data'            => $data,
+                'collectionCount' => $collectionCount,
+                'TODOCount'       => $TODOCount
+            ]
+        ]));
+    }
+
+    public function onlineUsers($data){
+        $this->server->push($this->frame->fd, json_encode([
+            'event' => 'make_trans',
+            'data'  => [
+                'data' => [],
+                'cid'  => $this->userInfo,
+                'info' => Common::getImUserInfo($data['id']),
+                'group'=> \app\common\model\UserGroup::order('id','desc')->select()
+            ]
+        ]));
+    }
+
+    public function groupUsers($data){
+        $res=[];
+        if(!empty($data['id'])){
+            $map=[];
+            if($data['id']=='ls'){
+                $map['u.admin_id']=['=',0];
+                $map['fu.group_id']=['=',2];
+            }elseif ($data['id']=='kf'){
+                $map['u.user_id']=['=',0];
+                $map['u.admin_id']=['>',0];
+            }
+            dump($map);
+            $searchSql = Db::name('fastim_user')
+                ->alias('u')
+                ->field('u.id,u.avatar,u.nickname,u.status,u.bio,u.gender,u.mobile,u.email,fu.avatar as fu_avatar,
+                fu.nickname as fu_nickname,fu.bio as fu_bio,fu.gender as fu_gender,fu.mobile as fu_mobile,fu.group_id,
+                fu.email as fu_email,a.avatar as a_avatar,a.nickname as a_nickname,a.email as a_email,fr.friend_id as is_friend,fr.remark')
+                ->join('user fu', 'u.user_id=fu.id', 'LEFT')
+                ->join('admin a', 'u.admin_id=a.id', 'LEFT')
+                ->join('fastim_friendship fr','fr.user_id=:uid AND fr.friend_id=u.id', 'LEFT')
+                //->where('u.status',1)
+                //->where('u.type','csr')
+                    ->where($map)
+                //->where('fu.group_id',$data['id'])
+                ->bind([
+                    'uid'=> $this->userInfo['id']
+                ])
+                ->where('u.id','<>',$this->userInfo['id'])
+                ->select(false);
+
+            $res = Db::query($searchSql);
+
+            foreach ($res as $key => $value) {
+                $res[$key]['nickname'] = Common::trueAttr(Common::nicknameSort([
+                    $value['remark'],
+                    $value['nickname'],
+                    $value['fu_nickname'],
+                    $value['a_nickname']
+                ], $value['id']));
+
+                $res[$key]['avatar'] = Common::avatarSrc([
+                    $value['avatar'],
+                    $value['fu_avatar'],
+                    $value['a_avatar']
+                ], $res[$key]['nickname']);
+
+                $res[$key]['bio'] = Common::trueAttr([
+                    $value['bio'],
+                    $value['fu_bio'],
+                    '这家伙很懒,什么也没写!'
+                ]);
+
+                $res[$key]['gender'] = Common::trueAttr([
+                    $value['gender'],
+                    $value['fu_gender'],
+                    '0'
+                ]);
+                $res[$key]['gender'] = Common::imUserGender($res[$key]['gender']);
+                $res[$key]['status'] = Common::imUserStatus($value['status']);
+                unset($res[$key]['fu_avatar'], $res[$key]['a_avatar'], $res[$key]['fu_nickname'], $res[$key]['a_nickname'], $res[$key]['fu_bio'], $res[$key]['fu_gender'], $res[$key]['mobile'], $res[$key]['fu_mobile'], $res[$key]['email'], $res[$key]['fu_email'], $res[$key]['a_email']);
+            }
+        }
+        $this->server->push($this->frame->fd, json_encode([
+                'event' => 'change_trans_users',
+                'data'  => [
+                    'data' => $res,
+                ]
+            ]));
+    }
+
+    public function newTrans($data){
+        $targetId=$data['user_id'];
+        $serverId=$data['id'];
+        $remark=$data['remark'];
+
+        if ($targetId==$serverId) {
+            $this->server->push($this->frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '不能转给聊天人~'
+            ]));
+            return false;
+        }
+
+        $self=Common::getImUserInfo($this->userInfo['id']);
+        $server=Common::getImUserInfo($serverId);
+        $imSession   = Common::imSession('single',$targetId, $this->userInfo['id']);
+        $targetMeSessionInfo = Common::sessionInfo($imSession['id'], $targetId);
+
+        // 同意-插入两条好友记录
+        $friendship = Db::name('fastim_friendship')
+            ->where('user_id', $targetId)
+            ->where('friend_id', $serverId)
+            ->value('user_id');
+        if (!$friendship) {
+            $friend[] = [
+                'user_id'    => $targetId,
+                'friend_id'  => $serverId,
+                'group'      => 0,
+                'remark'     => '',
+                'createtime' => time()
+            ];
+        }
+
+        $friendship = Db::name('fastim_friendship')
+            ->where('user_id', $serverId)
+            ->where('friend_id', $targetId)
+            ->value('friend_id');
+        if (!$friendship) {
+            $friend[] = [
+                'user_id'    => $serverId,
+                'friend_id'  => $targetId,
+                'group'      => 0,
+                'remark'     => $remark,
+                'createtime' => time()
+            ];
+        }else{
+            $remark && Db::name('fastim_friendship')
+                ->where('user_id', $serverId)
+                ->where('friend_id', $targetId)
+                ->update(['remark'=>$remark]);
+        }
+
+        if (isset($friend) && $friend) {
+            Db::name('fastim_friendship')->insertAll($friend);
+        }
+
+        // 给双方发送通知消息
+        $targetImSession = Common::imSession('single', $targetId, $serverId);
+        $serverImSession = Common::imSession('single', $serverId, $targetId);
+
+        $sessionInfo[] =$targetSessionInfo= Common::sessionInfo($targetImSession['id'], $targetId);
+        $sessionInfo[] =$serveSessionInfo= Common::sessionInfo($serverImSession['id'], $serverId);
+
+        foreach ($sessionInfo as $key => $value) {
+            if($friendship){
+                continue;
+            }
+            // 消息入库
+            $messageData = Common::insertMessage([
+                'type'         => 'system',
+                'session_id'   => $value['id'],
+                'sender_id'    => $value['user']['id'],
+                'recipient_id' => $value['sessionUser']['id'],
+                'message'      => [
+                    'message'      => '我们已经是好友了~',
+                    'display_user' => $value['sessionUser']['id']
+                ]
+            ]);
+            if ($messageData) {
+                $value['pushUser'] = Common::getImUserInfo($value['user']['id'], false, $value['sessionUser']['id']);
+                $this->swooleCommon->newMessagePush($messageData, $value, $value['sessionUser']['id']);
+            }
+        }
+
+        if($server['type']=='csr'){
+            $groupName='客服';
+        }else {
+            $group_id = \app\common\model\User::alias('a')->join('fastim_user b', 'a.id=b.user_id')->where('b.id', $serverId)->value('a.group_id', 0);
+            $groupName = \app\common\model\UserGroup::where('id', $group_id)->value('name', '');
+        }
+
+        $pushSendUserMessage = [
+            'type'  => 'csrs',
+            'csrs'         => [
+                [
+                    'name' => $groupName,
+                    'csrs' => [
+                        Common::getImUserInfo($serverId),
+                    ]
+                ]
+            ],
+            'title'        => "正在为您转接:"
+        ];
+        $kbsMessageData = Common::insertMessage([
+            'type'         => 'kbs_list',
+            'session_id'   => $targetMeSessionInfo['id'],
+            'sender_id'    => $this->userInfo['id'],
+            'recipient_id' => $targetId,
+            'message'      => $pushSendUserMessage,
+        ]);
+        $this->swooleCommon->newMessagePush($kbsMessageData, $targetMeSessionInfo, $targetId);
+        $targetMeSessionInfo['pushUser']=$self;
+        $this->swooleCommon->newMessagePush($kbsMessageData, $targetMeSessionInfo, $this->userInfo['id']);
+
+        /*$messageData = Common::insertMessage([
+            'type'         => "default",
+            'session_id'   => $targetMeSessionInfo['id'],
+            'sender_id'    => $this->userInfo['id'],
+            'recipient_id' => $targetId,
+            'message'      => "正在为您转接{$groupName}:{$server['nickname']}",
+            'status'       => 0
+        ]);
+        $this->swooleCommon->newMessagePush($messageData, $targetMeSessionInfo, $targetId);
+        $targetMeSessionInfo['pushUser']=$self;
+        $this->swooleCommon->newMessagePush($messageData, $targetMeSessionInfo, $this->userInfo['id']);*/
+    }
+}

+ 81 - 0
addons/fastim/library/pushapi/GTBaseApi.php

@@ -0,0 +1,81 @@
+<?php
+
+namespace addons\fastim\library\pushapi;
+
+use addons\fastim\library\pushapi\utils\GTHttpManager;
+use addons\fastim\library\pushapi\exception\GTException;
+
+class GTBaseApi
+{
+    protected $gtClient;
+
+    protected function post($api, $params)
+    {
+        return $this->httpRequest($api, $params, GTHttpManager::HTTP_METHOD_POST);
+    }
+
+    protected function put($api, $params)
+    {
+        return $this->httpRequest($api, $params, GTHttpManager::HTTP_METHOD_PUT);
+    }
+
+    protected function get($api, $params)
+    {
+        return $this->httpRequest($api, $params, GTHttpManager::HTTP_METHOD_GET);
+    }
+
+    protected function delete($api, $params)
+    {
+        return $this->httpRequest($api, $params, GTHttpManager::HTTP_METHOD_DELETE);
+    }
+
+    private function httpRequest($api, $params, $method, $gzip = false)
+    {
+        try {
+            $rep = GTHttpManager::httpRequest($this->getUrl($api), $params, $this->buildHead(), $gzip, $method);
+        } catch (GTException $e) {
+            throw $e;
+        }
+        if ($rep != null) {
+            if ('10001' == $rep['code']) {
+                try {
+                    if ($this->gtClient->auth()) {
+                        $rep = GTHttpManager::httpRequest($this->getUrl($api), $params, $this->buildHead(), $gzip, $method);
+                    }
+                } catch (GTException $e) {
+                    throw $e;
+                }
+            } elseif ('301' == $rep['code']) {
+                if (empty($rep["data"]) || empty($rep["data"]["host_list"]) || empty($rep["data"]["host_list"][0]["domain_list"])) {
+                    throw new Exception("域名错误");
+                }
+                $this->gtClient->setDomainUrlList($rep["data"]["host_list"][0]["domain_list"]);
+                try {
+                    $rep = GTHttpManager::httpRequest($this->getUrl($api), $params, $this->buildHead(), $gzip, $method);
+                } catch (GTException $e) {
+                    throw $e;
+                }
+            }
+        }
+        return $rep;
+    }
+
+    private function analysisUrlList()
+    {
+
+    }
+
+    private function buildHead()
+    {
+        $headers = [];
+        if ($this->gtClient->getAuthToken() != null) {
+            array_push($headers, "token:" . $this->gtClient->getAuthToken());
+        }
+        return $headers;
+    }
+
+    private function getUrl($api)
+    {
+        return $this->gtClient->getHost() . "/v2/" . $this->gtClient->getAppId() . $api;
+    }
+}

+ 135 - 0
addons/fastim/library/pushapi/GTClient.php

@@ -0,0 +1,135 @@
+<?php
+
+namespace addons\fastim\library\pushapi;
+
+use addons\fastim\library\pushapi\GTPushApi;
+use addons\fastim\library\pushapi\GTStatisticsApi;
+use addons\fastim\library\pushapi\GTUserApi;
+use addons\fastim\library\pushapi\utils\GTConfig;
+use addons\fastim\library\pushapi\request\auth\GTAuthRequest;
+
+/**
+ * 个推客户端,用于进行推送、用户设置、报表查询等操作
+ * 每new一个GTclient对象,会进行一次鉴权,建议用户保存GTclient对象重复使用。
+ **/
+class GTClient
+{
+    //第三方 标识
+    private $appkey;
+    //第三方 密钥
+    private $masterSecret;
+    //鉴权token
+    private $authToken;
+    //第三方 appid
+    private $appId;
+
+    //是否使用https连接 以该标志为准
+    private $useSSL = null;
+    //用户配置或者内置域名列表
+    private $domainUrlList = null;
+    //是否指定域名。如果指定,使用过程中域名不会发生变化
+    private $isAssigned = false;
+
+    //推送api
+    private $pushApi = null;
+    //报表api
+    private $statisticsApi = null;
+    //用户api
+    private $userApi = null;
+
+    public function __construct($domainUrl, $appkey, $appId, $masterSecret, $ssl = null)
+    {
+        $this->appkey        = $appkey;
+        $this->masterSecret  = $masterSecret;
+        $this->appId         = $appId;
+        $this->pushApi       = new GTPushApi($this);
+        $this->statisticsApi = new GTStatisticsApi($this);
+        $this->userApi       = new GTUserApi($this);
+
+        $domainUrl = trim($domainUrl);
+        if ($ssl == null && $domainUrl != null && strpos(strtolower($domainUrl), "https:") === 0) {
+            $ssl = true;
+        }
+
+        $this->useSSL = ($ssl == null ? false : $ssl);
+
+        if ($domainUrl == null || strlen($domainUrl) == 0) {
+            $this->domainUrlList = GTConfig::getDefaultDomainUrl($this->useSSL);
+        } else {
+            if (GTConfig::isNeedOSAsigned()) {
+                $this->isAssigned = true;
+            }
+            $this->domainUrlList = [$domainUrl];
+        }
+        //鉴权
+        try {
+            $this->auth();
+        } catch (Exception $e) {
+            echo $e->getMessage();
+        }
+    }
+
+    public function getAuthToken()
+    {
+        return $this->authToken;
+    }
+
+    public function setAuthToken($authToken)
+    {
+        $this->authToken = $authToken;
+    }
+
+    public function pushApi()
+    {
+        return $this->pushApi;
+    }
+
+    public function statisticsApi()
+    {
+        return $this->statisticsApi;
+    }
+
+    public function userApi()
+    {
+        return $this->userApi;
+    }
+
+
+    public function getHost()
+    {
+        return $this->domainUrlList[0];
+    }
+
+    public function setDomainUrlList($domainUrlList)
+    {
+        $this->domainUrlList = $domainUrlList;
+    }
+
+    public function getAppId()
+    {
+        return $this->appId;
+    }
+
+    public function auth()
+    {
+        $auth = new GTAuthRequest();
+        $auth->setAppkey($this->appkey);
+        $timeStamp = $this->getMicroTime();
+        $sign      = hash("sha256", $this->appkey . $timeStamp . $this->masterSecret);
+        $auth->setSign($sign);
+        $auth->setTimestamp($timeStamp);
+        $rep = $this->userApi()->auth($auth);
+        if ($rep["code"] == 0) {
+            $this->authToken = $rep["data"]["token"];
+            return true;
+        }
+        return false;
+    }
+
+    function getMicroTime()
+    {
+        [$usec, $sec] = explode(" ", microtime());
+        $time = ($sec . substr($usec, 2, 3));
+        return $time;
+    }
+}

+ 86 - 0
addons/fastim/library/pushapi/GTPushApi.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace addons\fastim\library\pushapi;
+
+use addons\fastim\library\pushapi\GTBaseApi;
+
+/**
+ * 推送相关api,官网文档路径:http://docs.getui.com/getui/server/rest_v2/push/
+ **/
+class GTPushApi extends GTBaseApi
+{
+
+    public function __construct($gtClient)
+    {
+        $this->gtClient = $gtClient;
+    }
+
+    //向单个用户推送消息,可根据cid指定用户
+    function pushToSingleByCid($params)
+    {
+        return $this->post("/push/single/cid", $params->getApiParam());
+    }
+
+    function pushToSingleByAlias($params)
+    {
+        return $this->post("/push/single/alias", $params->getApiParam());
+    }
+
+    function pushBatchByCid($params)
+    {
+        return $this->post("/push/single/batch/cid", $params->getApiParam());
+    }
+
+    function pushBatchByAlias($params)
+    {
+        return $this->post("/push/single/batch/alias", $params->getApiParam());
+    }
+
+    function createListMsg($params)
+    {
+        return $this->post("/push/list/message", $params->getApiParam());
+    }
+
+    function pushListByCid($params)
+    {
+        return $this->post("/push/list/cid", $params->getApiParam());
+    }
+
+    function pushListByAlias($params)
+    {
+        return $this->post("/push/list/alias", $params->getApiParam());
+    }
+
+    function pushAll($params)
+    {
+        return $this->post("/push/all", $params->getApiParam());
+    }
+
+    function pushByTag($params)
+    {
+        return $this->post("/push/tag", $params->getApiParam());
+    }
+
+    function pushByFastCustomTag($params)
+    {
+        return $this->post("/push/fast_custom_tag", $params->getApiParam());
+    }
+
+    //停止任务
+    function stopPush($params)
+    {
+        return $this->delete("/task/" . $params, null);
+    }
+
+    //查询定时任务状态
+    function queryScheduleTask($params)
+    {
+        return $this->get("/task/schedule/" . $params, null);
+    }
+
+    //删除定时任务
+    function deleteScheduleTask($params)
+    {
+        return $this->delete("/task/schedule/" . $params, null);
+    }
+}

+ 48 - 0
addons/fastim/library/pushapi/GTStatisticsApi.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace addons\fastim\library\pushapi;
+
+use addons\fastim\library\pushapi\GTBaseApi;
+
+/**
+ * 报表相关api,官网文档路径:http://docs.getui.com/getui/server/rest_v2/report/
+ **/
+class GTStatisticsApi extends GTBaseApi
+{
+
+    public function __construct($gtClient)
+    {
+        $this->gtClient = $gtClient;
+    }
+
+    //查询推送数据,可查询消息有效可下发总数,消息回执总数和用户点击数等结果。支持单个taskId查询和多个taskId查询。
+    //任务id,推送时返回,多个taskId以英文逗号隔开,一次最多传200个
+    function queryPushResultByTaskIds($params)
+    {
+        return $this->get("/report/push/task/" . implode(",", $params), null);
+    }
+
+    //根据任务组名查询推送结果,返回结果包括百日内联网用户数(活跃用户数)、实际下发数、到达数、展示数、点击数。
+    function queryPushResultByGroupName($params)
+    {
+        return $this->get("/report/push/task_group/" . $params, null);
+    }
+
+    //获取单日用户数据
+    function queryUserDataByDate($params)
+    {
+        return $this->get("/report/user/date/" . $params, null);
+    }
+
+    //获取单日推送数据
+    function queryPushResultByDate($params)
+    {
+        return $this->get("/report/push/date/" . $params, null);
+    }
+
+    //获取24小时在线用户数
+    function queryOnlineUserData()
+    {
+        return $this->get("/report/online_user", null);
+    }
+}

+ 122 - 0
addons/fastim/library/pushapi/GTUserApi.php

@@ -0,0 +1,122 @@
+<?php
+
+namespace addons\fastim\library\pushapi;
+
+use addons\fastim\library\pushapi\GTBaseApi;
+
+
+/**
+ * 用户相关api,官网文档路径:http://docs.getui.com/getui/server/rest_v2/user/
+ **/
+class GTUserApi extends GTBaseApi
+{
+
+    public function __construct($gtClient)
+    {
+        $this->gtClient = $gtClient;
+    }
+
+    //鉴权
+    function auth($params)
+    {
+        return $this->post("/auth", $params->getApiParam());
+    }
+
+    //关闭鉴权,如果不传入token,则关闭该gtClient的token
+    function closeAuth($params)
+    {
+        if ($params != null) {
+            return $this->delete("/auth/" . $params, null);
+        } else {
+            return $this->delete("/auth/" . $this->gtClient->getAuthToken(), null);
+        }
+    }
+
+    //用户绑定别名
+    function bindAlias($params)
+    {
+        return $this->post("/user/alias", $params->getApiParam());
+    }
+
+    //cid查别名
+    function queryAliasByCid($params)
+    {
+        return $this->get("/user/alias/cid/" . $params, null);
+    }
+
+    //别名查cid
+    function queryCidByAlias($params)
+    {
+        return $this->get("/user/cid/alias/" . $params, null);
+    }
+
+    //解绑别名cid
+    function unbindAlias($params)
+    {
+        return $this->delete("/user/alias", $params->getApiParam());
+    }
+
+    //解绑别名下所有cid
+    function unbindAllAlias($params)
+    {
+        return $this->delete("/user/alias/" . $params, null);
+    }
+
+
+    //一个用户绑定一批标签,此操作为覆盖操作,会删除历史绑定的标签
+    function setTagForCid($params)
+    {
+        return $this->post("/user/custom_tag/cid/" . $params->getCid(), $params->getApiParam());
+    }
+
+    //根据cid查询用户标签列表
+    function queryUserTag($params)
+    {
+        return $this->get("/user/custom_tag/cid/" . $params, null);
+    }
+
+    //一批用户绑定一个标签
+    function batchModifyTagForBatchCid($params)
+    {
+        return $this->put("/user/custom_tag/batch/" . $params->getCustomTag(), $params->getApiParam());
+    }
+
+    //解绑标签
+    function unbindTag($params)
+    {
+        return $this->delete("/user/custom_tag/batch/" . $params->getCustomTag(), $params->getApiParam());
+    }
+
+    //将单个或多个用户加入黑名单,对于黑名单用户在推送过程中会被过滤掉
+    //cid:用户标识,多个以英文逗号隔开,一次最多传200个
+    function addBlackUser($params)
+    {
+        return $this->post("/user/black/cid/" . implode(",", $params), null);
+    }
+
+    //查询用户状态
+    //cid:用户标识,多个以英文逗号隔开,一次最多传200个
+    function queryUserStatus($params)
+    {
+        return $this->get("/user/status/" . implode(",", $params), null);
+    }
+
+    //将单个cid或多个cid用户移出黑名单,对于黑名单用户在推送过程中会被过滤掉的,不会给黑名单用户推送消息
+    //cid:用户标识,多个以英文逗号隔开,一次最多传200个
+    function removeBlackUser($params)
+    {
+        return $this->delete("/user/black/cid/" . implode(",", $params), null);
+    }
+
+    //设置角标
+    function setBadge($params)
+    {
+        return $this->post("/user/badge/cid/" . implode(",", $params->getCids()), $params->getApiParam());
+    }
+
+    //通过指定查询条件来查询满足条件的用户数量
+    function queryUserCount($params)
+    {
+        return $this->post("/user/count", $params->getApiParam());
+    }
+}

+ 201 - 0
addons/fastim/library/pushapi/LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 4 - 0
addons/fastim/library/pushapi/README.md

@@ -0,0 +1,4 @@
+欢迎使用[个推**PUSH** SDK For PHP](https://docs.getui.com/getui/server/rest_v2/introduction/)。
+[功能可参考该链接](https://github.com/GetuiLaboratory/getui-pushapi-php-client-v2/tree/master/test)
+
+多余内容已清理

+ 219 - 0
addons/fastim/library/pushapi/UniPush.php

@@ -0,0 +1,219 @@
+<?php
+
+namespace addons\fastim\library\pushapi;
+
+use think\Db;
+use addons\fastim\library\pushapi\GTClient;
+use addons\fastim\library\pushapi\request\push\GTPushRequest;
+use addons\fastim\library\pushapi\request\push\GTSettings;
+use addons\fastim\library\pushapi\request\push\GTPushMessage;
+use addons\fastim\library\pushapi\request\push\GTNotification;
+use addons\fastim\library\pushapi\request\push\GTPushChannel;
+use addons\fastim\library\pushapi\request\push\android\GTThirdNotification;
+use addons\fastim\library\pushapi\request\push\android\GTAndroid;
+use addons\fastim\library\pushapi\request\push\android\GTUps;
+use addons\fastim\library\pushapi\request\push\GTStrategy;
+use addons\fastim\library\pushapi\request\push\ios\GTIos;
+use addons\fastim\library\pushapi\request\push\ios\GTAps;
+use addons\fastim\library\pushapi\request\push\ios\GTAlert;
+
+class UniPush
+{
+    protected static $instance = null;
+
+    protected $config = [];
+
+    protected $apiUrl = 'https://restapi.getui.com';
+
+    /**
+     * GTClient 类实例
+     * @var null
+     */
+    protected $api = null;
+
+    protected $_error = '';
+
+    public function __construct()
+    {
+        $pushConfigDb = Db::name('fastim_config')->field('name,value')->where('group', 'message_push')->select();
+        foreach ($pushConfigDb as $key => $value) {
+            $this->config[$value['name']] = $value['value'];
+        }
+
+        $this->api = new GTClient($this->apiUrl, $this->config['uni_push_appkey'], $this->config['uni_push_appid'], $this->config['uni_push_master_secret']);
+    }
+
+    /**
+     *
+     * @return UniPush
+     */
+    public static function instance()
+    {
+        if (is_null(self::$instance)) {
+            self::$instance = new static();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * 单推
+     * @param string $cid clientid
+     * @param string $title 消息标题
+     * @param string $content 消息内容
+     * @param string $payload payload
+     * @param string $platform 用户系统平台
+     * @return bool
+     */
+    public function single($cid, $title, $content, $payload, $platform = 'android')
+    {
+        $push = $this->paramPack($title, $content, $payload, $platform);
+        $push->setCid($cid);
+
+        /*print_r($push->getApiParam());
+        return ;*/
+
+        $this->api->pushApi()->pushToSingleByCid($push);
+    }
+
+    /**
+     * 参数打包
+     * @return GTPushRequest 类实例
+     */
+    public function paramPack($title, $content, $payload, $platform)
+    {
+        $title   = mb_substr($title, 0, 20); // VIVO手机限制20个汉字
+        $content = mb_substr($content, 0, 50); // VIVO手机限制50个汉字
+
+        $push = new GTPushRequest();
+        $push->setRequestId($this->microTime());
+
+        /**
+         * 设置推送条件 setting-start
+         */
+        $set = new GTSettings();
+        $set->setTtl(3600000); // 消息离线时间
+
+        $strategy = new GTStrategy();
+        $strategy->setDefault(GTStrategy::STRATEGY_GT_FIRST);
+        $set->setStrategy($strategy);
+        $push->setSettings($set);
+        /**
+         * 设置推送条件 setting-end
+         */
+
+        /**
+         * 设置个推通道消息内容 PushMessage-start
+         */
+        $message = new GTPushMessage();
+
+        // 通知消息
+        if ($platform == 'android') {
+            $notify = new GTNotification();
+            $notify->setTitle($title);
+            $notify->setBody($content);
+
+            $notify->setChannelId("Default");
+            $notify->setChannelName("Default");
+            $notify->setChannelLevel(4);
+
+            $notify->setClickType(GTThirdNotification::CLICK_TYPE_INTENT);
+            $notify->setIntent("intent:#Intent;action=android.intent.action.oppopush;launchFlags=0x14000000;component={$this->config['package_name_android']}/io.dcloud.PandoraEntry;S.UP-OL-SU=true;S.title={$title};S.content={$content};S.payload={$payload};end");
+            $notify->setBadgeAddNum(1);
+            if (is_numeric($payload)) {
+                $notify->setNotifyId($payload);
+            }
+            $message->setNotification($notify);
+        } else {
+            // 纯透传消息
+            $message->setTransmission('{"title": "' . $title . '", "content": "' . $content . '", "payload": "' . $payload . '"}');
+        }
+
+        $push->setPushMessage($message);
+        /**
+         * 设置个推通道消息内容 PushMessage-end
+         */
+
+        /**
+         * 厂商推送消息参数 pushChannel-start
+         */
+        $pushChannel = new GTPushChannel();
+
+        // 安卓
+        $android = new GTAndroid();
+        $ups     = new GTUps();
+        // 通知消息
+        $thirdNotification = new GTThirdNotification();
+        $thirdNotification->setTitle($title);
+        $thirdNotification->setBody($content);
+        if (is_numeric($payload)) {
+            $thirdNotification->setNotifyId($payload);
+        }
+        $thirdNotification->setClickType(GTThirdNotification::CLICK_TYPE_INTENT);
+        $thirdNotification->setIntent("intent:#Intent;action=android.intent.action.oppopush;launchFlags=0x14000000;component={$this->config['package_name_android']}/io.dcloud.PandoraEntry;S.UP-OL-SU=true;S.title={$title};S.content={$content};S.payload={$payload};end");
+        $ups->setNotification($thirdNotification);
+
+        // 厂商扩展参数
+        $ups->addOption('HW', '/message/android/notification/badge/class', 'io.dcloud.PandoraEntry');
+        $ups->addOption('HW', '/message/android/notification/badge/add_num', 1);
+        $ups->addOption('VV', 'classification', 1);
+
+        $android->setUps($ups);
+        $pushChannel->setAndroid($android);
+
+        // ios
+        $ios = new GTIos();
+        $ios->setType('notify');
+        // $ios->setAutoBadge('+1');
+        $ios->setPayload((string)$payload);
+        $ios->setApnsCollapseId('collapse-id' . $payload);
+        // aps设置
+        $aps = new GTAps();
+        $aps->setContentAvailable(0);
+        $aps->setSound("com.gexin.ios.silence");
+
+        // alert
+        $alert = new GTAlert();
+        $alert->setTitle($title);
+        $alert->setBody($content);
+        $aps->setAlert($alert);
+        $ios->setAps($aps);
+        $pushChannel->setIos($ios);
+
+        $push->setPushChannel($pushChannel);
+
+        /**
+         * 厂商推送消息参数 pushChannel-end
+         */
+
+        return $push;
+    }
+
+    /**
+     * 设置错误信息
+     *
+     * @param $error 错误信息
+     * @return Auth
+     */
+    public function setError($error)
+    {
+        $this->_error = $error;
+        return $this;
+    }
+
+    /**
+     * 获取错误信息
+     * @return string
+     */
+    public function getError()
+    {
+        return $this->_error ? __($this->_error) : '';
+    }
+
+    public function microTime()
+    {
+        [$usec, $sec] = explode(" ", microtime());
+        $time = ($sec . substr($usec, 2, 3));
+        return $time;
+    }
+}

+ 16 - 0
addons/fastim/library/pushapi/composer.json

@@ -0,0 +1,16 @@
+{
+  "name": "getuilaboratory/getui-pushapi-php-client-v2",
+  "description": "getui php client V2",
+  "keywords": ["getui", "getuilaboratory", "getui-pushapi-php-client","getui-pushapi-php-client-v2", "pushapi"],
+  "homepage": "https://www.getui.com/cn/",
+  "license": "MIT",
+  "authors": [
+    {
+      "name": "getui",
+      "homepage": "https://www.getui.com/cn"
+    }
+  ],
+  "autoload": {
+    "classmap": [""]
+  }
+}

+ 34 - 0
addons/fastim/library/pushapi/exception/GTException.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace addons\fastim\library\pushapi\exception;
+
+class GTException extends Exception
+{
+    var $requestId;
+
+    //重定义构造器使第一个参数 message 变为必须被指定的属性
+    public function __construct()
+    {
+        $a = func_get_args();
+        $i = func_num_args();
+        if (method_exists($this, $f = '__construct' . $i)) {
+            call_user_func_array([$this, $f], $a);
+        }
+    }
+
+    function __construct1($a1)
+    {
+        parent::__construct($a1);
+    }
+
+    function __construct3($a1, $a2, $a3)
+    {
+        parent::__construct($a2, null, $a3);
+        $this->requestId = $a1;
+    }
+
+    public function getRequestId()
+    {
+        return $this->requestId;
+    }
+}

+ 15 - 0
addons/fastim/library/pushapi/request/GTApiRequest.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request;
+
+class GTApiRequest
+{
+
+    //api参数集合
+    protected $apiParam = [];
+
+    public function getApiParam()
+    {
+        return $this->apiParam;
+    }
+}

+ 48 - 0
addons/fastim/library/pushapi/request/auth/GTAuthRequest.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\auth;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTAuthRequest extends GTApiRequest
+{
+    //鉴权秘钥
+    private $sign;
+    //时间戳
+    private $timestamp;
+    //第三方标识
+    private $appkey;
+
+    public function getSign()
+    {
+        return $this->sign;
+    }
+
+    public function setSign($sign)
+    {
+        $this->sign             = $sign;
+        $this->apiParam["sign"] = $sign;
+    }
+
+    public function getTimestamp()
+    {
+        return $this->timestamp;
+    }
+
+    public function setTimestamp($timestamp)
+    {
+        $this->timestamp             = $timestamp;
+        $this->apiParam["timestamp"] = $timestamp;
+    }
+
+    public function getAppkey()
+    {
+        return $this->appkey;
+    }
+
+    public function setAppkey($appkey)
+    {
+        $this->appkey             = $appkey;
+        $this->apiParam["appkey"] = $appkey;
+    }
+}

+ 68 - 0
addons/fastim/library/pushapi/request/push/GTAudienceRequest.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\push;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTAudienceRequest extends GTApiRequest
+{
+    private $cidList;
+    private $aliasList;
+    private $taskid;
+    private $isAsync;
+
+    public function getCidList()
+    {
+        return $this->cidList;
+    }
+
+    public function setCidList($cidList)
+    {
+        $this->cidList = $cidList;
+    }
+
+    public function getAliasList()
+    {
+        return $this->aliasList;
+    }
+
+    public function setAliasList($aliasList)
+    {
+        $this->aliasList = $aliasList;
+    }
+
+    public function getTaskid()
+    {
+        return $this->taskid;
+    }
+
+    public function setTaskid($taskid)
+    {
+        $this->taskid             = $taskid;
+        $this->apiParam["taskid"] = $taskid;
+    }
+
+    public function getIsAsync()
+    {
+        return $this->isAsync;
+    }
+
+    public function setIsAsync($isAsync)
+    {
+        $this->isAsync              = $isAsync;
+        $this->apiParam["is_async"] = $isAsync;
+    }
+
+    public function getApiParam()
+    {
+        if ($this->aliasList != null) {
+            $audience["alias"]          = $this->aliasList;
+            $this->apiParam["audience"] = $audience;
+        }
+        if ($this->cidList != null) {
+            $audience["cid"]            = $this->cidList;
+            $this->apiParam["audience"] = $audience;
+        }
+        return $this->apiParam;
+    }
+}

+ 52 - 0
addons/fastim/library/pushapi/request/push/GTCondition.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\push;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTCondition extends GTApiRequest
+{
+    private $key;
+    private $values;
+    private $opt_type;
+
+    public function getKey()
+    {
+        return $this->key;
+    }
+
+    public function setKey($key)
+    {
+        $this->key             = $key;
+        $this->apiParam["key"] = $key;
+    }
+
+    public function getValues()
+    {
+        return $this->values;
+    }
+
+    public function setValues($values)
+    {
+        $this->values = $values;
+    }
+
+    public function getOptType()
+    {
+        return $this->opt_type;
+    }
+
+    public function setOptType($opt_type)
+    {
+        $this->opt_type             = $opt_type;
+        $this->apiParam["opt_type"] = $opt_type;
+    }
+
+    public function getApiParam()
+    {
+        if ($this->values != null) {
+            $this->apiParam["values"] = $this->values;
+        }
+        return $this->apiParam;
+    }
+}

+ 261 - 0
addons/fastim/library/pushapi/request/push/GTNotification.php

@@ -0,0 +1,261 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\push;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTNotification extends GTApiRequest
+{
+    /**
+     * 第三方厂商通知标题,长度 ≤ 50
+     */
+    private $title;
+    /**
+     * 第三方厂商通知内容,长度 ≤ 256
+     */
+    private $body;
+    //长文本消息内容,通知消息+长文本样式,与big_image二选一,两个都填写时报错,长度 ≤ 512
+    private $bigText;
+    //大图的URL地址,通知消息+大图样式, 与big_text二选一,两个都填写时报错,长度 ≤ 1024
+    private $bigImage;
+    //通知的图标名称,包含后缀名(需要在客户端开发时嵌入),如“push.png”,长度 ≤ 64
+    private $logo;
+    //通知图标URL地址,长度 ≤ 256
+    private $logoUrl;
+    //通知渠道id,长度 ≤ 64
+    private $channelId;
+    //通知渠道名称,长度 ≤ 64
+    private $channelName;
+    /** @var 设置通知渠道重要性(可以控制响铃,震动,浮动,闪灯等等)
+     * android8.0以下
+     * 0,1,2:无声音,无振动,不浮动
+     * 3:有声音,无振动,不浮动
+     * 4:有声音,有振动,有浮动
+     * android8.0以上
+     * 0:无声音,无振动,不显示;
+     * 1:无声音,无振动,锁屏不显示,通知栏中被折叠显示,导航栏无logo;
+     * 2:无声音,无振动,锁屏和通知栏中都显示,通知不唤醒屏幕;
+     * 3:有声音,无振动,锁屏和通知栏中都显示,通知唤醒屏幕;
+     * 4:有声音,有振动,亮屏下通知悬浮展示,锁屏通知以默认形式展示且唤醒屏幕;
+     */
+    private $channelLevel;
+    /**
+     * @see com.gt.sdk.dto.CommonEnum.ClickTypeEnum
+     * 点击通知后续动作,
+     * 目前支持5种后续动作,
+     * intent:打开应用内特定页面,
+     * url:打开网页地址,
+     * payload:启动应用加自定义消息内容,
+     * startapp:打开应用首页,
+     * none:纯通知,无后续动作
+     */
+    private $clickType;
+
+    /**
+     * 点击通知打开应用特定页面,长度 ≤ 2048;
+     * 示例:intent:#Intent;component=你的包名/你要打开的 activity 全路径;S.parm1=value1;S.parm2=value2;end
+     */
+    private $intent;
+    /**
+     * 点击通知打开链接,长度 ≤ 1024
+     */
+    private $url;
+    /**
+     * 点击通知加自定义消息,长度 ≤ 3072
+     */
+    private $payload;
+    /**
+     * 消息覆盖使用,两条消息的notify_id相同,新的消息会覆盖老的消息
+     */
+    private $notifyId;
+    //自定义铃声,请填写文件名,不包含后缀名(需要在客户端开发时嵌入),个推通道下发有效,客户端SDK最低要求 2.14.0.0
+    private $ringName;
+    /** @var 角标,
+     * 必须大于0, 个推通道下发有效
+     * 此属性目前仅针对华为 EMUI 4.1 及以上设备有效
+     * 角标数字数据会和之前角标数字进行叠加;
+     * 举例:角标数字配置1,应用之前角标数为2,发送此角标消息后,应用角标数显示为3。
+     * 客户端SDK最低要求 2.14.0.0
+     */
+    private $badgeAddNum;
+
+
+    public function getTitle()
+    {
+        return $this->title;
+    }
+
+    public function setTitle($title)
+    {
+        $this->title             = $title;
+        $this->apiParam["title"] = $title;
+    }
+
+    public function getBody()
+    {
+        return $this->body;
+    }
+
+    public function setBody($body)
+    {
+        $this->body             = $body;
+        $this->apiParam["body"] = $body;
+    }
+
+    public function getBigText()
+    {
+        return $this->bigText;
+    }
+
+    public function setBigText($bigText)
+    {
+        $this->bigText              = $bigText;
+        $this->apiParam["big_text"] = $bigText;
+    }
+
+    public function getBigImage()
+    {
+        return $this->bigImage;
+    }
+
+    public function setBigImage($bigImage)
+    {
+        $this->bigImage              = $bigImage;
+        $this->apiParam["big_image"] = $bigImage;
+    }
+
+    public function getLogo()
+    {
+        return $this->logo;
+    }
+
+    public function setLogo($logo)
+    {
+        $this->logo             = $logo;
+        $this->apiParam["logo"] = $logo;
+    }
+
+    public function getLogoUrl()
+    {
+        return $this->logoUrl;
+    }
+
+    public function setLogoUrl($logoUrl)
+    {
+        $this->logoUrl              = $logoUrl;
+        $this->apiParam["logo_url"] = $logoUrl;
+    }
+
+    public function getChannelId()
+    {
+        return $this->channelId;
+    }
+
+    public function setChannelId($channelId)
+    {
+        $this->channelId              = $channelId;
+        $this->apiParam["channel_id"] = $channelId;
+    }
+
+    public function getChannelName()
+    {
+        return $this->channelName;
+    }
+
+    public function setChannelName($channelName)
+    {
+        $this->channelName              = $channelName;
+        $this->apiParam["channel_name"] = $channelName;
+    }
+
+    public function getChannelLevel()
+    {
+        return $this->channelLevel;
+    }
+
+    public function setChannelLevel($channelLevel)
+    {
+        $this->channelLevel              = $channelLevel;
+        $this->apiParam["channel_level"] = $channelLevel;
+    }
+
+    public function getClickType()
+    {
+        return $this->clickType;
+    }
+
+    public function setClickType($clickType)
+    {
+        $this->clickType              = $clickType;
+        $this->apiParam["click_type"] = $clickType;
+    }
+
+    public function getIntent()
+    {
+        return $this->intent;
+    }
+
+    public function setIntent($intent)
+    {
+        $this->intent             = $intent;
+        $this->apiParam["intent"] = $intent;
+    }
+
+    public function getUrl()
+    {
+        return $this->url;
+    }
+
+    public function setUrl($url)
+    {
+        $this->url             = $url;
+        $this->apiParam["url"] = $url;
+    }
+
+    public function getPayload()
+    {
+        return $this->payload;
+    }
+
+    public function setPayload($payload)
+    {
+        $this->payload             = $payload;
+        $this->apiParam["payload"] = $payload;
+    }
+
+    public function getNotifyId()
+    {
+        return $this->notifyId;
+    }
+
+    public function setNotifyId($notifyId)
+    {
+        $this->notifyId              = $notifyId;
+        $this->apiParam["notify_id"] = $notifyId;
+    }
+
+    public function getRingName()
+    {
+        return $this->ringName;
+    }
+
+    public function setRingName($ringName)
+    {
+        $this->ringName              = $ringName;
+        $this->apiParam["ring_name"] = $ringName;
+
+    }
+
+    public function getBadgeAddNum()
+    {
+        return $this->badgeAddNum;
+    }
+
+    public function setBadgeAddNum($badgeAddNum)
+    {
+        $this->badgeAddNum               = $badgeAddNum;
+        $this->apiParam["badge_add_num"] = $badgeAddNum;
+    }
+
+
+}

+ 50 - 0
addons/fastim/library/pushapi/request/push/GTPushBatchRequest.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\push;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+
+class GTPushBatchRequest extends GTApiRequest
+{
+
+    private $isAsync;
+    private $msgList = [];
+
+    public function getIsAsync()
+    {
+        return $this->isAsync;
+    }
+
+    public function setIsAsync($isAsync)
+    {
+        $this->isAsync              = $isAsync;
+        $this->apiParam["is_async"] = $isAsync;
+    }
+
+    public function getMsgList()
+    {
+        return $this->msgList;
+    }
+
+    public function addMsgList($msg)
+    {
+        array_push($this->msgList, $msg);
+    }
+
+    public function setMsgList($msg)
+    {
+        $this->msgList = $msg;
+    }
+
+    public function getApiParam()
+    {
+        if (!empty($this->msgList)) {
+            $this->apiParam["msg_list"] = [];
+            foreach ($this->msgList as $value) {
+                array_push($this->apiParam["msg_list"], $value->getApiParam());
+            }
+        }
+        return $this->apiParam;
+    }
+}

+ 48 - 0
addons/fastim/library/pushapi/request/push/GTPushChannel.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\push;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTPushChannel extends GTApiRequest
+{
+    /**
+     * ios通道推送消息内容
+     */
+    private $ios;
+    /**
+     * android通道推送消息内容
+     */
+    private $android;
+
+    public function getIos()
+    {
+        return $this->ios;
+    }
+
+    public function setIos($ios)
+    {
+        $this->ios = $ios;
+    }
+
+    public function getAndroid()
+    {
+        return $this->android;
+    }
+
+    public function setAndroid($android)
+    {
+        $this->android = $android;
+    }
+
+    public function getApiParam()
+    {
+        if ($this->ios != null) {
+            $this->apiParam["ios"] = $this->ios->getApiParam();
+        }
+        if ($this->android != null) {
+            $this->apiParam["android"] = $this->android->getApiParam();
+        }
+        return $this->apiParam;
+    }
+}

+ 82 - 0
addons/fastim/library/pushapi/request/push/GTPushMessage.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\push;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTPushMessage extends GTApiRequest
+{
+    /**
+     * 通知展示时间段,格式为毫秒时间戳段,两个时间的时间差必须大于10分钟,例如:"1590547347000-1590633747000"
+     */
+    private $duration;
+
+    /**
+     * 个推通知消息内容,与{@link #transmission}、{@link #revoke} 三选一
+     */
+    private $notification;
+
+    /**
+     * 透传消息内容,与{@link #notification}、{@link #revoke} 三选一
+     */
+    private $transmission;
+
+    /**
+     * 撤回消息,撤回消息不能与{@link #notification}和{@link #transmission}并存
+     */
+    private $revoke;
+
+    public function getDuration()
+    {
+        return $this->duration;
+    }
+
+    public function setDuration($duration)
+    {
+        $this->duration             = $duration;
+        $this->apiParam["duration"] = $duration;
+    }
+
+    public function getNotification()
+    {
+        return $this->notification;
+    }
+
+    public function setNotification($notification)
+    {
+        $this->notification = $notification;
+    }
+
+    public function getTransmission()
+    {
+        return $this->transmission;
+    }
+
+    public function setTransmission($transmission)
+    {
+        $this->transmission             = $transmission;
+        $this->apiParam["transmission"] = $transmission;
+    }
+
+    public function getRevoke()
+    {
+        return $this->revoke;
+    }
+
+    public function setRevoke($revoke)
+    {
+        $this->revoke = $revoke;
+    }
+
+    public function getApiParam()
+    {
+        if ($this->notification != null) {
+            $this->apiParam["notification"] = $this->notification->getApiParam();
+        }
+        if ($this->revoke != null) {
+            $this->apiParam["revoke"] = $this->revoke->getApiParam();
+        }
+        return $this->apiParam;
+    }
+
+}

+ 189 - 0
addons/fastim/library/pushapi/request/push/GTPushRequest.php

@@ -0,0 +1,189 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\push;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTPushRequest extends GTApiRequest
+{
+    /**
+     * 请求唯一标识号(10-32位之间)
+     */
+    private $requestId;
+    //任务组名
+    private $groupName;
+    //推送条件设置
+    private $settings;
+    //个推推送消息参数,详细内容见http://docs.getui.com/getui/server/rest_v2/common_args/?id=doc-title-6
+    private $pushMessage;
+    //厂商推送消息参数,包含ios消息参数,android厂商消息参数,详细内容见http://docs.getui.com/getui/server/rest_v2/common_args/?id=doc-title-7
+    private $pushChannel;
+    //设置cid,用于cid单推和批量cid单推
+    private $cid;
+    //设置别名,用于别名单推和批量别名单推
+    private $alias;
+    //设置cid列表,用于tolist-cid批量推送
+    private $cidList;
+    //设置别名列表,用于tolist-别名批量推送
+    private $aliasList;
+    //对指定应用的符合筛选条件的用户群发推送消息。支持定时、定速功能。
+    private $tagList;
+    //根据标签过滤用户并推送。支持定时、定速功能。用于使用标签快速推送
+    private $fastCustomTag;
+
+    public function getFastCustomTag()
+    {
+        return $this->fastCustomTag;
+    }
+
+    public function setFastCustomTag($fastCustomTag)
+    {
+        $this->fastCustomTag = $fastCustomTag;
+    }
+
+    public function getTagList()
+    {
+        return $this->tagList;
+    }
+
+    public function setTagList($tagList)
+    {
+        $this->tagList = $tagList;
+    }
+
+    public function getAliasList()
+    {
+        return $this->aliasList;
+    }
+
+    public function setAliasList($aliasList)
+    {
+        $this->aliasList = $aliasList;
+    }
+
+    public function getCid()
+    {
+        return $this->cid;
+    }
+
+    public function setCid($cid)
+    {
+        $this->cid = $cid;
+    }
+
+    public function getAlias()
+    {
+        return $this->alias;
+    }
+
+    public function setAlias($alias)
+    {
+        $this->alias = $alias;
+
+    }
+
+    public function getCidList()
+    {
+        return $this->cidList;
+    }
+
+    public function setCidList($cidList)
+    {
+        $this->cidList = $cidList;
+    }
+
+    public function getRequestId()
+    {
+        return $this->requestId;
+    }
+
+    public function setRequestId($requestId)
+    {
+        $this->requestId              = $requestId;
+        $this->apiParam["request_id"] = $requestId;
+    }
+
+    public function getGroupName()
+    {
+        return $this->groupName;
+    }
+
+    public function setGroupName($groupName)
+    {
+        $this->groupName              = $groupName;
+        $this->apiParam["group_name"] = $groupName;
+    }
+
+    public function getSettings()
+    {
+        return $this->settings;
+    }
+
+    public function setSettings($settings)
+    {
+        $this->settings = $settings;
+    }
+
+    public function getPushMessage()
+    {
+        return $this->pushMessage;
+    }
+
+    public function setPushMessage($pushMessage)
+    {
+        $this->pushMessage = $pushMessage;
+    }
+
+    public function getPushChannel()
+    {
+        return $this->pushChannel;
+    }
+
+    public function setPushChannel($pushChannel)
+    {
+        $this->pushChannel = $pushChannel;
+    }
+
+    public function getApiParam()
+    {
+        if ($this->pushMessage != null) {
+            $this->apiParam["push_message"] = $this->pushMessage->getApiParam();
+        }
+        if ($this->pushChannel) {
+            $this->apiParam["push_channel"] = $this->pushChannel->getApiParam();
+        }
+        if ($this->settings) {
+            $this->apiParam["settings"] = $this->settings->getApiParam();
+        }
+
+        $this->apiParam["audience"] = "all";
+        if ($this->cid != null) {
+            $audience["cid"]            = [$this->cid];
+            $this->apiParam["audience"] = $audience;
+        }
+        if ($this->alias != null) {
+            $audience["alias"]          = [$this->alias];
+            $this->apiParam["audience"] = $audience;
+        }
+        if ($this->cidList != null) {
+            $audience["cid"]            = $this->cidList;
+            $this->apiParam["audience"] = $audience;
+        }
+        if ($this->aliasList != null) {
+            $audience["alias"]          = $this->aliasList;
+            $this->apiParam["audience"] = $audience;
+        }
+        if ($this->tagList != null) {
+            $audience["tag"] = [];
+            foreach ($this->tagList as $v) {
+                array_push($audience["tag"], $v->getApiParam());
+            }
+            $this->apiParam["audience"] = $audience;
+        }
+        if ($this->fastCustomTag != null) {
+            $audience["fast_custom_tag"] = $this->fastCustomTag;
+            $this->apiParam["audience"]  = $audience;
+        }
+        return $this->apiParam;
+    }
+}

+ 39 - 0
addons/fastim/library/pushapi/request/push/GTRevoke.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\push;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTRevoke extends GTApiRequest
+{
+    /**
+     * 在没有找到对应的taskid,是否把对应appid下所有的通知都撤回
+     */
+    private $force = false;
+    /**
+     * 根据oldTaskId进行撤回
+     */
+    private $oldTaskId;
+
+    public function getForce()
+    {
+        return $this->force;
+    }
+
+    public function setForce($force)
+    {
+        $this->force             = $force;
+        $this->apiParam["force"] = $force;
+    }
+
+    public function getOldTaskId()
+    {
+        return $this->oldTaskId;
+    }
+
+    public function setOldTaskId($oldTaskId)
+    {
+        $this->oldTaskId               = $oldTaskId;
+        $this->apiParam["old_task_id"] = $oldTaskId;
+    }
+}

+ 76 - 0
addons/fastim/library/pushapi/request/push/GTSettings.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\push;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTSettings extends GTApiRequest
+{
+    /**
+     * 消息离线时间设置,单位毫秒,-1表示不设离线, -1 ~ 3 * 24 * 3600 * 1000之间
+     */
+    private $ttl;
+    /**
+     * 厂商通道策略
+     */
+    private $strategy;
+    /**
+     * 推送速度
+     */
+    private $speed;
+    /**
+     * 定时推送时间,格式:毫秒时间戳
+     */
+    private $scheduleTime;
+
+    public function getTtl()
+    {
+        return $this->ttl;
+    }
+
+    public function setTtl($ttl)
+    {
+        $this->ttl             = $ttl;
+        $this->apiParam["ttl"] = $ttl;
+    }
+
+    public function getStrategy()
+    {
+        return $this->strategy;
+    }
+
+    public function setStrategy($strategy)
+    {
+        $this->strategy = $strategy;
+    }
+
+    public function getSpeed()
+    {
+        return $this->speed;
+    }
+
+    public function setSpeed($speed)
+    {
+        $this->speed             = $speed;
+        $this->apiParam["speed"] = $speed;
+    }
+
+    public function getScheduleTime()
+    {
+        return $this->scheduleTime;
+    }
+
+    public function setScheduleTime($scheduleTime)
+    {
+        $this->scheduleTime              = $scheduleTime;
+        $this->apiParam["schedule_time"] = $scheduleTime;
+    }
+
+    public function getApiParam()
+    {
+        if ($this->strategy != null) {
+            $this->apiParam["strategy"] = $this->strategy->getApiParam();
+        }
+        return $this->apiParam;
+    }
+}

+ 138 - 0
addons/fastim/library/pushapi/request/push/GTStrategy.php

@@ -0,0 +1,138 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\push;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTStrategy extends GTApiRequest
+{
+    //表示该消息在用户在线时推送个推通道,用户离线时推送厂商通道;
+    const STRATEGY_GT_FIRST = 1;
+    //表示该消息只通过厂商通道策略下发,不考虑用户是否在线;
+    const STRATEGY_THIRD_ONLY = 2;
+    //表示该消息只通过个推通道下发,不考虑用户是否在线
+    const STRATEGY_GT_ONLY = 3;
+    //表示该消息优先从厂商通道下发,若消息内容在厂商通道代发失败后会从个推通道下发。
+    const STRATEGY_THIRD_FIRST = 4;
+
+    private $default;
+    private $ios;
+    private $hw;
+    private $xm;
+    private $mz;
+    private $op;
+    private $vv;
+    private $st;
+    private $hx;
+    private $hwq;
+
+    public function getDefault()
+    {
+        return $this->default;
+    }
+
+    public function setDefault($default)
+    {
+        $this->default             = $default;
+        $this->apiParam["default"] = $default;
+    }
+
+    public function getIos()
+    {
+        return $this->ios;
+    }
+
+    public function setIos($ios)
+    {
+        $this->ios             = $ios;
+        $this->apiParam["ios"] = $ios;
+    }
+
+    public function getHw()
+    {
+        return $this->hw;
+    }
+
+    public function setHw($hw)
+    {
+        $this->hw             = $hw;
+        $this->apiParam["hw"] = $hw;
+    }
+
+    public function getXm()
+    {
+        return $this->xm;
+    }
+
+    public function setXm($xm)
+    {
+        $this->xm             = $xm;
+        $this->apiParam["xm"] = $xm;
+    }
+
+    public function getMz()
+    {
+        return $this->mz;
+    }
+
+    public function setMz($mz)
+    {
+        $this->mz             = $mz;
+        $this->apiParam["mz"] = $mz;
+    }
+
+    public function getOp()
+    {
+        return $this->op;
+    }
+
+    public function setOp($op)
+    {
+        $this->op             = $op;
+        $this->apiParam["op"] = $op;
+    }
+
+    public function getVv()
+    {
+        return $this->vv;
+    }
+
+    public function setVv($vv)
+    {
+        $this->vv             = $vv;
+        $this->apiParam["vv"] = $vv;
+    }
+
+    public function getSt()
+    {
+        return $this->st;
+    }
+
+    public function setSt($st)
+    {
+        $this->st             = $st;
+        $this->apiParam["st"] = $st;
+    }
+
+    public function getHx()
+    {
+        return $this->hx;
+    }
+
+    public function setHx($hx)
+    {
+        $this->hx             = $hx;
+        $this->apiParam["hx"] = $hx;
+    }
+
+    public function getHwq()
+    {
+        return $this->hwq;
+    }
+
+    public function setHwq($hwq)
+    {
+        $this->hwq             = $hwq;
+        $this->apiParam["hwq"] = $hwq;
+    }
+}

+ 29 - 0
addons/fastim/library/pushapi/request/push/android/GTAndroid.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\push\android;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTAndroid extends GTApiRequest
+{
+    //android厂商通道推送消息内容
+    private $ups;
+
+    public function getUps()
+    {
+        return $this->ups;
+    }
+
+    public function setUps($ups)
+    {
+        $this->ups = $ups;
+    }
+
+    public function getApiParam()
+    {
+        if ($this->ups != null) {
+            $this->apiParam["ups"] = $this->ups->getApiParam();
+        }
+        return $this->apiParam;
+    }
+}

+ 129 - 0
addons/fastim/library/pushapi/request/push/android/GTThirdNotification.php

@@ -0,0 +1,129 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\push\android;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTThirdNotification extends GTApiRequest
+{
+    const CLICK_TYPE_INTENT   = "intent";
+    const CLICK_TYPE_URL      = "url";
+    const CLICK_TYPE_PAYLOAD  = "payload";
+    const CLICK_TYPE_STAERAPP = "startapp";
+    const CLICK_TYPE_NONE     = "none";
+
+    /**
+     * 第三方厂商通知标题,长度 ≤ 50
+     */
+    private $title;
+    /**
+     * 第三方厂商通知内容,长度 ≤ 256
+     */
+    private $body;
+    /**
+     * @see com.gt.sdk.dto.CommonEnum.ClickTypeEnum
+     * 点击通知后续动作,
+     * 目前支持5种后续动作,
+     * intent:打开应用内特定页面,
+     * url:打开网页地址,
+     * payload:启动应用加自定义消息内容,
+     * startapp:打开应用首页,
+     * none:纯通知,无后续动作
+     */
+    private $clickType;
+
+    /**
+     * 点击通知打开应用特定页面,长度 ≤ 2048;
+     * 示例:intent:#Intent;component=你的包名/你要打开的 activity 全路径;S.parm1=value1;S.parm2=value2;end
+     */
+    private $intent;
+    /**
+     * 点击通知打开链接,长度 ≤ 1024
+     */
+    private $url;
+    /**
+     * 点击通知加自定义消息,长度 ≤ 3072
+     */
+    private $payload;
+    /**
+     * 消息覆盖使用,两条消息的notify_id相同,新的消息会覆盖老的消息
+     */
+    private $notifyId;
+
+    public function getTitle()
+    {
+        return $this->title;
+    }
+
+    public function setTitle($title)
+    {
+        $this->title             = $title;
+        $this->apiParam["title"] = $title;
+    }
+
+    public function getBody()
+    {
+        return $this->body;
+    }
+
+    public function setBody($body)
+    {
+        $this->body             = $body;
+        $this->apiParam["body"] = $body;
+    }
+
+    public function getClickType()
+    {
+        return $this->clickType;
+    }
+
+    public function setClickType($clickType)
+    {
+        $this->clickType              = $clickType;
+        $this->apiParam["click_type"] = $clickType;
+    }
+
+    public function getIntent()
+    {
+        return $this->intent;
+    }
+
+    public function setIntent($intent)
+    {
+        $this->intent             = $intent;
+        $this->apiParam["intent"] = $intent;
+    }
+
+    public function getUrl()
+    {
+        return $this->url;
+    }
+
+    public function setUrl($url)
+    {
+        $this->url             = $url;
+        $this->apiParam["url"] = $url;
+    }
+
+    public function getPayload()
+    {
+        return $this->payload;
+    }
+
+    public function setPayload($payload)
+    {
+        $this->payload             = $payload;
+        $this->apiParam["payload"] = $payload;
+    }
+
+    public function getNotifyId()
+    {
+        return $this->notifyId;
+    }
+
+    public function setNotifyId($notifyId)
+    {
+        $this->notifyId              = $notifyId;
+        $this->apiParam["notify_id"] = $notifyId;
+    }
+}

+ 95 - 0
addons/fastim/library/pushapi/request/push/android/GTUps.php

@@ -0,0 +1,95 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\push\android;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTUps extends GTApiRequest
+{
+    /**
+     * 通知消息内容,与transmission 二选一,两个都填写时报错
+     */
+    private $notification;
+    /**
+     * 透传消息内容,与notification 二选一,两个都填写时报错,长度 ≤ 3072
+     */
+    private $transmission;
+
+    /**
+     * 第三方厂商通知扩展内容
+     *
+     * $constraint,扩展内容对应厂商通道设置如:HW,MZ,...,不填默认ALL
+     *
+     * 厂商内容扩展字段,单个厂商特有字段,
+     * key目前支持的所有字段:
+     * hw角标设置:badgeAddNum,
+     * badgeClass要设置hw角标,这两个字段需要配合使用 ,hw的icon
+     * op私信 channel,op的消息去重 app_meaasge_id,
+     * vv的消息分类classification, 0 代表运营消息,1代表系统消息,不填默认为0。
+     * xm的channel:目前只有op和xm支持
+     *
+     * value的设置根据key值决定。例如,
+     * hw角标设置:key设为badgeAddNum,value:1(原来的角标值+1)key设为badgeClass,value:请写入角标设置的应用类名)
+     * key设为icon,value:请写⼊对应图标地址
+     */
+    private $options;
+
+    public function getNotification()
+    {
+        return $this->notification;
+    }
+
+    public function setNotification($notification)
+    {
+        $this->notification = $notification;
+    }
+
+    public function getTransmission()
+    {
+        return $this->transmission;
+    }
+
+    public function setTransmission($transmission)
+    {
+        $this->transmission             = $transmission;
+        $this->apiParam["transmission"] = $transmission;
+    }
+
+    public function getOptions()
+    {
+        return $this->options;
+    }
+
+    public function setOptions($options)
+    {
+        $this->options = $options;
+    }
+
+    public function addOption($constraint, $key, $value)
+    {
+        if ($constraint == null) {
+            $constraint = "ALL";
+        }
+        $this->options[$constraint][$key] = $value;
+    }
+    //
+    //    public function addOptions($option)
+    //    {
+    //        if ($this->options == null) {
+    //            $this->options = array($option);
+    //        } else {
+    //            array_push($this->options, $option);
+    //        }
+    //    }
+
+    public function getApiParam()
+    {
+        if ($this->notification != null) {
+            $this->apiParam["notification"] = $this->notification->getApiParam();
+        }
+        if ($this->options != null) {
+            $this->apiParam["options"] = $this->options;
+        }
+        return $this->apiParam;
+    }
+}

+ 187 - 0
addons/fastim/library/pushapi/request/push/ios/GTAlert.php

@@ -0,0 +1,187 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\push\ios;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTAlert extends GTApiRequest
+{
+    /**
+     * 通知消息标题
+     */
+    private $title;
+    /**
+     * 通知消息内容
+     */
+    private $body;
+    /**
+     * (用于多语言支持)指定执行按钮所使用的Localizable.strings
+     */
+    private $actionLocKey;
+    /**
+     * (用于多语言支持)指定Localizable.strings文件中相应的key
+     */
+    private $locKey;
+    /**
+     * 如果loc-key中使用了占位符,则在loc-args中指定各参数
+     */
+    private $locArgs;
+    /**
+     * 指定启动界面图片名
+     */
+    private $launchImage;
+    /**
+     * (用于多语言支持)对于标题指定执行按钮所使用的Localizable.strings,仅支持iOS8.2以上版本
+     */
+    private $titleLocKey;
+    /**
+     * 对于标题,如果loc-key中使用的占位符,则在loc-args中指定各参数,仅支持iOS8.2以上版本
+     */
+    private $titleLocArgs;
+    /**
+     * 通知子标题,仅支持iOS8.2以上版本
+     */
+    private $subtitle;
+    /**
+     * 当前本地化文件中的子标题字符串的关键字,仅支持iOS8.2以上版本
+     */
+    private $subtitleLocKey;
+    /**
+     * 当前本地化子标题内容中需要置换的变量参数 ,仅支持iOS8.2以上版本
+     */
+    private $subtitleLocArgs;
+
+    public function getTitle()
+    {
+        return $this->title;
+    }
+
+    public function setTitle($title)
+    {
+        $this->title             = $title;
+        $this->apiParam["title"] = $title;
+    }
+
+
+    public function getBody()
+    {
+        return $this->body;
+    }
+
+    public function setBody($body)
+    {
+        $this->body             = $body;
+        $this->apiParam["body"] = $body;
+    }
+
+    public function getActionLocKey()
+    {
+        return $this->actionLocKey;
+    }
+
+    public function setActionLocKey($actionLocKey)
+    {
+        $this->actionLocKey               = $actionLocKey;
+        $this->apiParam["action-loc-key"] = $actionLocKey;
+    }
+
+    public function getLocKey()
+    {
+        return $this->locKey;
+    }
+
+    public function setLocKey($locKey)
+    {
+        $this->locKey              = $locKey;
+        $this->apiParam["loc-key"] = $locKey;
+    }
+
+    public function getLocArgs()
+    {
+        return $this->locArgs;
+    }
+
+    public function setLocArgs($locArgs)
+    {
+        $this->locArgs = $locArgs;
+    }
+
+    public function getLaunchImage()
+    {
+        return $this->launchImage;
+    }
+
+    public function setLaunchImage($launchImage)
+    {
+        $this->launchImage              = $launchImage;
+        $this->apiParam["launch-image"] = $launchImage;
+    }
+
+    public function getTitleLocKey()
+    {
+        return $this->titleLocKey;
+    }
+
+    public function setTitleLocKey($titleLocKey)
+    {
+        $this->titleLocKey               = $titleLocKey;
+        $this->apiParam["title-loc-key"] = $titleLocKey;
+    }
+
+    public function getTitleLocArgs()
+    {
+        return $this->titleLocArgs;
+    }
+
+    public function setTitleLocArgs($titleLocArgs)
+    {
+        $this->titleLocArgs = $titleLocArgs;
+    }
+
+    public function getSubtitle()
+    {
+        return $this->subtitle;
+    }
+
+    public function setSubtitle($subtitle)
+    {
+        $this->subtitle             = $subtitle;
+        $this->apiParam["subtitle"] = $subtitle;
+    }
+
+    public function getSubtitleLocKey()
+    {
+        return $this->subtitleLocKey;
+    }
+
+    public function setSubtitleLocKey($subtitleLocKey)
+    {
+        $this->subtitleLocKey               = $subtitleLocKey;
+        $this->apiParam["subtitle-loc-key"] = $subtitleLocKey;
+    }
+
+    public function getSubtitleLocArgs()
+    {
+        return $this->subtitleLocArgs;
+    }
+
+    public function setSubtitleLocArgs($subtitleLocArgs)
+    {
+        $this->subtitleLocArgs = $subtitleLocArgs;
+    }
+
+    public function getApiParam()
+    {
+        if ($this->subtitleLocArgs != null) {
+            $this->apiParam["subtitle-loc-args"] = $this->subtitleLocArgs;
+        }
+        if ($this->titleLocArgs != null) {
+            $this->apiParam["title-loc-args"] = $this->titleLocArgs;
+        }
+        if ($this->locArgs != null) {
+            $this->apiParam["loc-args"] = $this->locArgs;
+        }
+        return $this->apiParam;
+    }
+
+}

+ 90 - 0
addons/fastim/library/pushapi/request/push/ios/GTAps.php

@@ -0,0 +1,90 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\push\ios;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTAps extends GTApiRequest
+{
+    /**
+     * 通知消息
+     */
+    private $alert;
+    /**
+     * 推送直接带有透传数据,content-available=1表示静默推送,静默推送时不需要填写其他参数,详细参数填写见示例,苹果建议1小时最多推送3条静默消息
+     */
+    private $contentAvailable;
+    /**
+     * 通知铃声文件名,无声设置为“com.gexin.ios.silence”
+     */
+    private $sound;
+    /**
+     * 在客户端通知栏触发特定的action和button显示
+     */
+    private $category;
+    //ios的远程通知通过该属性对通知进行分组,仅支持iOS 12.0以上版本
+    private $threadId;
+
+
+    public function getAlert()
+    {
+        return $this->alert;
+    }
+
+    public function setAlert($alert)
+    {
+        $this->alert = $alert;
+    }
+
+    public function getContentAvailable()
+    {
+        return $this->contentAvailable;
+    }
+
+    public function setContentAvailable($contentAvailable)
+    {
+        $this->contentAvailable              = $contentAvailable;
+        $this->apiParam["content-available"] = $contentAvailable;
+    }
+
+    public function getSound()
+    {
+        return $this->sound;
+    }
+
+    public function setSound($sound)
+    {
+        $this->sound             = $sound;
+        $this->apiParam["sound"] = $sound;
+    }
+
+    public function getCategory()
+    {
+        return $this->category;
+    }
+
+    public function setCategory($category)
+    {
+        $this->category             = $category;
+        $this->apiParam["category"] = $category;
+    }
+
+    public function getThreadId()
+    {
+        return $this->threadId;
+    }
+
+    public function setThreadId($threadId)
+    {
+        $this->threadId              = $threadId;
+        $this->apiParam["thread-id"] = $threadId;
+    }
+
+    public function getApiParam()
+    {
+        if ($this->alert != null) {
+            $this->apiParam["alert"] = $this->alert->getApiParam();
+        }
+        return $this->apiParam;
+    }
+}

+ 120 - 0
addons/fastim/library/pushapi/request/push/ios/GTIos.php

@@ -0,0 +1,120 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\push\ios;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTIos extends GTApiRequest
+{
+    /**
+     * voip:voip语音推送,notify:apns通知消息
+     */
+    private $type;
+
+    /**
+     * 推送通知消息内容
+     */
+    private $aps;
+    /**
+     * 用于计算icon上显示的数字,还可以实现显示数字的自动增减,如“+1”、 “-1”、 “1” 等,计算结果将覆盖badge
+     */
+    private $autoBadge;
+    /**
+     * 增加自定义的数据
+     */
+    private $payload;
+    /**
+     * 多媒体设置
+     */
+    private $multimedia;
+
+    //使用相同的apns-collapse-id可以覆盖之前的消息
+    private $apnsCollapseId;
+
+    public function getType()
+    {
+        return $this->type;
+    }
+
+    public function setType($type)
+    {
+        $this->type             = $type;
+        $this->apiParam["type"] = $type;
+    }
+
+    public function getAps()
+    {
+        return $this->aps;
+    }
+
+    public function setAps($aps)
+    {
+        $this->aps = $aps;
+    }
+
+    public function getAutoBadge()
+    {
+        return $this->autoBadge;
+    }
+
+    public function setAutoBadge($autoBadge)
+    {
+        $this->autoBadge              = $autoBadge;
+        $this->apiParam["auto_badge"] = $autoBadge;
+    }
+
+    public function getPayload()
+    {
+        return $this->payload;
+    }
+
+    public function setPayload($payload)
+    {
+        $this->payload             = $payload;
+        $this->apiParam["payload"] = $payload;
+    }
+
+    public function getMultimedia()
+    {
+        return $this->multimedia;
+    }
+
+    public function setMultimedia($multimedia)
+    {
+        $this->multimedia = $multimedia;
+    }
+
+    public function addMultimedia($multimedia)
+    {
+        if (empty($this->multimedia)) {
+            $this->multimedia = [$multimedia];
+        } else {
+            array_push($this->multimedia, $multimedia);
+        }
+    }
+
+    public function getApnsCollapseId()
+    {
+        return $this->apnsCollapseId;
+    }
+
+    public function setApnsCollapseId($apnsCollapseId)
+    {
+        $this->apnsCollapseId               = $apnsCollapseId;
+        $this->apiParam["apns-collapse-id"] = $apnsCollapseId;
+    }
+
+    public function getApiParam()
+    {
+        if ($this->multimedia != null) {
+            $this->apiParam["multimedia"] = [];
+            foreach ($this->multimedia as $value) {
+                array_push($this->apiParam["multimedia"], $value->getApiParam());
+            }
+        }
+        if ($this->aps != null) {
+            $this->apiParam["aps"] = $this->aps->getApiParam();
+        }
+        return $this->apiParam;
+    }
+}

+ 54 - 0
addons/fastim/library/pushapi/request/push/ios/GTMultimedia.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\push\ios;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTMultimedia extends GTApiRequest
+{
+    /**
+     * 多媒体资源地址
+     */
+    private $url;
+    /**
+     * 资源类型(1.图片,2.音频,3.视频)
+     */
+    private $type;
+    /**
+     * 是否只在wifi环境下加载,如果设置成true,但未使用wifi时,会展示成普通通知
+     */
+    private $onlyWifi;
+
+    public function getUrl()
+    {
+        return $this->url;
+    }
+
+    public function setUrl($url)
+    {
+        $this->url             = $url;
+        $this->apiParam["url"] = $url;
+    }
+
+    public function getType()
+    {
+        return $this->type;
+    }
+
+    public function setType($type)
+    {
+        $this->type             = $type;
+        $this->apiParam["type"] = $type;
+    }
+
+    public function getOnlyWifi()
+    {
+        return $this->onlyWifi;
+    }
+
+    public function setOnlyWifi($onlyWifi)
+    {
+        $this->onlyWifi              = $onlyWifi;
+        $this->apiParam["only_wifi"] = $onlyWifi;
+    }
+}

+ 38 - 0
addons/fastim/library/pushapi/request/user/GTAliasRequest.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\user;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+
+class GTAliasRequest extends GTApiRequest
+{
+    //dataList	Json Array	数据列表,数组长度不大于200
+    private $dataList = [];
+
+    public function getDataList()
+    {
+        return $this->dataList;
+    }
+
+    //添加单个CidAlias
+    public function addDataList($cidAlias)
+    {
+        array_push($this->dataList, $cidAlias);
+    }
+
+    //set CidAlias数组
+    public function setDataList($cidAliasList)
+    {
+        $this->dataList = $cidAliasList;
+    }
+
+    public function getApiParam()
+    {
+        $this->apiParam["data_list"] = [];
+        foreach ($this->dataList as $value) {
+            array_push($this->apiParam["data_list"], $value->getApiParam());
+        }
+        return $this->apiParam;
+    }
+}

+ 39 - 0
addons/fastim/library/pushapi/request/user/GTBadgeSetRequest.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\user;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTBadgeSetRequest extends GTApiRequest
+{
+
+    /** 用户应用icon上显示的数字
+     * +N: 在原有badge上+N
+     * -N: 在原有badge上-N
+     * N: 直接设置badge(数字,会覆盖原有的badge值)
+     */
+    private $badge;
+    //用户标识,多个以英文逗号隔开,一次最多传200个
+    private $cids;
+
+    public function getBadge()
+    {
+        return $this->badge;
+    }
+
+    public function setBadge($badge)
+    {
+        $this->badge             = $badge;
+        $this->apiParam["badge"] = $badge;
+    }
+
+    public function getCids()
+    {
+        return $this->cids;
+    }
+
+    public function setCids($cids)
+    {
+        $this->cids = $cids;
+    }
+}

+ 39 - 0
addons/fastim/library/pushapi/request/user/GTCidAlias.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\user;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTCidAlias extends GTApiRequest
+{
+    //cid,用户标识
+    public $cid;
+    /** 别名,有效的别名组成:
+     * 字母(区分大小写)、数字、下划线、汉字;
+     * 长度<40;
+     * 一个别名最多允许绑定10个cid。
+     */
+    public $alias;
+
+    public function getCid()
+    {
+        return $this->cid;
+    }
+
+    public function setCid($cid)
+    {
+        $this->cid             = $cid;
+        $this->apiParam["cid"] = $cid;
+    }
+
+    public function getAlias()
+    {
+        return $this->alias;
+    }
+
+    public function setAlias($alias)
+    {
+        $this->alias             = $alias;
+        $this->apiParam["alias"] = $alias;
+    }
+}

+ 41 - 0
addons/fastim/library/pushapi/request/user/GTTagBatchSetRequest.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\user;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTTagBatchSetRequest extends GTApiRequest
+{
+    //要修改标签属性的cid列表,数组长度不大于200
+    private $cid;
+    //用户标签,标签中不能包含空格,如果含有中文字符需要编码(UrlEncode)
+    private $customTag;
+
+    public function getCid()
+    {
+        return $this->cid;
+    }
+
+    public function setCid($cid)
+    {
+        $this->cid = $cid;
+    }
+
+    public function getCustomTag()
+    {
+        return $this->customTag;
+    }
+
+    public function setCustomTag($customTag)
+    {
+        $this->customTag = $customTag;
+    }
+
+    public function getApiParam()
+    {
+        if ($this->cid != null) {
+            $this->apiParam["cid"] = $this->cid;
+        }
+        return $this->apiParam;
+    }
+}

+ 34 - 0
addons/fastim/library/pushapi/request/user/GTTagSetRequest.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\user;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTTagSetRequest extends GTApiRequest
+{
+    //用户标识
+    private $cid;
+    //标签列表,标签中不能包含空格
+    private $customTag;
+
+    public function getCid()
+    {
+        return $this->cid;
+    }
+
+    public function setCid($cid)
+    {
+        $this->cid = $cid;
+    }
+
+    public function getCustomTag()
+    {
+        return $this->customTag;
+    }
+
+    public function setCustomTag($customTag)
+    {
+        $this->customTag              = $customTag;
+        $this->apiParam["custom_tag"] = $this->customTag;
+    }
+}

+ 34 - 0
addons/fastim/library/pushapi/request/user/GTUserQueryRequest.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace addons\fastim\library\pushapi\request\user;
+
+use addons\fastim\library\pushapi\request\GTApiRequest;
+
+class GTUserQueryRequest extends GTApiRequest
+{
+    private $tag = [];
+
+    public function getTag()
+    {
+        return $this->tag;
+    }
+
+    public function addTag($condition)
+    {
+        array_push($this->tag, $condition);
+    }
+
+    public function setTag($conditions)
+    {
+        $this->tag = $conditions;
+    }
+
+    public function getApiParam()
+    {
+        $this->apiParam["tag"] = [];
+        foreach ($this->tag as $value) {
+            array_push($this->apiParam["tag"], $value->getApiParam());
+        }
+        return $this->apiParam;
+    }
+}

+ 275 - 0
addons/fastim/library/pushapi/test/PushApiTest.php

@@ -0,0 +1,275 @@
+<?php
+
+namespace addons\fastim\library\pushapi\test;
+
+use addons\fastim\library\pushapi\GTClient;
+
+define("APPKEY", "*");
+define("APPID", "*");
+define("MS", "*");
+define("URL", "*");
+define("CID1", "*");
+define("CID2", "*");
+define("CID3", "*");
+
+
+$token  = null;
+$taskId = null;
+$api    = new GTClient(URL, APPKEY, APPID, MS);
+
+pushToSingleByCid();
+//pushToSingleByAlias();
+//pushBatchByCid();
+//pushBatchByAlias();
+//createListMsg();
+//pushListByCid();
+//pushListByAlias();
+//pushAll();
+//pushByTag();
+//pushByFastCustomTag();
+//stoppushApi();
+//queryScheduleTask();
+//deleteScheduleTask();
+
+
+function pushToSingleByCid()
+{
+    $push = getParam();
+    $push->setCid(CID3);
+    global $api;
+    echo json_encode($api->pushApi()->pushToSingleByCid($push));
+}
+
+function pushToSingleByAlias()
+{
+    $push = getParam();
+    $push->setAlias("cccc");
+
+    global $api;
+    echo json_encode($api->pushApi()->pushToSingleByAlias($push));
+}
+
+function pushBatchByCid()
+{
+    $batch = new GTPushBatchRequest();
+    $push  = getParam();
+    $push->setCid(CID3);
+    //    $push1 = getParam();
+    //    $push1->setCid(CID1);
+    $batch->setMsgList([$push]);
+    //    $batch->addMsgList($push1);
+    $batch->setIsAsync(false);
+
+    global $api;
+    echo json_encode($api->pushApi()->pushBatchByCid($batch));
+}
+
+function pushBatchByAlias()
+{
+    $batch = new GTPushBatchRequest();
+    $push  = getParam();
+    $push->setAlias("cccc");
+
+    $batch->addMsgList($push);
+    $batch->setIsAsync(true);
+
+    global $api;
+    echo json_encode($api->pushApi()->pushBatchByAlias($batch));
+}
+
+function createListMsg()
+{
+    $push = getParam();
+    $push->setGroupName("1202test");
+    global $api;
+    echo json_encode($api->pushApi()->createListMsg($push));
+}
+
+function pushListByCid()
+{
+    $user = new GTAudienceRequest();
+    $user->setIsAsync(true);
+    $user->setTaskid("taskid");
+    $user->setCidList([CID3]);
+    global $api;
+    echo json_encode($api->pushApi()->pushListByCid($user));
+}
+
+function pushListByAlias()
+{
+    $user = new GTAudienceRequest();
+    $user->setIsAsync(true);
+    $user->setTaskid("taskid");
+    $user->setAliasList(["cccc"]);
+    global $api;
+    echo json_encode($api->pushApi()->pushListByAlias($user));
+}
+
+function pushAll()
+{
+    $push = getParam();
+    $push->setGroupName("test");
+    global $api;
+    echo json_encode($api->pushApi()->pushAll($push));
+}
+
+function pushByTag()
+{
+    $push = getParam();
+    $tag1 = new GTCondition();
+    $tag1->setOptType("and");
+    $tag1->setKey("phone_type");
+    $tag1->setValues(["IOS"]);
+    $push->setTagList([$tag1]);
+    global $api;
+    echo json_encode($api->pushApi()->pushByTag($push));
+}
+
+function pushByFastCustomTag()
+{
+    $push = getParam();
+    $push->setFastCustomTag("tag2");
+    global $api;
+    echo json_encode($api->pushApi()->pushByFastCustomTag($push));
+}
+
+function stoppushApi()
+{
+    global $api;
+    echo json_encode($api->pushApi()->stopPush("taskid"));
+}
+
+function queryScheduleTask()
+{
+    global $api;
+    echo json_encode($api->pushApi()->queryScheduleTask("taskid"));
+}
+
+function deleteScheduleTask()
+{
+    global $api, $tasId;
+    echo json_encode($api->pushApi()->deleteScheduleTask("taskid"));
+}
+
+function getParam()
+{
+    $push = new GTPushRequest();
+    $push->setRequestId(micro_time());
+    //设置setting
+    $set = new GTSettings();
+    $set->setTtl(3600000);
+    //    $set->setSpeed(1000);
+    //    $set->setScheduleTime(1591794372930);
+    $strategy = new GTStrategy();
+    $strategy->setDefault(GTStrategy::STRATEGY_THIRD_FIRST);
+    //    $strategy->setIos(GTStrategy::STRATEGY_GT_ONLY);
+    //    $strategy->setOp(GTStrategy::STRATEGY_THIRD_FIRST);
+    //    $strategy->setHw(GTStrategy::STRATEGY_THIRD_ONLY);
+    $set->setStrategy($strategy);
+    $push->setSettings($set);
+    //设置PushMessage,
+    $message = new GTPushMessage();
+    //通知
+    $notify = new GTNotification();
+    $notify->setTitle("notdifyddd");
+    $notify->setBody("notify bdoddy");
+    $notify->setBigText("bigTdext");
+    //与big_text二选一
+    //    $notify->setBigImage("BigImage");
+
+    $notify->setLogo("push.png");
+    $notify->setLogoUrl("LogoUrl");
+    $notify->setChannelId("Default");
+    $notify->setChannelName("Default");
+    $notify->setChannelLevel(2);
+
+    $notify->setClickType("none");
+    $notify->setIntent("intent:#Intent;component=你的包名/你要打开的 activity 全路径;S.parm1=value1;S.parm2=value2;end");
+    $notify->setUrl("url");
+    $notify->setPayload("Payload");
+    $notify->setNotifyId(22334455);
+    $notify->setRingName("ring_name");
+    $notify->setBadgeAddNum(1);
+    //    $message->setNotification($notify);
+    //透传 ,与通知、撤回三选一
+    $message->setTransmission("试试透传");
+    //撤回
+    $revoke = new GTRevoke();
+    $revoke->setForce(true);
+    $revoke->setOldTaskId("taskId");
+    //    $message->setRevoke($revoke);
+    $push->setPushMessage($message);
+    $message->setDuration("1590547347000-1590633747000");
+    //厂商推送消息参数
+    $pushChannel = new GTPushChannel();
+    //ios
+    $ios = new GTIos();
+    $ios->setType("notify");
+    $ios->setAutoBadge("1");
+    $ios->setPayload("ios_payload");
+    $ios->setApnsCollapseId("apnsCollapseId");
+    //aps设置
+    $aps = new GTAps();
+    $aps->setContentAvailable(0);
+    $aps->setSound("com.gexin.ios.silenc");
+    $aps->setCategory("category");
+    $aps->setThreadId("threadId");
+
+    $alert = new GTAlert();
+    $alert->setTitle("alert title");
+    $alert->setBody("alert body");
+    $alert->setActionLocKey("ActionLocKey");
+    $alert->setLocKey("LocKey");
+    $alert->setLocArgs(["LocArgs1", "LocArgs2"]);
+    $alert->setLaunchImage("LaunchImage");
+    $alert->setTitleLocKey("TitleLocKey");
+    $alert->setTitleLocArgs(["TitleLocArgs1", "TitleLocArgs2"]);
+    $alert->setSubtitle("Subtitle");
+    $alert->setSubtitleLocKey("SubtitleLocKey");
+    $alert->setSubtitleLocArgs(["subtitleLocArgs1", "subtitleLocArgs2"]);
+    $aps->setAlert($alert);
+    $ios->setAps($aps);
+
+    $multimedia = new GTMultimedia();
+    $multimedia->setUrl("url");
+    $multimedia->setType(1);
+    $multimedia->setOnlyWifi(false);
+    $multimedia2 = new GTMultimedia();
+    $multimedia2->setUrl("url2");
+    $multimedia2->setType(2);
+    $multimedia2->setOnlyWifi(true);
+    $ios->setMultimedia([$multimedia]);
+    $ios->addMultimedia($multimedia2);
+    $pushChannel->setIos($ios);
+    //安卓
+    $android = new GTAndroid();
+    $ups     = new GTUps();
+    //    $ups->setTransmission("ups Transmission");
+    $thirdNotification = new GTThirdNotification();
+    $thirdNotification->setTitle("title" . micro_time());
+    $thirdNotification->setBody("body" . micro_time());
+    $thirdNotification->setClickType(GTThirdNotification::CLICK_TYPE_URL);
+    $thirdNotification->setIntent("intent:#Intent;component=你的包名/你要打开的 activity 全路径;S.parm1=value1;S.parm2=value2;end");
+    $thirdNotification->setUrl("http://docs.getui.com/getui/server/rest_v2/push/");
+    $thirdNotification->setPayload("payload");
+    $thirdNotification->setNotifyId(456666);
+    $ups->addOption("HW", "badgeAddNum", 1);
+    $ups->addOption("OP", "channel", "Default");
+    $ups->addOption("OP", "aaa", "bbb");
+    $ups->addOption(null, "a", "b");
+
+    $ups->setNotification($thirdNotification);
+    $android->setUps($ups);
+    $pushChannel->setAndroid($android);
+    $push->setPushChannel($pushChannel);
+
+    return $push;
+}
+
+function micro_time()
+{
+    [$usec, $sec] = explode(" ", microtime());
+    $time = ($sec . substr($usec, 2, 3));
+    return $time;
+}
+

+ 52 - 0
addons/fastim/library/pushapi/test/StatisticsApiTest.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace addons\fastim\library\pushapi\test;
+
+use addons\fastim\library\pushapi\GTClient;
+
+define("APPKEY", "*");
+define("APPID", "*");
+define("MS", "*");
+define("URL", "*");
+define("CID1", "*");
+define("CID2", "*");
+define("CID3", "*");
+
+$taskId = null;
+$api    = new GTClient(URL, APPKEY, APPID, MS);
+
+queryPushResultByTaskIds();
+queryPushResultByGroupName();
+queryUserDataByDate();
+queryOnlineUserData();
+queryPushResultByDate();
+
+function queryPushResultByTaskIds()
+{
+    global $api;
+    echo json_encode($api->statisticsApi()->queryPushResultByTaskIds(["taskid"]));
+}
+
+function queryPushResultByGroupName()
+{
+    global $api;
+    echo json_encode($api->statisticsApi()->queryPushResultByGroupName("test"));
+}
+
+function queryUserDataByDate()
+{
+    global $api;
+    echo json_encode($api->statisticsApi()->queryUserDataByDate("2020-11-30"));
+}
+
+function queryOnlineUserData()
+{
+    global $api;
+    echo json_encode($api->statisticsApi()->queryOnlineUserData());
+}
+
+function queryPushResultByDate()
+{
+    global $api;
+    echo json_encode($api->statisticsApi()->queryPushResultByDate("2020-11-30"));
+}

+ 192 - 0
addons/fastim/library/pushapi/test/UserApiTest.php

@@ -0,0 +1,192 @@
+<?php
+
+namespace addons\fastim\library\pushapi\test;
+
+use addons\fastim\library\pushapi\GTClient;
+
+define("APPKEY", "*");
+define("APPID", "*");
+define("MS", "*");
+define("URL", "*");
+define("CID1", "*");
+define("CID2", "*");
+define("CID3", "*");
+
+$token  = null;
+$taskId = null;
+$api    = new GTClient(URL, APPKEY, APPID, MS);
+
+//closeAuth();
+//别名
+bindAlias1();
+bindAlias2();
+queryAliasByCid();
+queryCidByAlias();
+unBindAlias();
+unBindAllAlias();
+
+//标签
+queryUserTag();
+setTagForCid();
+batchModifyTagForBatchCid();
+unbindTag();
+queryUserStatus();
+addBlackUser();
+removeBlackUser();
+setBadge();
+queryUserCount();
+
+function closeAuth()
+{
+    global $api;
+    echo json_encode($api->userApi()->closeAuth());
+}
+
+//用户
+function bindAlias1()
+{
+    $cidAliasListRequest = new GTAliasRequest();
+    //    $als1 = new GTCidAlias();
+    //    $als1->setCid(CID1);
+    //    $als1->setAlias("aaa");
+    $als2 = new GTCidAlias();
+    $als2->setCid(CID3);
+    $als2->setAlias("cccc");
+    //    $cidAliasListRequest->addDataList($als1);
+    $cidAliasListRequest->addDataList($als2);
+    global $api;
+    echo json_encode($api->userApi()->bindAlias($cidAliasListRequest));
+}
+
+function bindAlias2()
+{
+    $cidAliasListRequest = new GTAliasRequest();
+    $als1                = new GTCidAlias();
+    $als1->setCid(CID1);
+    $als1->setAlias("tag1");
+    $als2 = new GTCidAlias();
+    $als2->setCid(CID3);
+    $als2->setAlias("tag3");
+    $arr = [$als1, $als2];
+    $cidAliasListRequest->setDataList($arr);
+    global $api;
+    echo json_encode($api->userApi()->bindAlias($cidAliasListRequest));
+}
+
+function queryAliasByCid()
+{
+    global $api;
+    echo json_encode($api->userApi()->queryAliasByCid(CID3));
+}
+
+function queryCidByAlias()
+{
+    global $api;
+    echo json_encode($api->userApi()->queryCidByAlias("tag1"));
+}
+
+function unBindAlias()
+{
+    $cidAliasListRequest = new GTAliasRequest();
+    $als1                = new GTCidAlias();
+    $als1->setCid(CID1);
+    $als1->setAlias("aaa");
+    $cidAliasListRequest->addDataList($als1);
+    global $api;
+    echo json_encode($api->userApi()->unBindAlias($cidAliasListRequest));
+}
+
+function unBindAllAlias()
+{
+    global $api;
+    echo json_encode($api->userApi()->unBindAllAlias("tag1"));
+}
+
+function setTagForCid()
+{
+    $tags = new GTTagSetRequest();
+    $tags->setCid(CID1);
+    $array = ["tag3", "tag2", "tag4"];
+    $tags->setCustomTag($array);
+    global $api;
+    echo json_encode($api->userApi()->setTagForCid($tags));
+}
+
+function batchModifyTagForBatchCid()
+{
+    $tags = new GTTagBatchSetRequest();
+    $tags->setCustomTag("tagb");
+    $array = [CID1, CID2];
+    $tags->setCid($array);
+    global $api;
+    echo json_encode($api->userApi()->batchModifyTagForBatchCid($tags));
+}
+
+function unbindTag()
+{
+    $tags = new GTTagBatchSetRequest();
+    $tags->setCustomTag("tag3");
+    $array = [CID1];
+    $tags->setCid($array);
+    global $api;
+    $rep = $api->userApi()->unbindTag($tags);
+    echo json_encode($rep);
+}
+
+function queryUserTag()
+{
+    global $api;
+    $rep = $api->userApi()->queryUserTag(CID3);
+    echo json_encode($rep);
+}
+
+function addBlackUser()
+{
+    $array = [CID1];
+    global $api;
+    echo json_encode($api->userApi()->addBlackUser($array));
+}
+
+function queryUserStatus()
+{
+    $array = [CID1];
+    global $api;
+    echo json_encode($api->userApi()->queryUserStatus($array));
+}
+
+function removeBlackUser()
+{
+    $array = [CID1];
+    global $api;
+    echo json_encode($api->userApi()->removeBlackUser($array));
+}
+
+function setBadge()
+{
+    $param = new GTBadgeSetRequest();
+    $param->setBadge(10);
+    $array = [CID1];
+    $param->setCids($array);
+    global $api;
+    echo json_encode($api->userApi()->setBadge($param));
+}
+
+function queryUserCount()
+{
+    $param     = new GTUserQueryRequest();
+    $condition = new GTCondition();
+    $condition->setKey("custom_tag");
+    $condition->setValues(["tagb"]);
+    $condition->setOptType("and");
+    $condition1 = new GTCondition();
+    $condition1->setKey("custom_tag");
+    $condition1->setValues(["tag2"]);
+    $condition1->setOptType("and");
+    $param->setTag([$condition1]);
+    $param->addTag($condition);
+    global $api;
+    echo json_encode($api->userApi()->queryUserCount($param));
+}
+
+
+

+ 104 - 0
addons/fastim/library/pushapi/utils/GTConfig.php

@@ -0,0 +1,104 @@
+<?php
+
+namespace addons\fastim\library\pushapi\utils;
+
+class GTConfig
+{
+
+    public static function isNeedOSAsigned()
+    {
+        return "true" == GTConfig::getProperty("getui_isNeedAssign", "false");
+    }
+
+    public static function getHttpProxyIp()
+    {
+        return GTConfig::getProperty("getui_http_proxy_ip");
+    }
+
+    public static function getHttpProxyPort()
+    {
+        return (int)GTConfig::getProperty("getui_http_proxy_port", 80);
+    }
+
+    public static function getHttpProxyUserName()
+    {
+        return GTConfig::getProperty("getui_http_proxy_username");
+    }
+
+    public static function getHttpProxyPasswd()
+    {
+        return GTConfig::getProperty("getui_http_proxy_passwd");
+    }
+
+    public static function getHttpConnectionTimeOut()
+    {
+        return (int)GTConfig::getProperty("getui_http_connecton_timeout", 60000);
+    }
+
+    public static function getHttpInspectInterval()
+    {
+        return (int)GTConfig::getProperty("getui_inspect_interval", 300000);
+    }
+
+
+    public static function getHttpSoTimeOut()
+    {
+        return (int)GTConfig::getProperty("getui_http_so_timeout", 30000);
+    }
+
+    public static function getHttpTryCount()
+    {
+        return (int)GTConfig::getProperty("getui_http_tryCount", 3);
+    }
+
+    public static function getDefaultDomainUrl($useSSL)
+    {
+        $urlStr = GTConfig::getProperty("getui_default_domainurl");
+        if ($urlStr == null || "" . equals(trim($urlStr))) {
+            if ($useSSL) {
+                $hosts = [
+                    "https://restapi.getui.com",
+                    "https://cncrestapi.getui.com",
+                    "https://nzrestapi.getui.com"
+                ];
+            } else {
+                $hosts = [
+                    "http://restapi.getui.com",
+                    "http://cncrestapi.getui.com",
+                    "http://nzrestapi.getui.com"
+                ];
+            }
+        } else {
+            $list  = explode(",", $urlStr);
+            $hosts = [];
+            foreach ($list as $value) {
+                if (strpos($value, "https://") === 0 && !$useSSL) {
+                    continue;
+                }
+                if (strpos($value, "http://") === 0 && $useSSL) {
+                    continue;
+                }
+                if ($useSSL && strpos($value, "http") != 0) {
+                    $value = "https://" . $value;
+                }
+                array_push($hosts, $value);
+            }
+        }
+        return $hosts;
+    }
+
+    private static function getProperty($key, $defaultValue = null)
+    {
+        $value = getenv($key);
+        if ($value != null) {
+            return $value;
+        } else {
+            return $defaultValue;
+        }
+    }
+
+    public static function getSDKVersion()
+    {
+        return "1.0.0.0";
+    }
+}

+ 116 - 0
addons/fastim/library/pushapi/utils/GTHttpManager.php

@@ -0,0 +1,116 @@
+<?php
+
+namespace addons\fastim\library\pushapi\utils;
+
+use addons\fastim\library\pushapi\exception\GTException;
+
+class GTHttpManager
+{
+    const HTTP_METHOD_POST   = "post";
+    const HTTP_METHOD_PUT    = "put";
+    const HTTP_METHOD_GET    = "get";
+    const HTTP_METHOD_DELETE = "del";
+
+    static $curls = [];
+
+    private static function request($url, $data, $gzip, $method, $headers)
+    {
+        if (!isset(GTHttpManager::$curls[$url])) {
+            $curl                       = curl_init($url);
+            GTHttpManager::$curls[$url] = $curl;
+        }
+        $curl = GTHttpManager::$curls[$url];
+        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+        curl_setopt($curl, CURLOPT_BINARYTRANSFER, 1);
+        curl_setopt($curl, CURLOPT_USERAGENT, 'GeTui RAS2 PHP/1.0');
+        curl_setopt($curl, CURLOPT_FORBID_REUSE, 0);
+        curl_setopt($curl, CURLOPT_FRESH_CONNECT, 0);
+        curl_setopt($curl, CURLOPT_CONNECTTIMEOUT_MS, GTConfig::getHttpConnectionTimeOut());
+        curl_setopt($curl, CURLOPT_TIMEOUT_MS, GTConfig::getHttpSoTimeOut());
+        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
+        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
+        $header = null;
+        if ($headers != null) {
+            array_push($headers, "Content-Type:application/json;charset=UTF-8", "Connection: Keep-Alive");
+            $header = $headers;
+        } else {
+            $header = ["Content-Type:application/json;charset=UTF-8", "Connection: Keep-Alive"];
+        }
+        if ($gzip) {
+            $data = gzencode($data, 9);
+            array_push($header, 'Accept-Encoding:gzip');
+            array_push($header, 'Content-Encoding:gzip');
+            curl_setopt($curl, CURLOPT_ENCODING, "gzip");
+        }
+        curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
+        switch ($method) {
+            case self::HTTP_METHOD_GET:
+                curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET');
+                break;
+            case self::HTTP_METHOD_POST:
+                //post
+                curl_setopt($curl, CURLOPT_POST, 1);
+                curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
+                break;
+            case self::HTTP_METHOD_DELETE:
+                curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
+                curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
+                break;
+            case self::HTTP_METHOD_PUT:
+                curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "PUT"); //设置请求方式
+                curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
+                break;
+        }
+        $curl_version = curl_version();
+        if ($curl_version['version_number'] >= 462850) {
+            curl_setopt($curl, CURLOPT_CONNECTTIMEOUT_MS, 30000);
+            curl_setopt($curl, CURLOPT_NOSIGNAL, 1);
+        }
+        // 通过代理访问接口需要在此处配置代理
+        curl_setopt($curl, CURLOPT_PROXY, GTConfig::getHttpProxyIp());
+        curl_setopt($curl, CURLOPT_PROXYPORT, GTConfig::getHttpProxyPort());
+        curl_setopt($curl, CURLOPT_PROXYUSERNAME, GTConfig::getHttpProxyUserName());
+        curl_setopt($curl, CURLOPT_PROXYPASSWORD, GTConfig::getHttpProxyPasswd());
+        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); // return don't print
+        curl_setopt($curl, CURLOPT_TIMEOUT, 30); //设置超时时间
+        curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');
+        curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); // 302 redirect
+        curl_setopt($curl, CURLOPT_MAXREDIRS, 7); //HTTp定向级别
+        //请求失败有3次重试机会
+        $result = GTHttpManager::exeBySetTimes(GTConfig::getHttpTryCount(), $curl);
+        return $result;
+    }
+
+    public static function httpRequest($url, $params, $headers, $gzip = false, $method)
+    {
+        $data   = json_encode($params);
+        $result = null;
+        try {
+            $resp   = GTHttpManager::request($url, $data, $gzip, $method, $headers);
+            $result = json_decode($resp, true);
+            return $result;
+        } catch (Exception $e) {
+            //throw new GTException($params["request_id"], "httpPost:[" . $url . "] [" . $data . " ] [ " . $result . "]:", $e);
+            throw new Exception('Request Fail:' . $result);
+        }
+    }
+
+    private static function exeBySetTimes($count, $curl)
+    {
+        $result = curl_exec($curl);
+        $info   = curl_getinfo($curl);
+        $code   = $info["http_code"];
+        if (curl_errno($curl) != 0 && $code != 200) {
+            $count--;
+            if ($count > 0) {
+                $result = GTHttpManager::exeBySetTimes($count, $curl);
+            } else {
+                if ($code == 0 || $code == 404 || $code == 504) {
+                    //throw new GTException("connect failed, code = " . strval($code));
+                    throw new Exception("connect failed, code = " . strval($code));
+                }
+            }
+        }
+        return $result;
+    }
+}

+ 609 - 0
addons/fastim/library/swoole/Common.php

@@ -0,0 +1,609 @@
+<?php
+
+namespace addons\fastim\library\swoole;
+
+use think\Db;
+use Swoole\Table;
+use addons\fastim\library\CommonCode;
+use addons\fastim\library\Common as extCommon;
+use addons\fastim\library\pushapi\UniPush;
+
+/**
+ * Swoole 通用类
+ */
+class Common
+{
+    /**
+     * 保存了uid和fd的对应关系
+     * uid => fd1,fd2,fd3
+     * fd最大长度为 $this->fdlen
+     * 高速内存表
+     * @var Swoole\Table
+     */
+    protected $tableUid;
+
+    /**
+     * 保存了fd的相关资料
+     * 字段列表:fd,uid,tourist=是否是游客,platform=此连接的系统
+     * @var Swoole\Table
+     */
+    protected $tableFd;
+
+    /**
+     * Swoole\WebSocket\Server
+     * @var obj
+     */
+    protected $server;
+
+    /**
+     * Swoole\WebSocket\Frame
+     * @var obj
+     */
+    protected $frame;
+
+    protected static $fdlen = 32;
+
+    public function __construct($tableSize, $server)
+    {
+        $this->server = $server;
+        $this->table($tableSize);
+    }
+
+    public function table($size)
+    {
+        /**
+         * uid => fd1,fd2,fd3
+         * fd字段保存的fd总长度不超过 self::$fdlen 个字符
+         * fd从范围为从1-1600万,一个连接占一个fd
+         * 若有单用户同时打开大量窗口的需求,可以调大此值
+         */
+        $this->tableUid = new Table($size);
+        $this->tableUid->column('fd', Table::TYPE_STRING, self::$fdlen);
+        $uidCreate = $this->tableUid->create();
+
+        /**
+         * 保存了fd的相关资料
+         * fd => uid,tourist=是否是游客:1=是,2=不是,platform=此连接的系统:0=web,1=安卓,2=IOS
+         * @var Table
+         */
+        $this->tableFd = new Table($size);
+        $this->tableFd->column('uid', Table::TYPE_INT, 11);
+        $this->tableFd->column('tourist', Table::TYPE_INT, 1);
+        $this->tableFd->column('platform', Table::TYPE_INT, 1);
+        $fdCreate = $this->tableFd->create();
+
+        if (!$uidCreate || !$fdCreate) {
+            throw new Exception('create table fail! Out of memory!');
+        }
+    }
+
+    public function setFrame($frame)
+    {
+        $this->frame = $frame;
+    }
+
+    /**
+     * 检查fd字符串的长度
+     */
+    public static function checkFdlen($fdStr)
+    {
+        if (strlen($fdStr) > self::$fdlen) {
+            // 删除第一个fd
+            $fds = explode('|', $fdStr);
+            array_shift($fds);
+            $fdStr = implode('|', $fds);
+            return self::checkFdlen($fdStr);
+        } else {
+            return $fdStr;
+        }
+    }
+
+    /**
+     * 绑定uid到fd
+     * @param int $fd swooleFd
+     * @param int $uid imUserID
+     * @param int $tourist 是否是游客
+     * @param int $platform 此连接的系统
+     * @return bool
+     */
+    public function bindFd($fd, $uid, $tourist, $platform)
+    {
+        $platformArr = [
+            'web'     => 0,
+            'android' => 1,
+            'ios'     => 2
+        ];
+        $this->tableFd->set($fd, [
+            'uid'      => $uid,
+            'tourist'  => $tourist ? 1 : 2,
+            'platform' => $platformArr[$platform] ?? 0
+        ]);
+    }
+
+    /**
+     * 通过fd获得用户uid
+     * @param  [type] $fd [description]
+     * @return [type]     [description]
+     */
+    public function getUidByFd($fd)
+    {
+        $uid = $this->tableFd->get($fd, 'uid');
+        return $uid ?? false;
+    }
+
+    /**
+     * 将uid与fd绑定
+     * 高性能内存table,实现单uid对多fd的功能
+     * @param int $fd swooleFd
+     * @param int $uid imUserID
+     */
+    public function bindUid($fd, $uid)
+    {
+        $fds = $this->tableUid->get($uid);
+        if ($fds) {
+            $fds['fd'] .= '|' . $fd;
+            $this->tableUid->set($uid, [
+                'fd' => self::checkFdlen($fds['fd'])
+            ]);
+        } else {
+            $this->tableUid->set($uid, [
+                'fd' => $fd
+            ]);
+        }
+    }
+
+    /**
+     * 获取一个fd是否是游客登录的
+     * @param int $fd swooleFd
+     * @return int 1=是游客,2=不是游客
+     */
+    public function getUserIsTourist($fd)
+    {
+        $tourist = $this->tableFd->get($fd, 'tourist');
+        return $tourist ? $tourist : '1';
+    }
+
+    /**
+     * 获取一个fd的连接系统
+     * @param int $fd swooleFd
+     * @return int 0=web,1=安卓,2=IOS
+     */
+    public function getUserPlatform($fd)
+    {
+        $platform = $this->tableFd->get($fd, 'platform');
+        return $platform ?? 0;
+    }
+
+    /**
+     * 获取一个uid下绑定的所有fd
+     * @param int $uid imUserID
+     * @return array swooleFd列表
+     */
+    public function getFdByUid($uid)
+    {
+        $fds = $this->tableUid->get($uid);
+        if ($fds) {
+            $fds = explode('|', $fds['fd']);
+            return $fds;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 解除uid与fd的绑定
+     * @param int $fd swooleFd
+     * @param int $uid imUserID
+     * @return bool 是否还有fd与uid绑定
+     */
+    public function unbindUid($fd, $uid)
+    {
+        $fds = $this->tableUid->get($uid);
+        if ($fds) {
+            $fds   = explode('|', $fds['fd']);
+            $fdKey = array_search($fd, $fds);
+            if ($fdKey !== false) {
+                unset($fds[$fdKey]);
+            }
+            if ($fds) {
+                $fds = implode('|', $fds);
+                $this->tableUid->set($uid, [
+                    'fd' => $fds
+                ]);
+                return true;
+            } else {
+                $this->tableUid->del($uid);
+                return false;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 推送消息给用户
+     * @param int $uid 用户ID
+     * @param string||arrar $message 消息内容
+     */
+    public function pushMessageToUser($uid, $message)
+    {
+        $recipientIdFds = $this->getFdByUid($uid);
+        if (!$recipientIdFds) {
+            return;
+        }
+
+        foreach ($recipientIdFds as $key => $value) {
+            if ($this->server->isEstablished($value)) {
+                $this->server->push($value, json_encode($message));
+            }
+        }
+    }
+
+    /**
+     * 必填参数检查器
+     * @param array $param 要检查的参数列表
+     * @param array $data 数据
+     * @param boolean $close 检查失败是否关闭链接
+     * @param array $tip 检查失败的提示
+     * @return boolean
+     */
+    public function requiredParamCheck($param, $data, $close = false, $tip = [])
+    {
+        foreach ($param as $key => $value) {
+            if (!isset($data[$value]) || !$data[$value]) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => isset($tip[$value]) ? $tip[$value] : '重要参数丢失,请重试~',
+                    'close' => $close
+                ]));
+                if ($close) {
+                    $this->server->close($this->frame->fd);
+                }
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 发送状态消息
+     * @param int $uid 用户
+     * @param int $status 新状态:0=离线,1=上线,2=忙碌
+     */
+    public function radioStatusMessage($uid, $status)
+    {
+        // 从会话列表读取要通知的好友
+        $dbprefix    = config('database.prefix');
+        $sessionList = Db::name('fastim_session')
+            ->alias('s')
+            ->field('s.*, (SELECT createtime FROM `' . $dbprefix . 'fastim_message` `m` WHERE m.session_id=s.id ORDER BY m.createtime DESC LIMIT 1) as m_createtime')
+            ->where('s.type', 'single')
+            ->where('s.user_one|s.user_two', $uid)
+            ->where('s.deleteuser', '<>', $uid)
+            ->order('m_createtime desc, s.createtime desc')
+            ->limit(40)
+            ->select();
+        foreach ($sessionList as $key => $value) {
+            $friendID = ($value['user_one'] == $uid) ? $value['user_two'] : $value['user_one'];
+            $this->pushMessageToUser($friendID, [
+                'event' => 'update_status',
+                'data'  => [
+                    'user_id'    => $uid,
+                    'session_id' => $value['id'],
+                    'status'     => $status
+                ]
+            ]);
+        }
+    }
+
+    /**
+     * 阅读群消息
+     * @param array $sessionInfo 会话资料
+     */
+    public function readGroupMessage($sessionInfo)
+    {
+        $lastReadId = Db::name('fastim_group_user')
+            ->where('group_id', $sessionInfo['chat_id'])
+            ->where('user_id', $sessionInfo['user']['id'])
+            ->value('last_read_id');
+
+        // 当前最大消息ID
+        $maxMessageId = Db::name('fastim_message')
+            ->where('group_id', $sessionInfo['chat_id'])
+            ->where('status', '<=', 4)
+            ->max('id');
+
+        Db::name('fastim_group_user')
+            ->where('group_id', $sessionInfo['chat_id'])
+            ->where('user_id', $sessionInfo['user']['id'])
+            ->update([
+                'last_read_id' => $maxMessageId
+            ]);
+
+        Db::name('fastim_message')
+            ->where('group_id', $sessionInfo['chat_id'])
+            ->whereIn('type', implode(',', CommonCode::nonPrivilegedMessage(false)))
+            ->where('status', '<', 4)
+            ->where('id', '>', $lastReadId)
+            ->where('sender_id', '<>', $sessionInfo['user']['id'])
+            ->setInc('read_number');
+
+        $message = Db::name('fastim_message')
+            ->field('id,sender_id,read_number,message,createtime')
+            ->where('group_id', $sessionInfo['chat_id'])
+            ->whereIn('type', implode(',', CommonCode::nonPrivilegedMessage(false)))
+            ->where('status', '<', 4)
+            ->where('id', '>', $lastReadId)
+            ->where('sender_id', '<>', $sessionInfo['user']['id'])
+            ->select();
+        foreach ($message as $key => $value) {
+            $this->pushMessageToUser($value['sender_id'], [
+                'event' => 'read_message',
+                'data'  => [
+                    'sessionType' => 'group',
+                    'message_id'  => $value['id'],
+                    'status'      => ($value['read_number'] >= ($sessionInfo['sessionUser']['user_count'] - 1)) ? 6 : 5,
+                    'read_number' => $value['read_number']
+                ]
+            ]);
+        }
+
+        // 阅读无弹窗的公告
+        extCommon::readGroupChatNotice($sessionInfo['chat_id'], $sessionInfo['user']['id'], '', 'all');
+
+        // 阅读所有@消息
+        Db::name('fastim_reading_log')
+            ->where('type', 2)
+            ->where('group_id', $sessionInfo['chat_id'])
+            ->where('user_id', $sessionInfo['user']['id'])
+            ->where('receipt', 0)
+            ->update([
+                'receipt'     => 1,
+                'confirmtime' => time()
+            ]);
+    }
+
+    /**
+     * 新消息推送
+     */
+    public function newMessagePush($messageData, $sessionInfo, $recipientId)
+    {
+        // 会话置顶状态
+        $sessionInfo['top'] = Db::name('fastim_session_top')
+            ->where('user_id', $recipientId)
+            ->where('session_id', $sessionInfo['id'])
+            ->value('top');
+
+        // 接受人和发送人
+        if (!isset($sessionInfo['pushUser'])) {
+            if ($recipientId == $sessionInfo['sessionUser']['id']) {
+                $sessionInfo['pushUser']      = $sessionInfo['user'];
+                $sessionInfo['recipientUser'] = $sessionInfo['sessionUser'];
+            } else {
+                $sessionInfo['pushUser']      = $sessionInfo['sessionUser'];
+                $sessionInfo['recipientUser'] = $sessionInfo['user'];
+            }
+        }
+        unset($sessionInfo['sessionUser'], $sessionInfo['user']);
+
+        // 会话删除状态检查
+        Db::name('fastim_session')->where('id', $sessionInfo['id'])->update([
+            'deleteuser' => 0
+        ]);
+
+        // 最后消息和推送消息资料
+        $lastMessage = extCommon::getLastMessage($recipientId, $sessionInfo);
+
+        $pushTitle = isset($sessionInfo['pushUser']) ? $sessionInfo['pushUser']['nickname'] : '收到了一条新消息~';
+        if ($sessionInfo['type'] == 'single') {
+            self::offlineMessagePush($recipientId, $sessionInfo['id'], $pushTitle, $lastMessage['last_message']);
+        } elseif ($sessionInfo['type'] == 'service') {
+            self::offlineMessagePush($recipientId, 0, $pushTitle, $lastMessage['last_message']);
+        }
+
+        $this->pushMessageToUser($recipientId, [
+            'event' => 'new_message',
+            'data'  => [
+                'messageData'          => $messageData,
+                'sessionInfo'          => $sessionInfo,
+                'lastMessage'          => $lastMessage,
+                'unreadMessagesNumber' => extCommon::getUnreadMessagesNumber($recipientId, $sessionInfo),
+                'shield'               => false
+            ]
+        ]);
+    }
+
+    /**
+     * 离线消息推送-uniPush
+     */
+    public function offlineMessagePush($recipientId, $sessionId, $title, $content)
+    {
+        $sysUniPushSwitch = Db::name('fastim_config')->where('name', 'uni_push_switch')->value('value');
+        if ($sysUniPushSwitch == 0) {
+            return false;
+        }
+        $userNewMessagePushNotice = extCommon::imUserConfig($recipientId, 'new_message_push_notice');
+        if ($userNewMessagePushNotice == 0) {
+            return false;
+        }
+
+        $appConnection = false;
+        $fds           = self::getFdByUid($recipientId);
+        if ($fds) {
+            foreach ($fds as $key => $value) {
+                $platform = self::getUserPlatform($value);
+                if ($platform == 1 || $platform == 2) {
+                    $appConnection = true;
+                    break;
+                }
+            }
+        } else {
+            $appConnection = false;
+        }
+
+        if ($appConnection) {
+            return false;
+        }
+
+        $userCid = Db::name('fastim_user_push_clientid')
+            ->field('clientid, platform')
+            ->where('user_id', $recipientId)
+            ->order('updatetime desc')
+            ->select();
+        if (!$userCid) {
+            return false;
+        }
+
+        foreach ($userCid as $key => $value) {
+            // 发送通知
+            UniPush::instance()->single($value['clientid'], $title, $content, $sessionId, $value['platform']);
+        }
+    }
+
+    /**
+     * 发送消息到群聊
+     * @param  [type]  $type         消息类型
+     * @param  [type]  $message      要入库的消息数据
+     * @param boolean $noPushUser 不推送给此用户
+     * @param integer $oldMessageId 前台生成的临时消息ID
+     * @param array $atUser at的群成员数据
+     */
+    public function pushMessageToGroup($type, $messageData, $noPushUser = false, $oldMessageId = 0, $atUser = [])
+    {
+        if (!isset($messageData['group_id']) || !$messageData['group_id']) {
+            return false;
+        }
+
+        $messageData['type'] = $type; // 消息 type 通过外部传递,方便查找、识别代码
+
+        // 取得群聊中所有的用户
+        $groupUser = Db::name('fastim_group_user')->where('group_id', $messageData['group_id'])->select();
+
+        $senderUserInfo  = extCommon::getImUserInfo($messageData['sender_id']);
+        $imGroupChatInfo = extCommon::getImGroupChat($messageData['group_id']);
+
+        $speak = ($imGroupChatInfo['speak'] == 1 && $messageData['sender_id'] != $imGroupChatInfo['leader']) ? true : false;
+
+        $messageData['status']     = $speak ? 4 : 0;
+        $messageData['createtime'] = time();
+        $messageData               = extCommon::insertMessage($messageData);
+
+        if ($messageData) {
+            // 只入库不推送
+            if ($speak) {
+                return false;
+            }
+
+            // 此时推送发送成功的消息(以免成功通知在群消息阅读通知之后)
+            if ($oldMessageId) {
+                $this->server->push($this->frame->fd, json_encode([
+                    'event' => 'send_message',
+                    'data'  => [
+                        'message_id'     => $oldMessageId,
+                        'new_message_id' => $messageData['id'],
+                        'status'         => $messageData['status']
+                    ]
+                ]));
+            }
+
+            $pushMessage['messageData'] = $messageData;
+            $pushMessage['sessionInfo'] = [
+                'type'        => 'group',
+                'chat_id'     => $messageData['group_id'],
+                'sessionUser' => $imGroupChatInfo,
+                'pushUser'    => $senderUserInfo
+            ];
+
+            // 发信人群聊昵称
+            $groupUserNickname = Db::name('fastim_group_user')
+                ->where('group_id', $messageData['group_id'])
+                ->where('user_id', $messageData['sender_id'])
+                ->value('nickname');
+
+            if ($type == 'group_chat_notice') {
+                $tempNoticeMessage = $pushMessage['messageData']['message'];
+            }
+
+            // @群成员处理
+            if ($atUser) {
+                // 去除消息内容中不存在的被@用户-去除重复@的用户
+                foreach ($atUser as $key => $value) {
+                    $atNickname = '@' . trim($value['nickname'], ' ');
+                    if (mb_strpos($messageData['message'], $atNickname) !== false) {
+                        $insertAt[$value['id']] = [
+                            'type'       => 2,
+                            'group_id'   => $messageData['group_id'],
+                            'user_id'    => $value['id'],
+                            'data_id'    => $messageData['id'],
+                            'createtime' => time()
+                        ];
+                    }
+                }
+                if ($insertAt) {
+                    Db::name('fastim_reading_log')->insertAll($insertAt);
+                }
+            }
+
+            foreach ($groupUser as $key => $value) {
+                if ($noPushUser && $value['user_id'] == $noPushUser) {
+                    continue;
+                }
+
+                if ($type == 'group_chat_notice') {
+                    $pushMessage['messageData']['message'] = extCommon::formatGroupChatNotice($tempNoticeMessage, $value['user_id'], false);
+                }
+
+                $pushMessage['lastMessage'] = extCommon::getLastMessage($value['user_id'], $pushMessage['sessionInfo']);
+
+                // 获取发信人给群聊中的收信人设置的备注
+                // 不在大循环中使用 extCommon::getImUserInfo
+                $friend                                             = Db::name('fastim_friendship')
+                    ->field('user_id,remark')
+                    ->where('user_id', $value['user_id'])
+                    ->where('friend_id', $messageData['sender_id'])
+                    ->find();
+                $pushMessage['sessionInfo']['pushUser']['friend']   = $friend ? true : false;
+                $pushMessage['sessionInfo']['pushUser']['avatar']   = $senderUserInfo['avatar'];
+                $pushMessage['sessionInfo']['pushUser']['nickname'] = $senderUserInfo['nickname'];
+                $newNickname                                        = extCommon::trueAttr(extCommon::nicknameSort([
+                    $groupUserNickname,
+                    $friend['remark'] ?? '',// 加入用户群昵称
+                    $pushMessage['sessionInfo']['pushUser']['nickname']
+                ], $pushMessage['sessionInfo']['pushUser']['id']));
+
+                // 必要时重新生成收信人头像
+                if ($pushMessage['sessionInfo']['pushUser']['nickname'] != $newNickname) {
+                    $pushMessage['sessionInfo']['pushUser']['nickname'] = $newNickname;
+                    $defaultAvatar                                      = preg_match('/^data:image/', $pushMessage['sessionInfo']['pushUser']['avatar']);
+                    $pushMessage['sessionInfo']['pushUser']['avatar']   = extCommon::avatarSrc([
+                        $defaultAvatar ? '' : $pushMessage['sessionInfo']['pushUser']['avatar']
+                    ], $newNickname);
+                }
+
+                $pushMessage['sessionInfo']['id']    = $value['session_id'];
+                $pushMessage['shield']               = $value['block_messages'] ? true : false;
+                $pushMessage['unreadMessagesNumber'] = extCommon::getUnreadMessagesNumber($value['user_id'], $pushMessage['sessionInfo']);
+
+                $pushMessage['sessionInfo']['top'] = Db::name('fastim_session_top')
+                    ->where('user_id', $value['user_id'])
+                    ->where('session_id', $pushMessage['sessionInfo']['id'])
+                    ->value('top');
+
+                if (!$value['block_messages']) {
+                    self::offlineMessagePush($value['user_id'], $value['session_id'], $imGroupChatInfo['nickname'], $pushMessage['lastMessage']['last_message']);
+                }
+                self::pushMessageToUser($value['user_id'], [
+                    'event' => 'new_message',
+                    'data'  => $pushMessage
+                ]);
+
+                // 会话删除状态检查
+                Db::name('fastim_session')->where('id', $value['session_id'])->update([
+                    'deleteuser' => 0
+                ]);
+            }
+            return $messageData;
+        } else {
+            return false;
+        }
+    }
+}

+ 289 - 0
addons/fastim/library/swoole/WebSocket.php

@@ -0,0 +1,289 @@
+<?php
+
+/**
+ * FastIm v1.0.0
+ * FastAdmin企业IM客服系统
+ * https://www.fastadmin.net/store/fastim.html
+ *
+ * Copyright 2021 FastAdmin:thinkphp
+ *
+ * FastAdmin企业IM客服系统不是开源产品,所有文字、图片、样式、风格等版权归企业IM客服系统作者所有,如有复制、仿冒、抄袭、盗用,FastAdmin和企业IM客服系统作者将追究法律责任
+ *
+ * Released on: November 8, 2021
+ */
+
+namespace addons\fastim\library\swoole;
+
+use think\Db;
+use Swoole\Server\Helper;
+use Swoole\WebSocket\Frame;
+use Swoole\WebSocket\Server as WebSocketServer;
+use Exception;
+use Swoole\Timer;
+
+/**
+ * Swoole WebSocket Server 命令行服务类
+ * 此文件的修改不支持热更新,请于更新后重启swoole-websocket服务
+ */
+class WebSocket
+{
+    protected $monitor;
+    protected $lastMtime;
+    protected $config;
+
+    /**
+     * Swoole对象
+     * @var object
+     */
+    protected $swoole;
+
+    /**
+     * Socket的类型
+     * @var int
+     */
+    protected $sockType = SWOOLE_SOCK_TCP;
+
+    /**
+     * 运行模式
+     * @var int
+     */
+    protected $mode = SWOOLE_PROCESS;
+
+    /**
+     * swooleCommon 类实例
+     */
+    protected $swooleCommon;
+
+    /**
+     * 支持的响应事件
+     * @var array
+     */
+    protected $event = [
+        'Start',
+        'Shutdown',
+        'WorkerStart',
+        'WorkerStop',
+        'WorkerExit',
+        'Close',
+        'Task',
+        'Finish',
+        'PipeMessage',
+        'WorkerError',
+        'ManagerStart',
+        'ManagerStop',
+        'Open',
+        'Message',
+        'HandShake',
+        'Request'
+    ];
+
+    function __construct()
+    {
+        $this->config = get_addon_config('fastim');
+
+        if ($this->config['wss_switch']) {
+            if (file_exists($this->config['ssl_cert_file']) && file_exists($this->config['ssl_key_file'])) {
+                $this->sockType = SWOOLE_SOCK_TCP | SWOOLE_SSL;
+            } else {
+                throw new Exception('SSL certificate file does not exist!');
+            }
+        }
+
+        $this->swoole = new WebSocketServer('0.0.0.0', $this->config['websocket_port'], $this->mode, $this->sockType);
+    }
+
+    /**
+     * Worker 进程启动
+     * @param  $server
+     * @param  $worker_id
+     */
+    public function onWorkerStart($server, $worker_id)
+    {
+        $this->lastMtime = time();
+
+        if (0 == $worker_id && $this->monitor) {
+            $this->monitor($server);
+        }
+
+        // print_r(get_included_files());// 查看不支持热更新的文件列表
+
+        // 保持mysql链接可用性
+        $server->tick(27000, function () {
+            Db::execute("SELECT 1");
+        });
+    }
+
+    /**
+     * 链接握手成功
+     * @param  $server
+     * @param  $frame
+     */
+    public function onOpen($ws, $request)
+    {
+        $ws->push($request->fd, json_encode([
+            'event' => 'open'
+        ]));
+    }
+
+    /**
+     * 收到数据帧
+     * @param  $server
+     * @param  $frame
+     */
+    public function onMessage($server, $frame)
+    {
+        $data = json_decode($frame->data, true);
+        // 安全检查过滤
+        array_walk_recursive($data, ['addons\fastim\library\Common', 'checkVariable']);
+
+        if (!is_array($data) || !isset($data['c']) || !isset($data['a'])) {
+            $server->push($frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '错误的请求!',
+                'close' => true
+            ]));
+            $server->close($frame->fd);
+            return;
+        }
+
+        // 载入文件类似:根目录/addons/fastim/library/controller/index.php
+        $filename = __DIR__ . '/../controller/' . $data['c'] . '.php';
+        if (file_exists($filename)) {
+            require_once $filename;
+
+            // 检查要访问的类是否存在
+            if (!class_exists($data['c'], false)) {
+                $server->push($frame->fd, json_encode([
+                    'event' => 'show_msg',
+                    'data'  => '访问的控制器不存在!',
+                    'close' => true
+                ]));
+                $server->close($frame->fd);
+                return;
+            }
+        } else {
+            $server->push($frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '错误的请求!',
+                'close' => true
+            ]));
+            $server->close($frame->fd);
+            return;
+        }
+
+        $o = new $data['c']([$server, $frame, $this->swooleCommon]); // 新建对象
+        if (!method_exists($o, $data['a'])) {
+            $server->push($frame->fd, json_encode([
+                'event' => 'show_msg',
+                'data'  => '访问的方法不存在!'
+            ]));
+            return;
+        }
+        if ($o->authCheck($data['a'])) {
+            $data['data'] = $data['data'] ?? [];
+            call_user_func_array([$o, $data['a']], [$data['data']]); //调用对象$o($c)里的方法$a
+        }
+    }
+
+    /**
+     * 链接关闭
+     */
+    public function onClose($server, $fd, $reactorId)
+    {
+        // 解除fd绑定并修改用户状态
+        $uid = $this->swooleCommon->getUidByFd($fd);
+        if (!$this->swooleCommon->unbindUid($fd, $uid)) {
+
+            /**
+             * 用户断开1分钟后才设置为离线状态
+             * Timer::after 不同于sleep,不会阻塞
+             */
+            Timer::after(60000, function () use ($uid) {
+                if (!$this->swooleCommon->getFdByUid($uid)) {
+                    Db::name('fastim_user')->where('id', $uid)->update([
+                        'status' => 0
+                    ]);
+                    $this->swooleCommon->radioStatusMessage($uid, 0);
+                }
+            });
+        }
+    }
+
+    public function option(array $option)
+    {
+        if (!empty($option)) {
+            $this->swoole->set($this->checkOptions($option));
+        }
+
+        // 注册回调
+        foreach ($this->event as $event) {
+            if (method_exists($this, 'on' . $event)) {
+                $this->swoole->on($event, [$this, 'on' . $event]);
+            }
+        }
+
+        // 实例化swooleCommon类
+        $this->swooleCommon = new \addons\fastim\library\swoole\Common($option['max_connections'], $this->swoole);
+    }
+
+    protected function checkOptions(array $options)
+    {
+        if (class_exists(Helper::class)) {
+            $constOptions = Helper::GLOBAL_OPTIONS + Helper::SERVER_OPTIONS + Helper::PORT_OPTIONS + Helper::HELPER_OPTIONS;
+            foreach ($options as $k => $v) {
+                if (!array_key_exists(strtolower($k), $constOptions)) {
+                    unset($options[$k]);
+                }
+            }
+        }
+        return $options;
+    }
+
+    public function setMonitor($interval = 2, $path = [])
+    {
+        $this->monitor['interval'] = $interval;
+        $this->monitor['path']     = (array)$path;
+    }
+
+    /**
+     * 文件监控
+     *
+     * @param $server
+     */
+    public function monitor($server)
+    {
+        if ($this->monitor['path']) {
+            $server->tick($this->monitor['interval'], function () use ($server) {
+                foreach ($this->monitor['path'] as $path) {
+                    $dir      = new \RecursiveDirectoryIterator($path);
+                    $iterator = new \RecursiveIteratorIterator($dir);
+
+                    foreach ($iterator as $file) {
+                        if (pathinfo($file, PATHINFO_EXTENSION) != 'php') {
+                            continue;
+                        }
+
+                        if ($this->lastMtime < $file->getMTime()) {
+                            $this->lastMtime = $file->getMTime();
+                            echo '[update]' . $file . " reload...\n";
+                            $server->reload();
+                            return;
+                        }
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * 魔术方法 有不存在的操作的时候执行
+     * @access public
+     * @param string $method 方法名
+     * @param array $args 参数
+     * @return mixed
+     */
+    public function __call($method, $args)
+    {
+        call_user_func_array([$this->swoole, $method], $args);
+    }
+}

+ 124 - 0
addons/fastim/uniapp/App.vue

@@ -0,0 +1,124 @@
+<script>
+	var iosSessionId = 0;
+	import Vue from 'vue'
+	export default {
+		onLaunch: function() {
+			var that = this
+			
+			// #ifdef APP-PLUS
+			plus.push.addEventListener('click', function(msg) {
+				// console.log('用户点击了', msg)
+				// 安卓离线通知被点击可能会触发两次click,第一次的msg.payload为莫名obj
+				if ((msg.payload && Object.prototype.toString.call(msg.payload) !== '[object Object]') || iosSessionId) {
+					let messageShow = () => {
+						let sessionId = (iosSessionId ? iosSessionId:msg.payload)
+						if (parseInt(sessionId) == 0) {
+							uni.switchTab({
+								url: '/pages/message/message'
+							})
+							return ;
+						}
+						uni.navigateTo({
+							url: '/pages/session-info/session-info?id=' + sessionId
+						})
+					}
+					let pages = getCurrentPages(), page = pages[pages.length - 1]
+					if (page && page.route == 'pages/message/message') {
+						messageShow()
+					} else {
+						that.ws.messageReady = messageShow
+					}
+				}
+			}, false)
+			
+			let platform = that.ws.userPlatform ? that.ws.userPlatform:uni.getSystemInfoSync().platform
+			if (platform == 'ios') {
+				plus.push.addEventListener("receive", function(msg) {
+					if ('ignore' == msg.payload) {
+						
+					} else {
+						//接收透传消息
+						iosSessionId = msg.payload;
+						plus.push.createMessage(msg.content, 'ignore', {
+							title: msg.title,
+							cover: false
+						});
+					}
+				}, false);
+			}
+			// #endif
+			
+			uni.onNetworkStatusChange(function (res) {
+			    if (res.isConnected) {
+					const userinfo = uni.getStorageSync('userinfo');
+					if (userinfo) {
+						that.ws.init(userinfo.token, userinfo.auth_token)
+					}
+				} else {
+					that.ws.socketTask.close()
+					that.ws.socketOpen = false
+					that.ws.checkNetwork()
+				}
+			});
+		},
+		onShow: function(query) {
+			// #ifdef APP-PLUS
+			plus.runtime.setBadgeNumber(0);
+			// #endif
+			if (query.path != 'pages/center/login' && query.path != 'pages/center/register') {
+				this.ws.pageRefresh.message = true
+				this.ws.pageRefresh.sessionInfo = true
+				this.checkLogin()
+			}
+		},
+		methods: {
+			checkLogin: function () {
+				const userinfo = uni.getStorageSync('userinfo');
+				var valid = true;
+				if (!userinfo || !userinfo.token) {
+					valid = false;
+				} else {
+					let token = userinfo.token.split('|');
+					let time = Date.parse(new Date()).toString();
+					time = time.substr(0,10);
+					// 减去一秒,防止刚好到时间造成发送了错误的请求
+					if ((parseInt(token[2]) - 2) < parseInt(time)) {
+						valid = false;
+					}
+				}
+				
+				if (!valid) {
+					setTimeout(() => {
+						this.ws.logout()
+					}, 300)
+				} else {
+					this.ws.init(userinfo.token, userinfo.auth_token)
+				}
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	@import "uview-ui/index.scss";
+	
+	.user-status-0 {
+	    color: $--gray !important;
+	}
+	.user-status-1 {
+	    color: #21E589 !important;
+	}
+	.user-status-2 {
+	    color: #FF647E !important;
+	}
+	.user-status-3 {
+	    color: #FF9C4E !important;
+	}
+	.im-data-none {
+		display: block;
+		line-height: 100rpx;
+		text-align: center;
+		font-size: 28rpx;
+		color: $--gray;
+	}
+</style>

+ 12 - 0
addons/fastim/uniapp/common/config.js

@@ -0,0 +1,12 @@
+export default {
+	baseUrl: 'example.com', // 启动 swoole 服务的域名,无需填写协议和端口
+	httpsSwitch: false, // 是否启用https协议(默认关,小程序和app正式版必开,且需要参考文档创建wss服务)
+	httPort: '', // HTTP端口,若为默认的80端口则无需填写,请注意是`http`端口,不是`ws`端口,ws请在后台插件配置中进行配置
+	// 无需登录的api
+	noNeedLogin: [
+		'/api/sms/send',
+		'/addons/fastim/api.user/login',
+		'/addons/fastim/api.user/register',
+		'/addons/fastim/api.user/captchaPre',
+	],
+}

+ 10 - 0
addons/fastim/uniapp/common/css/common.scss

@@ -0,0 +1,10 @@
+$--white: #ffffff;
+$--black: #3F3F3F;
+$--grey: #EBEBEB;
+$--gray: #999999;
+$--dark: #1a1a1a;
+$--light: #f4f4f4;
+$--blue: #6388fb;
+$--red: #f74c31;
+$--light-blue: #9EB9FD;
+$--bg-color: #F8F8F8;

+ 36 - 0
addons/fastim/uniapp/common/http.interceptor.js

@@ -0,0 +1,36 @@
+import imConfig from "./config.js"; // 本地配置数据
+var config = {
+	baseUrl: (imConfig.httpsSwitch ? 'https://':'http://') + imConfig.baseUrl, // 请求的域名
+	method: 'POST',
+	// 设置为json,返回后会对数据进行一次JSON.parse()
+	dataType: 'json',
+	showLoading: true, // 是否显示请求中的 loading
+	loadingText: '稍等片刻...', // 请求loading中的文字提示
+	loadingTime: 800, // 在此时间内,请求还没回来的话,就显示加载中动画,单位ms
+	originalData: false, // 是否在拦截器中返回服务端的原始数据
+	loadingMask: true, // 展示loading的时候,是否给一个透明的蒙层,防止触摸穿透
+	// 配置请求头信息
+	header: {
+		'content-type': 'application/json;charset=UTF-8'
+	},
+}
+
+const install = (Vue, vm) => {
+	Vue.prototype.$u.http.setConfig(config);
+	
+	Vue.prototype.$u.http.interceptor.request = (config) => {
+		const userinfo = uni.getStorageSync('userinfo');
+		if (!imConfig.noNeedLogin.includes(config.url)) {
+			if (userinfo) {
+				config.header.token = userinfo.token;
+			} else {
+				return false;// 不再请求,app.vue内有延时的登录跳转
+			}
+		}
+		return config;
+	}
+}
+
+export default {
+	install
+}

+ 2728 - 0
addons/fastim/uniapp/common/websocket.js

@@ -0,0 +1,2728 @@
+import imConfig from "./config.js"; // 本地配置数据
+var ws = {
+	that: null,
+	pageThat: null,
+	socketTask: null,
+	socketOpen: false,
+	ready: false,
+	needReconnect: true,
+	sessionId: 0,
+	timer: null, // 全局计时器
+	reconnecting: false,
+	errorMsg: [], // 发送失败的消息
+	maxReconnectCount: 5, // 最大重连次数
+	currentReconnectCount: 0,
+	initializeData: false, // 初始化请求来的基础数据
+	initializeEmoji: false,
+	connectSuccess: null,
+	showMsgCallback: null,
+	messageShow: [],
+	pageRefresh: {
+		message: false,
+		addressList: false
+	},
+	authToken: '',
+	userPlatform: null,
+	innerAudioContext: null,
+	recorder: null,
+	onMessageCallBack: new Map(),
+	init: function(token, auth_token = '') {
+		var that = this
+		if (this.socketTask && this.socketOpen) {
+			// console.log('无需链接 ws')
+			return false;
+		}
+		
+		that.authToken = auth_token ? auth_token:that.authToken
+		if (!this.initializeData) {
+			// 发送初始化请求
+			this.that.$u.get(this.buildUrl('initialize', token), {}).then(res => {
+				
+				if (res.code == 402) {
+					uni.clearStorageSync()
+					that.pageRefresh.message = true
+					uni.reLaunch({
+						url: '/pages/center/login'
+					})
+					return false;
+				} else if (res.code != 1) {
+					uni.showModal({
+						title: '温馨提示',
+						content: '初始化失败,请重试!',
+						showCancel: false
+					})
+					return false;
+				}
+
+				that.initializeData = {
+					config: res.data.config,
+					tokens: res.data.tokens,
+					userinfo: res.data.userinfo
+				}
+				
+				// 来信提示音初始化
+				that.innerAudioContext = uni.createInnerAudioContext();
+				that.innerAudioContext.src = that.buildUrl('message_prompt', that.initializeData.tokens.im_tourists_token);
+				
+				that.recorder = uni.getRecorderManager();
+				
+				that.connect()
+			})
+			
+			this.userPlatform = uni.getSystemInfoSync().platform
+		} else {
+			that.connect()
+		}
+	},
+	connect: function() {
+		var that = this
+		if (imConfig.httpsSwitch && parseInt(that.initializeData.config.wss_switch) != 1) {
+			uni.showModal({
+				title: '温馨提示',
+				content: that.initializeData.config.im_name + ' https下须创建wss服务才能连接网络,请参考文档!',
+				showCancel: false
+			})
+			return false;
+		}
+
+		// 开始链接 ws
+		that.socketTask = uni.connectSocket({
+			url: that.buildUrl('ws'),
+			header: {
+				'content-type': 'application/json'
+			},
+			complete: res => {}
+		});
+
+		that.socketTask.onOpen(function(res) {
+			console.log('链接已打开')
+			that.socketOpen = true
+			that.currentReconnectCount = 0;
+			that.needReconnect = true;
+			that.pageThat.commonTips = ''
+
+			if (that.timer != null) {
+				clearInterval(that.timer);
+			}
+
+			that.timer = setInterval(function() {
+				that.send({
+					c: 'ImBase',
+					a: 'ping'
+				})
+			}, 28000); //定时发送心跳
+		});
+
+		that.socketTask.onMessage(function(res) {
+			let msg = JSON.parse(res.data)
+			that.onMessage(msg);
+		});
+
+		that.socketTask.onError(function(res) {
+			that.socketOpen = false;
+			that.reconnecting = false;
+			console.log('链接出错', res)
+			
+			if (that.timer != null) {
+				clearInterval(that.timer);
+			}
+			
+			that.pageThat.commonTips = '网络不给力,正在自动重连~'
+			
+			that.reconnect(); // 重连
+		});
+
+		// 链接关闭
+		that.socketTask.onClose(function(res) {
+			console.log('链接已关闭', res)
+			that.socketOpen = false;
+			that.reconnecting = false;
+			if (that.timer != null) {
+				clearInterval(that.timer);
+			}
+			
+			that.pageThat.commonTips = '网络不给力,正在自动重连~'
+			
+			if (typeof that.closeCallback == 'function') {
+				that.closeCallback()
+				that.closeCallback = null
+			}
+
+			that.reconnect(); // 重连
+		});
+	},
+	reconnect: function() {
+		if (!this.needReconnect || this.reconnecting) {
+			return false;
+		}
+		this.reconnecting = true
+		var that = this
+
+		if (this.currentReconnectCount < this.maxReconnectCount) {
+			this.currentReconnectCount++;
+			if (this.currentReconnectCount == 1) {
+				that.init();
+				console.log('正在重连 WebSocket 第' + this.currentReconnectCount + '次');
+			} else {
+				console.log('6秒后重连 WebSocket 第' + this.currentReconnectCount + '次');
+				this.timer = setTimeout(function() {
+					that.init();
+					console.log('正在重连 WebSocket 第' + that.currentReconnectCount + '次');
+				}, 6000)
+			}
+		} else {
+			if (this.timer != null) {
+				clearInterval(this.timer);
+			}
+			console.log('18秒后将再次尝试重连 WebSocket')
+			this.timer = setTimeout(() => {
+				console.log('正在重连...')
+				that.init()
+			}, 18000); //每18秒重新连接一次
+		}
+	},
+	send: function(message) {
+		var that = this
+		if (!message) {
+			return;
+		}
+
+		let noNeedLogin = [
+			'ImBaselogin'
+		];
+
+		if (!noNeedLogin.includes(message.c + message.a) && !that.ready) {
+			uni.showToast({
+				title: '请稍等网络连接成功后再试哦~',
+				icon: 'none',
+				mask: true
+			})
+			return;
+		}
+
+		if (that.socketTask && that.socketOpen) {
+			that.socketTask.send({
+				data: JSON.stringify(message),
+				fail: res => {
+					console.log('消息发送出错', message, res)
+					that.errorMsg.push(message);
+				}
+			});
+		} else {
+			console.log('消息发送出错-ws链接异常', message, that.socketTask, that.socketOpen)
+			that.errorMsg.push(message);
+		}
+	},
+	sendMessage: function(message, type, sessionId) {
+		var messageId = new Date().getTime() + sessionId + Math.floor(Math.random() * 10000), that = this
+		that.send({
+			c: 'Message',
+			a: 'sendMessage',
+			data: {
+				message: message,
+				type: type,
+				session_id: sessionId,
+				tokens: that.initializeData ? that.initializeData.tokens : false, // 发消息时检测用户登录态是否过期
+				message_id: messageId,
+				identity: that.initializeData.userinfo.identity
+			}
+		})
+	},
+	formatMessage: function(data) {
+		var message = ''
+		if (data.type == 'image') {
+			message = '[图片]';
+		} else if (data.type == 'audio') {
+			message = '[音频]';
+		} else if (data.type == 'voice') {
+			message = '[语音消息]';
+		} else if (data.type == 'video') {
+			message = '[视频]';
+		} else if (data.type == 'file') {
+			message = '[文件]';
+		} else if (data.type == 'link') {
+			message = '[链接]';
+		} else if (data.type == 'kbs_list') {
+			message = data.message.title;
+		} else {
+			message = data.message.replace(/<img.*?title="(.+?)".*?>/g, "[$1]");
+			message = message.replace(/<img.*?src="(.+?)".*?>/g, "[图片]");
+		}
+		return message;
+	},
+	imSession: function(data, pageThat, moveTop = true) {
+		// message页数据保障
+		var currentSessionIndex = -1;
+		if (data.sessionInfo.top) {
+			for (let m in pageThat.sessionListTop) {
+				if (pageThat.sessionListTop[m].id == data.sessionInfo.id) {
+					currentSessionIndex = m;
+					pageThat.sessionListTop[m].unreadMessagesNumber = (data.unreadMessagesNumber !== false) ? data.unreadMessagesNumber:pageThat.sessionListTop[m].unreadMessagesNumber
+					if (data.unreadMessagesNumber === 0) {
+						pageThat.sessionListTop[m].unread_fixed_msg = ''
+					}
+					if (data.lastMessage) {
+						pageThat.sessionListTop[m].last_time = data.lastMessage.last_time
+						pageThat.sessionListTop[m].last_message = data.lastMessage.last_message
+						pageThat.sessionListTop[m].unread_fixed_msg = data.lastMessage.unread_fixed_msg ? data.lastMessage.unread_fixed_msg.message:''
+					}
+					break;
+				}
+			}
+		} else {
+			for (let m in pageThat.sessionList) {
+				if (pageThat.sessionList[m].id == data.sessionInfo.id) {
+					currentSessionIndex = m;
+					pageThat.sessionList[m].unreadMessagesNumber = (data.unreadMessagesNumber !== false) ? data.unreadMessagesNumber:pageThat.sessionList[m].unreadMessagesNumber
+					if (data.unreadMessagesNumber === 0) {
+						pageThat.sessionList[m].unread_fixed_msg = ''
+					}
+					if (data.lastMessage) {
+						pageThat.sessionList[m].last_time = data.lastMessage.last_time
+						pageThat.sessionList[m].last_message = data.lastMessage.last_message
+						pageThat.sessionList[m].unread_fixed_msg = data.lastMessage.unread_fixed_msg ? data.lastMessage.unread_fixed_msg.message:''
+					}
+					break;
+				}
+			}
+		}
+		
+		// 使用 `splice` 操作数组,会造成数据渲染异常/重复
+		// 改用 `filter`、`unshift`来调整会话顺序
+		
+		if (currentSessionIndex !== -1) {
+			
+			if (moveTop) {
+				if (data.sessionInfo.top) {
+					let currentSessionTemp = pageThat.sessionListTop[currentSessionIndex]
+					pageThat.sessionListTop = pageThat.sessionListTop.filter(item => {
+						return item.id != data.sessionInfo.id;
+					})
+					pageThat.sessionListTop.unshift(currentSessionTemp);
+				} else {
+					let currentSessionTemp = pageThat.sessionList[currentSessionIndex]
+					pageThat.sessionList = pageThat.sessionList.filter(item => {
+						return item.id != data.sessionInfo.id;
+					})
+					pageThat.sessionList.unshift(currentSessionTemp);
+				}
+			}
+		} else {
+			// 组装会话资料,建立会话
+			let sessionItem = {}
+			if (data.sessionInfo.type == 'single') {
+				if (data.sessionInfo.pushUser.status) {
+					let statusValue = parseInt(data.sessionInfo.pushUser.status.value)
+					if (statusValue == 0) {
+						sessionItem.avatar_gray = 'im-img-gray'
+						sessionItem.user_status = '[离线]'
+					} else if (statusValue == 2) {
+						sessionItem.avatar_gray = ''
+						sessionItem.user_status = '[忙碌]'
+					} else {
+						sessionItem.avatar_gray = ''
+						sessionItem.user_status = ''
+					}
+				}
+			}
+			
+			sessionItem.id = data.sessionInfo.id
+			sessionItem.type = data.sessionInfo.type
+			sessionItem.chat_id = data.sessionInfo.chat_id
+			sessionItem.avatar = this.imgUrl(data.sessionInfo.pushUser.avatar)
+			sessionItem.nickname = data.sessionInfo.pushUser.nickname
+			sessionItem.top = data.sessionInfo.top ? 'session-top' : ''
+			sessionItem.last_time = data.lastMessage.last_time
+			sessionItem.last_message = data.lastMessage.last_message
+			sessionItem.shield = data.shield
+			sessionItem.unreadMessagesNumber = data.unreadMessagesNumber
+			
+			if (data.sessionInfo.top) {
+				pageThat.sessionListTop.unshift(sessionItem);
+			} else {
+				// 将会话移动到非置顶会话的第一位
+				pageThat.sessionList.unshift(sessionItem);
+			}
+			
+			this.pageRefresh.addressList = true
+		}
+	},
+	newMessageNotice: function(nickname, lastMessage, noticeAvatar, ringing = true){
+		// 新消息通知
+		var that = this
+		
+		// 震动
+		if (parseInt(that.initializeData.config.user_config.new_message_shake) == 1) {
+			uni.vibrateLong({
+			    success: function () {}
+			});
+		}
+		
+		// 铃声
+		if (ringing && parseInt(that.initializeData.config.user_config.new_message_sound) == 1) {
+			that.newMessageRinging()
+		}
+	},
+	newMessageRinging: function() {
+		if (this.innerAudioContext) {
+			this.innerAudioContext.play();
+			setTimeout(() => {
+				this.innerAudioContext.stop();
+			}, 1500)
+		} else {
+			console.error('来信提示音播放失败!');
+		}
+	},
+	pushCid: function(type = 'save'){
+		if (parseInt(this.initializeData.config.uni_push_switch) == 0) {
+			return false;
+		}
+		// #ifdef APP-PLUS
+		var callBack = (info) => {
+			this.send({
+				c: 'User',
+				a: 'pushCid',
+				data: {
+					clientid: info.clientid,
+					platform: this.userPlatform,
+					type: type
+				}
+			});
+		}
+		
+		let info = plus.push.getClientInfo();
+		if (info && info.clientid) {
+			callBack(info)
+		} else {
+			var obtainingCIDTimer = setInterval(() => {
+				info = plus.push.getClientInfo();
+				if (info && info.clientid) {
+					callBack(info)
+					clearInterval(obtainingCIDTimer);
+				}
+			}, 50)
+		}
+		// #endif
+	},
+	onMessage: function(msg) {
+		var that = this
+
+		var commonCallback = function() {
+			if (msg.data && msg.data.msg && that.pageThat) {
+				that.pageThat.$refs.uToast.show({
+					title: msg.data.msg,
+					type: 'default'
+				})
+			}
+			if (typeof that.showMsgCallback == 'function') {
+				that.showMsgCallback()
+				that.showMsgCallback = null
+				return ;
+			}
+			that.pageRefresh.message = true
+			that.pageRefresh.addressList = true
+			setTimeout(function() {
+				uni.navigateBack({
+					delta: 1
+				})
+			}, 2000)
+		}
+
+		var msgFun = new Map([
+			['open', () => {
+				that.initializeData.tokens.auth_token = that.authToken
+				let message = {
+					c: 'ImBase',
+					a: 'login',
+					data: {
+						'tokens': that.initializeData.tokens,
+						'identity': that.initializeData.userinfo.identity,
+						'platform': that.userPlatform
+					}
+				}
+				that.send(message);
+			}],
+			['login_success', () => {
+				// 标记连接成功
+				that.ready = true;
+				that.pageThat.commonTips = ''
+				typeof that.connectSuccess == 'function' && that.connectSuccess()
+				that.connectSuccess = null
+				
+				// 重新发送所有出错的消息
+				for (let i in that.errorMsg) {
+					that.send(that.errorMsg[i]);
+				}
+				that.errorMsg = [];
+				
+				// 单设备重复在线检测(IOS微信小程序重连后会有单实例多个连接的情况,且在同一句柄上无法关闭)
+				// #ifdef MP-WEIXIN
+				if (that.userPlatform == 'ios') {
+					that.send({
+						c: 'ImBase',
+						a: 'equipmentInspection',
+						data: {
+							type: 'check',
+							time: Date.now()
+						}
+					});
+				}
+				// #endif
+				
+				that.pushCid()
+			}],
+			['pong', () => {}],
+			['logout', () => {
+				that.logout()
+			}],
+			['equipment-inspection', () => {
+				let time = msg.data.time.toString()
+				if (that.equipmentLastInspectionTime == time) {
+					that.send({
+						c: 'ImBase',
+						a: 'equipmentInspection',
+						data: {
+							type: 'close',
+							fd: msg.data.fd
+						}
+					});
+				}
+				that.equipmentLastInspectionTime = time
+			}],
+			['show_msg', () => {
+				if (msg.data && that.pageThat.$refs.uToast) {
+					if (Object.prototype.toString.call(msg.data) === '[object Object]') {
+						that.pageThat.$refs.uToast.show({
+							title: msg.data.msg,
+							type: (msg.data.type == 'tips') ? 'info' : msg.data.type
+						})
+					} else {
+						that.pageThat.$refs.uToast.show({
+							title: msg.data,
+							type: 'default'
+						})
+					}
+				} else {
+					uni.showToast({
+						title: (Object.prototype.toString.call(msg.data) === '[object Object]') ? msg.data.msg:msg.data,
+						icon: 'none'
+					})
+				}
+				if (msg.close) {
+					console.log('收服务端要求禁止自动重新连接ws!');
+					that.needReconnect = false;
+				}
+				typeof that.showMsgCallback == 'function' && that.showMsgCallback()
+				that.showMsgCallback = null
+			}],
+			['user_all_fast_reply', () => {
+				that.initializeData.config.fast_reply = msg.data
+			}],
+			['forward_message', () => {
+				var reloadRecord = false
+				for (var i = 0; i < msg.data.session_ids.length; i++) {
+					reloadRecord = (reloadRecord || (msg.data.session_ids[i] == parseInt(msg.data.data.session_id))) ? true:false
+					for (var y = 0; y < msg.data.message.length; y++) {
+						that.sendMessage(msg.data.message[y].message, msg.data.message[y].type, msg.data.session_ids[i])
+					}
+				}
+				
+				that.pageThat.$refs.uToast.show({
+					title: (msg.data.data.type == 'collection' ? '收藏':'消息') + '已转发~',
+					type: 'success'
+				})
+				
+				that.pageRefresh.message = true
+				setTimeout(res => {
+					uni.navigateBack({
+						delta: 1
+					})
+					if (reloadRecord) {
+						that.pageRefresh.sessionInfo = true
+					}
+				}, 2000)
+			}],
+			['message_operation', () => {
+				if (msg.data.method == 'show_msg') {
+					that.pageThat.$refs.uToast.show({
+						title: msg.data.msg,
+						type: (msg.data.type == 'tips') ? 'info' : msg.data.type
+					})
+				} else {
+					if (msg.data.data.action == 'message-copy') {
+						uni.setClipboardData({
+							data: msg.data.messageInfo.messageText,
+							success: function () {
+								that.pageThat.$refs.uToast.show({
+									title: '复制成功',
+									type: 'success'
+								})
+								uni.hideToast()
+							},
+							fail: function () {
+								that.pageThat.$refs.uToast.show({
+									title: '复制失败,请重试!',
+									type: 'error'
+								})
+							}
+						});
+					} else if (msg.data.data.action == 'message-delete') {
+						// 删除消息
+						for (let d in that.pageThat.messageList) {
+							let index = that.pageThat.messageList[d].data.findIndex(e => {
+								return e.id == parseInt(msg.data.data.id);
+							})
+							if (index !== -1) {
+								that.pageThat.messageList[d].data.splice(index, 1)
+								// 修改会话列表中的最后消息
+								that.messageShow.push((pageThat = that.pageThat) => {
+									that.imSession({
+										sessionInfo: {
+											id: that.pageThat.info.id,
+											top: that.pageThat.info.top
+										},
+										lastMessage: {
+											last_time: msg.data.messageInfo.last_time,
+											last_message: msg.data.messageInfo.last_message
+										},
+										unreadMessagesNumber: false
+									}, pageThat, false)
+								})
+								break;
+							}
+						}
+					}
+				}
+				that.pageThat.maskClick()
+			}],
+			['load_session_list', () => {
+				var session = msg.data.session
+				if (msg.data.data.method == 'load') {
+					if (session.length) {
+						var sessionList = [], sessionListTop = []
+						for (var i = 0; i < session.length; i++) {
+							let sessionItem = {}
+							if (session[i].sessionInfo.pushUser.status) {
+								let statusValue = parseInt(session[i].sessionInfo.pushUser.status.value)
+								if (statusValue == 0) {
+									sessionItem.avatar_gray = 'im-img-gray'
+									sessionItem.user_status = '[离线]'
+								} else if (statusValue == 2) {
+									sessionItem.avatar_gray = ''
+									sessionItem.user_status = '[忙碌]'
+								} else {
+									sessionItem.avatar_gray = ''
+									sessionItem.user_status = ''
+								}
+							}
+							
+							sessionItem.id = session[i].sessionInfo.id
+							sessionItem.type = session[i].sessionInfo.type
+							sessionItem.chat_id = session[i].sessionInfo.chat_id
+							sessionItem.shield = session[i].sessionInfo.shield ? session[i].sessionInfo.shield:false
+							sessionItem.avatar = that.imgUrl(session[i].sessionInfo.pushUser.avatar)
+							sessionItem.nickname = session[i].sessionInfo.pushUser.nickname
+							sessionItem.top = session[i].sessionInfo.top ? 'session-top' : ''
+							sessionItem.unreadMessagesNumber = session[i].unreadMessagesNumber
+							sessionItem.last_time = session[i].lastMessage.last_time
+							sessionItem.last_message = session[i].lastMessage.last_message
+							sessionItem.unread_fixed_msg = session[i].lastMessage.unread_fixed_msg ? session[i].lastMessage.unread_fixed_msg.message:''
+							
+							if (!sessionItem.top) {
+								sessionList.push(sessionItem)
+							} else {
+								sessionListTop.push(sessionItem)
+							}
+						}
+						that.pageThat.sessionList = sessionList
+						that.pageThat.sessionListTop = sessionListTop
+						that.pageThat.loadStatus = false;
+					} else {
+						that.pageThat.loadStatus = '没有更多会话了...'
+					}
+				}
+				
+				if (typeof that.messageReady == 'function') {
+					setTimeout(() => {
+						that.messageReady()
+						that.messageReady = null
+					}, 200)
+				}
+			}],
+			['load_to_do', () => {
+				if (msg.data.data.method == 'get-all-count') {
+					that.pageThat.TODOcount = msg.data.count
+				} else {
+					that.pageThat.loadTODO = msg.data.data.nextpage ? msg.data.data : false;
+					for (var i = 0; i < msg.data.completed.length; i++) {
+						msg.data.completed[i].checked = true
+					}
+
+					if (parseInt(msg.data.data.page) == 1) {
+						that.pageThat.completedNumber = msg.data.data.completed
+						for (var i = 0; i < msg.data.incomplete.length; i++) {
+							msg.data.incomplete[i].checked = false
+						}
+						that.pageThat.incomplete = msg.data.incomplete
+						that.pageThat.completed = msg.data.completed
+					} else {
+						that.pageThat.completed = that.pageThat.completed.concat(msg.data.completed)
+					}
+				}
+			}],
+			['user_search', () => {
+				if (msg.data.data.type == 'all') {
+					for (var i = 0; i < msg.data.friends.length; i++) {
+						msg.data.friends[i].avatar = that.imgUrl(msg.data.friends[i].avatar)
+						if (msg.data.friends[i].remark) {
+							msg.data.friends[i].nickname = msg.data.friends[i].remark + ' (' + msg
+								.data.friends[i].nickname_origin + ')';
+						}
+					}
+					that.pageThat.friends = msg.data.friends
+
+					for (var i = 0; i < msg.data.group_chat.length; i++) {
+						msg.data.group_chat[i].avatar = that.imgUrl(msg.data.group_chat[i].avatar)
+					}
+					that.pageThat.group_chat = msg.data.group_chat
+
+					for (var i = 0; i < msg.data.lately_session.length; i++) {
+						msg.data.lately_session[i].id = msg.data.lately_session[i].sessionInfo
+							.pushUser.id
+						msg.data.lately_session[i].avatar = that.imgUrl(msg.data.lately_session[i]
+							.sessionInfo.pushUser.avatar)
+						msg.data.lately_session[i].nickname = msg.data.lately_session[i].sessionInfo
+							.pushUser.nickname
+						msg.data.lately_session[i].last_time = msg.data.lately_session[i]
+							.lastMessage.last_time
+					}
+					that.pageThat.lately_session = msg.data.lately_session
+				} else if (msg.data.data.type == 'pick-user') {
+					// 已经选择的用户
+					var userIds = []
+					for (var i in that.pageThat.friends) {
+						for (let y in that.pageThat.friends[i]) {
+							if (that.pageThat.friends[i][y].checked) {
+								userIds.push(that.pageThat.friends[i][y].friend_id)
+							}
+						}
+					}
+
+					that.pageThat.showSearchPopup = true
+					for (var i = 0; i < msg.data.friends.length; i++) {
+						msg.data.friends[i].checked = userIds.includes(msg.data.friends[i].id);
+						msg.data.friends[i].avatar = that.imgUrl(msg.data.friends[i].avatar)
+					}
+					that.pageThat.searchRes = msg.data.friends
+				}
+			}],
+			['load_contact', () => {
+				that.pageThat.indexList = msg.data.res.initialPinyinIndex
+
+				for (let item in msg.data.res.initialPinyinArr) {
+					for (var i = 0; i < msg.data.res.initialPinyinArr[item].length; i++) {
+						if (that.pageThat.preselection && msg.data.res.initialPinyinArr[item][i].friend_id == that.pageThat.preselection) {
+							msg.data.res.initialPinyinArr[item][i].checked = true
+						} else {
+							msg.data.res.initialPinyinArr[item][i].checked = false
+						}
+						msg.data.res.initialPinyinArr[item][i].avatar = that.imgUrl(msg.data.res
+							.initialPinyinArr[item][i].avatar)
+					}
+				}
+				that.pageThat.friends = msg.data.res.initialPinyinArr
+			}],
+			['create_new_contact', () => {
+				var buttons = [],
+					showEl = {
+						group: true,
+						remark: true,
+						note: true,
+						tis: false
+					};
+				msg.data.info.avatar = that.imgUrl(msg.data.info.avatar)
+
+				if (msg.data.data.type == 'friends') {
+					msg.data.info.icon = that.imgUrl('/assets/addons/fastim/icon/' + msg.data.info
+						.gender.value + '.png');
+
+					if (parseInt(msg.data.info.temp_session) == 1) {
+						// 添加直接聊天的按钮
+						buttons.push({
+							type: 'user',
+							action: 'open-session',
+							btype: 'error',
+							title: '仅聊天'
+						})
+					}
+
+					// 预同意
+					if (msg.data.data.action && msg.data.data.action == 'pre-agree') {
+						msg.data.info.id = msg.data.data.message_id
+						showEl.note = false
+						buttons.push({
+							type: 'user',
+							action: 'submit-new-contact-form',
+							btype: 'primary',
+							opt: 'agree-friends',
+							title: '同意申请'
+						})
+					} else if (parseInt(msg.data.info.verify_method) == 1) {
+						showEl.note = false
+						showEl.tis = '添加TA为好友无需验证'
+						buttons.push({
+							type: 'user',
+							action: 'submit-new-contact-form',
+							btype: 'success',
+							opt: 'apply',
+							title: '加为好友'
+						})
+					} else if (parseInt(msg.data.info.verify_method) == 0) {
+						buttons.push({
+							type: 'user',
+							action: 'submit-new-contact-form',
+							btype: 'primary',
+							opt: 'apply',
+							title: '发送申请'
+						})
+					}
+				} else if (msg.data.data.type == 'group') {
+					msg.data.info.icon = that.imgUrl('/assets/addons/fastim/icon/group.png')
+					msg.data.info.other = '群账号:' + msg.data.info.id
+					showEl.remark = false
+					if (parseInt(msg.data.info.add_mode) == 0) {
+						// 需要验证
+						buttons.push({
+							type: 'group',
+							action: 'submit-new-contact-form',
+							btype: 'primary',
+							opt: 'apply',
+							title: '申请加群'
+						})
+					} else if (parseInt(msg.data.info.add_mode) == 1) {
+						// 无需验证
+						showEl.note = false
+						showEl.tis = '加入该群无需管理员验证'
+						buttons.push({
+							type: 'group',
+							action: 'submit-new-contact-form',
+							btype: 'success',
+							opt: 'apply',
+							title: '加入群聊'
+						})
+					}
+				}
+
+				that.pageThat.showEl = showEl
+				that.pageThat.buttons = buttons
+				that.pageThat.info = msg.data.info
+			}],
+			['info-detail', () => {
+				msg.data.info.avatar = that.imgUrl(msg.data.info.avatar)
+				if (msg.data.data.method == 'user-edit') {
+					// 编辑用户资料
+					var occupationData = []
+					for (var i = 0; i < msg.data.info.occupationData.length; i++) {
+						occupationData.push({
+							value: i,
+							label: msg.data.info.occupationData[i],
+							extra: '7'
+						})
+					}
+
+					var genderValue = [{
+							value: 'secrecy',
+							label: '保密',
+							extra: '2'
+						},
+						{
+							value: 'male',
+							label: '男',
+							extra: '2'
+						},
+						{
+							value: 'female',
+							label: '女',
+							extra: '2'
+						}
+					];
+
+					var genderValueIndex = 0;
+					for (var i = 0; i < genderValue.length; i++) {
+						if (genderValue[i].value == msg.data.info.gender.value) {
+							genderValueIndex = i
+						}
+					}
+
+					that.pageThat.showAvatarUpload = true
+					that.pageThat.avatarFileList = [{
+						url: msg.data.info.avatar
+					}]
+					if (!msg.data.info.birthday) {
+						var date = new Date();
+						msg.data.info.birthday = [date.getFullYear(), date.getMonth() + 1, date.getDate()]
+					}
+					that.pageThat.detail = [{
+							title: '昵  称',
+							placeholderTitle: '昵称',
+							type: 'input',
+							name: 'nickname',
+							value: msg.data.info.nickname
+						},
+						{
+							title: '签  名',
+							placeholderTitle: '签名',
+							type: 'textarea',
+							name: 'bio',
+							value: msg.data.info.bio
+						},
+						{
+							title: '性  别',
+							type: 'select',
+							name: 'gender',
+							data: genderValue,
+							value: [genderValueIndex],
+							show: false
+						},
+						{
+							title: '生  日',
+							placeholderTitle: '生日',
+							type: 'date',
+							value: msg.data.info.birthday,
+							name: 'birthday',
+							show: false
+						},
+						{
+							title: '邮  箱',
+							placeholderTitle: '邮箱',
+							type: 'input',
+							name: 'email',
+							value: msg.data.info.email
+						},
+						{
+							title: '手  机',
+							placeholderTitle: '手机',
+							type: 'input',
+							name: 'mobile',
+							value: msg.data.info.mobile
+						},
+						{
+							title: '公  司',
+							placeholderTitle: '公司',
+							type: 'input',
+							name: 'company',
+							value: msg.data.info.company
+						},
+						{
+							title: '职  业',
+							type: 'select',
+							name: 'occupation',
+							data: occupationData,
+							value: [msg.data.info.occupation],
+							show: false
+						}
+					]
+				} else if (msg.data.data.method == 'group-edit') {
+					// 编辑群组资料
+					that.pageThat.showAvatarUpload = true
+					that.pageThat.avatarFileList = [{
+						url: msg.data.info.avatar
+					}]
+
+					var addModeValueIndex = 0,
+						inviteJoinGroupIndex = 0,
+						historyMessageIndex = 0,
+						speakIndex = 0,
+						retrievalIndex= 0;
+
+					uni.setNavigationBarTitle({
+						title: pageTitle
+					});
+
+					// 加群模式
+					var addModeValue = [{
+							value: '0',
+							label: '需管理员审核',
+							extra: '2'
+						},
+						{
+							value: '1',
+							label: '无需审核',
+							extra: '2'
+						}
+					];
+					for (var i = 0; i < addModeValue.length; i++) {
+						if (addModeValue[i].value == msg.data.info.add_mode) {
+							addModeValueIndex = i
+						}
+					}
+
+					// 邀请免审
+					var inviteJoinGroup = [{
+							value: '0',
+							label: '成员邀请好友需审核',
+							extra: '3'
+						},
+						{
+							value: '1',
+							label: '成员邀请好友免审核',
+							extra: '3'
+						},
+					];
+					for (var i = 0; i < inviteJoinGroup.length; i++) {
+						if (inviteJoinGroup[i].value == msg.data.info.invite_join_group) {
+							inviteJoinGroupIndex = i
+						}
+					}
+
+					// 历史消息
+					var historyMessage = [{
+							value: '0',
+							label: '不允许新入群用户查看',
+							extra: '4'
+						},
+						{
+							value: '1',
+							label: '允许新入群用户查看',
+							extra: '4'
+						},
+					];
+					for (var i = 0; i < historyMessage.length; i++) {
+						if (historyMessage[i].value == msg.data.info.history_message) {
+							historyMessageIndex = i
+						}
+					}
+
+					// 成员发言
+					var speak = [{
+							value: '0',
+							label: '允许发言',
+							extra: '5'
+						},
+						{
+							value: '1',
+							label: '禁止发言',
+							extra: '5'
+						},
+					];
+					for (var i = 0; i < speak.length; i++) {
+						if (speak[i].value == msg.data.info.speak) {
+							speakIndex = i
+						}
+					}
+					
+					// 检索设置
+					var retrieval = [{
+						value: '0',
+						label: '禁用搜素加群',
+						extra: '6'
+					},
+					{
+						value: '1',
+						label: '允许被搜素到',
+						extra: '6'
+					}];
+					for (var i = 0; i < retrieval.length; i++) {
+						if (parseInt(retrieval[i].value) == msg.data.info.retrieval_settings) {
+							retrievalIndex = i
+						}
+					}
+
+					that.pageThat.detail = [{
+							title: '群  名',
+							placeholderTitle: '群名',
+							type: 'input',
+							name: 'nickname',
+							value: msg.data.info.nickname
+						},
+						{
+							title: '群聊简介',
+							type: 'textarea',
+							name: 'bio',
+							value: msg.data.info.bio
+						},
+						{
+							title: '加群模式',
+							type: 'select',
+							name: 'add_mode',
+							data: addModeValue,
+							value: [addModeValueIndex],
+							show: false
+						},
+						{
+							title: '邀请免审',
+							type: 'select',
+							name: 'invite_join_group',
+							data: inviteJoinGroup,
+							value: [inviteJoinGroupIndex],
+							show: false
+						},
+						{
+							title: '历史消息',
+							type: 'select',
+							name: 'history_message',
+							data: historyMessage,
+							value: [historyMessageIndex],
+							show: false
+						},
+						{
+							title: '成员发言',
+							type: 'select',
+							name: 'speak',
+							data: speak,
+							value: [speakIndex],
+							show: false
+						},
+						{
+							title: '检索设置',
+							type: 'select',
+							name: 'retrieval_settings',
+							data: retrieval,
+							value: [retrievalIndex],
+							show: false
+						}
+					];
+				} else {
+					// 查看资料
+					if (msg.data.data.method == 'get-new-info') {
+						that.pageRefresh.message = true
+						that.pageRefresh.addressList = true
+					}
+
+					var pageTitle = '查看资料'
+					if (msg.data.data.type == 'user') {
+						// 用户资料
+						if (msg.data.info.remark) {
+							msg.data.info.nickname = msg.data.info.nickname_origin
+						}
+
+						that.pageThat.detail = [{
+								title: '账号',
+								value: msg.data.info.id,
+								name: 'id'
+							},
+							{
+								title: '个人',
+								value: (parseInt(msg.data.info.age) ? msg.data.info.age + '岁 ' :
+									'') + msg.data.info.gender.chinese
+							},
+							{
+								title: '生日',
+								value: msg.data.info.birthday
+							},
+							{
+								title: '邮箱',
+								value: msg.data.info.email
+							},
+							{
+								title: '手机',
+								value: msg.data.info.mobile
+							},
+							{
+								title: '公司',
+								value: msg.data.info.company
+							},
+							{
+								title: '职业',
+								value: msg.data.info.occupation
+							}
+						]
+
+						for (let d in that.pageThat.detail) {
+							that.pageThat.detail[d].value = that.pageThat.detail[d].value ? that
+								.pageThat.detail[d].value : '-'
+						}
+
+						if (parseInt(msg.data.data.requestor) == parseInt(msg.data.info.id)) {
+							pageTitle = '我的资料';
+							that.pageThat.buttons = [{
+								action: 'userinfo-opt',
+								type: 'user',
+								data: msg.data.info.id,
+								btype: 'default',
+								opt: 'edit',
+								name: '编辑资料'
+							}];
+						} else if (msg.data.info.friend) {
+							// 发送消息
+							that.pageThat.buttons = [{
+									action: 'open-session',
+									type: 'user',
+									data: msg.data.info.id,
+									btype: 'success',
+									name: '发送消息'
+								},
+								{
+									action: 'del-contact',
+									type: 'user',
+									data: msg.data.info.id,
+									btype: 'error',
+									name: '删除好友'
+								}
+							];
+						} else {
+							that.pageThat.buttons = [{
+								action: 'add-friends',
+								type: 'user',
+								data: msg.data.info.id,
+								btype: 'primary',
+								name: '加为好友'
+							}];
+						}
+						msg.data.info.oldRemark = msg.data.info.remark
+						that.pageThat.info = msg.data.info
+					} else if (msg.data.data.type == 'group') {
+						// 群组资料
+						msg.data.info.avatar = that.imgUrl(msg.data.info.avatar)
+						pageTitle = '群聊资料';
+						msg.data.info.other = '群账号:' + msg.data.info.id
+						msg.data.info.type = 'group'
+						that.pageThat.info = msg.data.info
+
+						var detail = [];
+						if (msg.data.info.leader) {
+							msg.data.info.leader.avatar = that.imgUrl(msg.data.info.leader.avatar)
+							detail.push({
+								title: '群主',
+								type: 'leader',
+								leader: msg.data.info.leader
+							})
+						}
+
+						detail.push({
+							title: '群人数',
+							type: 'text',
+							value: msg.data.info.user_count + '/' + msg.data.info
+								.max_user_count
+						})
+
+						if (typeof msg.data.info.add_mode != 'undefined') {
+							detail.push({
+								title: '加群模式',
+								value: (parseInt(msg.data.info.add_mode) == 0) ? '需管理员审核' :
+									'无需审核'
+							})
+						}
+						if (typeof msg.data.info.invite_join_group != 'undefined') {
+							detail.push({
+								title: '邀请模式',
+								value: (parseInt(msg.data.info.invite_join_group) == 0) ?
+									'成员邀请好友需要审核' : '成员邀请好友无需审核'
+							})
+						}
+						if (typeof msg.data.info.history_message != 'undefined') {
+							detail.push({
+								title: '历史消息',
+								value: (parseInt(msg.data.info.history_message) == 0) ?
+									'不允许新入群用户查看' : '允许新入群用户查看'
+							})
+						}
+						detail.push({
+							title: '成员发言',
+							value: (parseInt(msg.data.info.speak) == 0) ? '允许发言' : '禁止发言'
+						}, {
+							title: '创建时间',
+							value: msg.data.info.createtime
+						})
+						that.pageThat.detail = detail
+
+						if (msg.data.info.isLeader) {
+							if (msg.data.info.deletetime) {
+								return;
+							}
+							that.pageThat.buttons = [{
+									action: 'userinfo-opt',
+									type: 'group',
+									data: msg.data.info.id,
+									btype: 'default',
+									opt: 'edit',
+									name: '编辑资料'
+								},
+								{
+									action: 'del-contact',
+									type: 'dissolution-group',
+									data: msg.data.info.id,
+									btype: 'error',
+									name: '解散群聊'
+								},
+							];
+						} else if (!msg.data.info.groupMember) {
+							// 加入群聊
+							that.pageThat.buttons = [{
+								action: 'userinfo-opt',
+								type: 'group',
+								data: msg.data.info.id,
+								btype: 'primary',
+								opt: 'join',
+								name: '加入群聊'
+							}];
+						} else {
+							that.pageThat.buttons = [{
+									action: 'open-session',
+									type: 'group',
+									data: msg.data.info.id,
+									btype: 'success',
+									name: '发送消息'
+								},
+								{
+									action: 'del-contact',
+									type: 'group',
+									data: msg.data.info.id,
+									btype: 'error',
+									name: '退出群聊'
+								},
+							];
+						}
+					} else if (msg.data.data.type == 'service') {
+						// 服务号资料
+						msg.data.info.avatar = that.imgUrl(msg.data.info.avatar)
+						pageTitle = '服务号资料';
+						msg.data.info.other = '服务号'
+						msg.data.info.type = 'service'
+						that.pageThat.info = msg.data.info
+						
+						var detail = [];
+						detail.push({
+							title: '个人',
+							type: 'text',
+							value: '-'
+						}, {
+							title: '邮箱',
+							type: 'text',
+							value: '-'
+						}, {
+							title: '手机',
+							type: 'text',
+							value: '-'
+						})
+						that.pageThat.detail = detail
+						
+						that.pageThat.buttons = [{
+							action: 'close',
+							btype: 'default',
+							name: ' 关闭 ',
+							plain: true
+						}];
+					}
+					uni.setNavigationBarTitle({
+						title: pageTitle
+					});
+				}
+			}],
+			['center', () => {
+				msg.data.info.avatar = that.imgUrl(msg.data.info.avatar)
+				that.pageThat.info = msg.data.info
+				that.pageThat.TODOCount = msg.data.TODOCount
+				that.pageThat.collectionCount = msg.data.collectionCount
+			}],
+			['change_status', () => {
+				that.pageThat.info.status = {
+					value: parseInt(msg.data.status),
+					chinese: msg.data.status_chinese
+				}
+			}],
+			['read_message', () => {
+				var that = this
+				let pages = getCurrentPages(), page = pages[pages.length - 1]
+				if (page.route == 'pages/session-info/session-info') {
+					if (msg.data.sessionType == 'group') {
+						for (let m in that.pageThat.messageList) {
+							for (let y in that.pageThat.messageList[m].data) {
+								if (that.pageThat.messageList[m].data[y].id == msg.data.message_id) {
+									that.pageThat.messageList[m].data[y].status = that.messageStatus(msg.data.status, msg.data.message_id, msg.data.read_number);
+									break;
+								}
+							}
+						}
+					} else if (parseInt(msg.data.session_id) == parseInt(that.pageThat.id)) {
+						for (let m in that.pageThat.messageList) {
+							for (let y in that.pageThat.messageList[m].data) {
+								if (that.pageThat.messageList[m].data[y].sender == 'me' && that.pageThat.messageList[m].data[y].status.status != '已读') {
+									that.pageThat.messageList[m].data[y].status = that.messageStatus(1, 'all');
+								}
+							}
+						}
+					}
+				}
+			}],
+			['update_status', () => {
+				// 更新好友的状态
+				var that = this
+				let pages = getCurrentPages(), page = pages[pages.length - 1]
+				var statusText = '', status = parseInt(msg.data.status);
+				if (status == 0) {
+					statusText = '离线'
+				} else if (status == 2) {
+					statusText = '忙碌'
+				}
+				
+				var messageShow = function (pageThat = that.pageThat) {
+					statusText = (status == 1) ? '':statusText
+					statusText = statusText ? '[' + statusText + ']':''
+					for (let s in pageThat.sessionList) {
+						if (pageThat.sessionList[s].id == msg.data.session_id) {
+							pageThat.sessionList[s].user_status = statusText
+							pageThat.sessionList[s].avatar_gray = (msg.data.status == 0) ? 'im-img-gray':''
+							break;
+						}
+					}
+				}
+				
+				if (page.route == 'pages/session-info/session-info') {
+					that.pageThat.info.sessionUser.status = {
+						chinese: statusText,
+						value: msg.data.status
+					}
+					that.messageShow.push(messageShow)
+				} else if (page.route == 'pages/message/message') {
+					messageShow(that.pageThat)
+					messageShow = null
+				}
+			}],
+			['del-contact', commonCallback],
+			['open_session', () => {
+				that.pageRefresh.message = true
+				uni.redirectTo({
+					url: '/pages/session-info/session-info?id=' + msg.data.sessionInfo.id
+				})
+			}],
+			['new_friends_option', commonCallback],
+			['get_groups', () => {
+				if (msg.data.data.type == 'collection') {
+					that.pageThat.tabs = msg.data.res.collection
+					that.pageThat.loadCollection(msg.data.res.collection[0].id, 1)
+				}
+			}],
+			['load_group', () => {
+				if (msg.data.data.method == 'load-collection') {
+					that.pageThat.loadGroup = msg.data.nextpage ? msg.data.data : false
+					
+					for (let c in msg.data.res) {
+						msg.data.res[c].checked = that.pageThat.collectionSelected.has(msg.data.res[c].id) ? true:false
+						msg.data.res[c] = that.buildMessage(msg.data.res[c], 'collection')
+					}
+					if (parseInt(msg.data.data.page) <= 1) {
+						that.pageThat.collections = msg.data.res
+					} else {
+						that.pageThat.collections = that.pageThat.collections.concat(msg.data.res)
+					}
+				}
+			}],
+			['del-collection', () => {
+				that.pageThat.collections.splice(msg.data.data.index, 1);
+				that.pageThat.$refs.uToast.show({
+					title: '收藏已删除~',
+					type: 'success'
+				})
+			}],
+			['fast_reply', () => {
+				if (msg.data.data.method == 'edit') {
+					msg.data.info.status = parseInt(msg.data.info.status) == 0 ? false : true
+					that.pageThat.form = msg.data.info
+					return;
+				} else if (msg.data.data.method == 'opt-done') {
+					that.pageThat.$refs.uToast.show({
+						title: msg.data.data.msg,
+						type: 'success'
+					})
+					setTimeout(function() {
+						that.pageThat.submitButtonStatus = false
+						uni.navigateBack({
+							delta: 1
+						})
+					}, 2000)
+					return;
+				}
+				if (msg.data.data.nextpage) {
+					that.pageThat.loadFastReply = msg.data.data;
+					that.pageThat.loadFastReply.method = 'get'
+				} else {
+					that.pageThat.loadFastReply = false
+				}
+
+				if (parseInt(msg.data.data.page) == 1) {
+					that.pageThat.quickReply = msg.data.fast_reply_list
+				} else {
+					that.pageThat.quickReply = that.pageThat.quickReply.concat(msg.data
+						.fast_reply_list)
+				}
+			}],
+			['report', () => {
+				that.pageThat.type = (msg.data.type == 'single') ? 'user' : msg.data.type
+				that.pageThat.describePlaceholder = '请详细描述被举报对象的恶意行为'
+				uni.setNavigationBarTitle({
+					title: '举报' + msg.data.sessionUser.nickname
+				});
+			}],
+			['search_new_contact', () => {
+				that.pageThat.newContactData = msg.data.nextpage ? msg.data.data : false;
+				if (msg.data.res.length) {
+					for (var i = 0; i < msg.data.res.length; i++) {
+						msg.data.res[i].avatar = that.imgUrl(msg.data.res[i].avatar)
+						if (msg.data.data.type == 'group') {
+							msg.data.res[i].type = 'group'
+							msg.data.res[i].gender_icon = that.imgUrl(
+								'/assets/addons/fastim/icon/group.png');
+							msg.data.res[i].status = msg.data.res[i].user_count + '/' + msg.data
+								.res[i].max_user_count
+							if (msg.data.res[i].is_group_user) {
+								msg.data.res[i].button = {
+									action: 'open-session',
+									opt: false,
+									text: '发消息'
+								}
+							} else {
+								msg.data.res[i].button = {
+									action: 'userinfo-opt',
+									opt: 'join',
+									text: '加入'
+								}
+							}
+						} else {
+							msg.data.res[i].type = 'user'
+							msg.data.res[i].gender_icon = that.imgUrl(
+								'/assets/addons/fastim/icon/' + msg.data.res[i].gender.value +
+								'.png');
+							msg.data.res[i].status = msg.data.res[i].status.chinese
+							if (msg.data.res[i].is_friend) {
+								// 发送消息
+								msg.data.res[i].button = {
+									action: 'open-session',
+									opt: false,
+									text: '发消息'
+								}
+							} else {
+								// 加为好友
+								msg.data.res[i].button = {
+									action: 'add-friends',
+									opt: false,
+									text: '加好友'
+								}
+							}
+						}
+					}
+
+					if (parseInt(msg.data.data.page) == 1) {
+						that.pageThat.res = msg.data.res
+					} else {
+						that.pageThat.res = that.pageThat.res.concat(msg.data.res)
+					}
+
+					if (!msg.data.nextpage) {
+						that.pageThat.loadStatus = 'nomore'
+					}
+				} else {
+					if (parseInt(msg.data.data.page) == 1) {
+						that.pageThat.res = []
+					}
+					that.pageThat.loadStatus = 'nomore'
+				}
+			}],
+			['service_session', () => {
+				that.pageFun(function() {
+					that.send({
+						c: 'User',
+						a: 'loadSession',
+						data: {
+							'session_id': msg.data.service_info.id,
+							'page': 1,
+							'refresh': msg.data.data.refresh ? true:false
+						}
+					});
+				}, that.pageThat)
+			}],
+			['send_message', () => {
+				
+				if (msg.data.sendToYourself) {
+					/**
+					 * sendToYourself:是否是发送给自己的
+					 * 发送给自己的任然需推送给自己的其他在线设备
+					 * 此处做标记以在前端删除可能存在的原消息:`message_id`
+					 */
+					if (that.pageThat && that.pageThat.messageList) {
+						var find = true;
+						for (var i = (that.pageThat.messageList.length - 1); i >= 0; i--) {
+							for (var y = (that.pageThat.messageList[i].data.length - 1); y >= 0; y--) {
+								that.pageThat.messageList[i].data = that.pageThat.messageList[i].data.filter(item => {
+									let findTemp = (item.id != msg.data.message_id);
+									if (!findTemp) {
+										find = false;
+									}
+									return findTemp;
+								})
+								if (find === false) {
+									break;
+								}
+							}
+						}
+					}
+					return ;
+				}
+				
+				// 倒序循环-最快速度找到刚发送的消息
+				if (that.pageThat && that.pageThat.messageList) {
+					for (var i = (that.pageThat.messageList.length - 1); i >= 0; i--) {
+						for (var y = (that.pageThat.messageList[i].data.length - 1); y >= 0; y--) {
+							if (that.pageThat.messageList[i].data[y].id == msg.data.message_id) {
+								that.pageThat.messageList[i].data[y].id = msg.data.new_message_id
+								that.pageThat.messageList[i].data[y].status = that.messageStatus(msg.data.status, msg.data.message_id)
+							}
+						}
+					}
+				}
+			}],
+			['reload_session_list', () => {
+				let pages = getCurrentPages(), page = pages[pages.length - 1]
+				let messageShow = (pageThat = that.pageThat) => {
+					pageThat.pageDataLoad()
+				}
+				if (page.route == 'pages/message/message') {
+					messageShow()
+					messageShow = null
+				} else {
+					that.messageShow.push(messageShow)
+				}
+			}],
+			['group_chat_notice', () => {
+				if (msg.data.data.method == 'list') {
+					that.pageThat.isLeader = msg.data.isLeader
+					that.pageThat.loadGroupChatNotice = msg.data.nextpage ? msg.data.data : false;
+					if (msg.data.list.length) {
+						if (parseInt(msg.data.data.page) == 1) {
+							that.pageThat.noticeList = msg.data.list
+						} else {
+							that.pageThat.noticeList = that.pageThat.noticeList.concat(msg.data.list)
+						}
+						if (!msg.data.nextpage) {
+							that.pageThat.loadStatus = '没有更多了...'
+						}
+					} else {
+						if (parseInt(msg.data.data.page) == 1) {
+							that.pageThat.noticeList = []
+						}
+						that.pageThat.loadStatus = '没有更多了...'
+					}
+				}
+			}],
+			['group-notice-users', () => {
+				 that.pageThat.loadGroupNoticeUsers = msg.data.nextpage ? msg.data.data : false;
+				 if (msg.data.users.length) {
+					for (var i = 0; i < msg.data.users.length; i++) {
+						msg.data.users[i].avatar = that.imgUrl(msg.data.users[i].avatar)
+					}
+				 	if (parseInt(msg.data.data.page) == 1) {
+				 		that.pageThat.users = msg.data.users
+				 	} else {
+				 		that.pageThat.users = that.pageThat.users.concat(msg.data.users)
+				 	}
+				 	if (!msg.data.nextpage) {
+				 		that.pageThat.loadStatus = '没有更多了...'
+				 	}
+				} else {
+				 	if (parseInt(msg.data.data.page) == 1) {
+				 		that.pageThat.users = []
+				 	}
+				 	that.pageThat.loadStatus = '没有更多了...'
+				}
+			}],
+			['group_chat_notice_info', () => {
+				if (msg.data.data.type == 'edit') {
+					that.pageThat.form.content = msg.data.info.content
+					var images = []
+					for (var i = 0; i < msg.data.info.images.length; i++) {
+						images.push({
+							url: msg.data.info.images[i]
+						})
+					}
+					that.pageThat.filesArr = images
+					that.pageThat.checkboxs[0].checked = (parseInt(msg.data.info.popup) == 1) ? true:false
+					that.pageThat.checkboxs[1].checked = (parseInt(msg.data.info.receipt) == 1) ? true:false
+					that.pageThat.checkboxs[2].checked = (parseInt(msg.data.info.top) == 1) ? true:false
+				} else if (msg.data.data.type == 'get') {
+					that.pageThat.showNotice = true
+					that.pageThat.noticeInfo = msg.data.info
+				}
+			}],
+			['receipt_group_chat_notice', () => {
+				that.pageThat.$refs.uToast.show({
+					title: msg.data.msg,
+					type: 'success'
+				})
+				that.pageThat.pageDataLoad()
+			}],
+			['group_chat_at', () => {
+				if (msg.data.group_member.length) {
+					for (let u in msg.data.group_member) {
+						msg.data.group_member[u].avatar = that.imgUrl(msg.data.group_member[u].avatar)
+					}
+					that.pageThat.atUsers = msg.data.group_member
+				} else {
+					that.pageThat.atUsers = []
+				}
+			}],
+			['new_message', () => {
+				let pages = getCurrentPages(), page = pages[pages.length - 1]
+				var sessionType = msg.data.sessionInfo.type, pushNickname = '', pushAvatar = ''
+				if (sessionType == 'group') {
+					// 群聊-不理会发送给特定用户的系统消息
+					if (msg.data.messageData.type == 'system' && (msg.data.messageData.message.display_user != that.initializeData.userinfo.id && msg.data.messageData.message.display_user != 'all')) {
+						return;
+					}
+					
+					let messageShow = (pageThat = that.pageThat) => {
+						that.imSession({
+							lastMessage: msg.data.lastMessage,
+							sessionInfo: {
+								id: msg.data.sessionInfo.id,
+								top: msg.data.sessionInfo.top,
+								type: msg.data.sessionInfo.type,
+								chat_id: msg.data.sessionInfo.chat_id,
+								pushUser: msg.data.sessionInfo.sessionUser
+							},
+							unreadMessagesNumber: msg.data.unreadMessagesNumber,
+							shield: msg.data.shield
+						}, pageThat);// 会话列表会话
+					}
+					if (page.route == 'pages/message/message') {
+						messageShow()
+						messageShow = null
+					} else if (page.route == 'pages/session-info/session-info') {
+						that.messageShow.push(messageShow)
+						if (msg.data.messageData.type == 'group_chat_notice' && parseInt(msg.data.messageData.message.popup) == 1) {
+							// 弹窗公告
+							that.pageThat.showNotice = true
+							that.pageThat.noticeInfo = msg.data.messageData.message
+						}
+					} else {
+						that.messageShow.push(messageShow)
+					}
+					
+					pushAvatar = that.imgUrl(msg.data.sessionInfo.sessionUser.avatar)
+					pushNickname = msg.data.sessionInfo.sessionUser.nickname;
+				} else {
+					pushAvatar = that.imgUrl(msg.data.sessionInfo.pushUser.avatar)
+					pushNickname = msg.data.sessionInfo.pushUser.nickname;
+					if (page.route == 'pages/message/message') {
+						that.imSession(msg.data, that.pageThat);
+					} else {
+						that.messageShow.push((pageThat = that.pageThat) => {
+							that.imSession(msg.data, pageThat);
+						})
+					}
+				}
+				
+				var readMessage = function() {
+					let message = {
+						c: 'Message',
+						a: 'readMessage',
+						data: {
+							'session_id': msg.data.sessionInfo.id
+						}
+					}
+					that.send(message);
+					
+					that.messageShow.push((pageThat = that.pageThat) => {
+						that.imSession({
+							sessionInfo: {
+								id: msg.data.sessionInfo.id,
+								top: msg.data.sessionInfo.top
+							},
+							lastMessage: false,
+							unreadMessagesNumber: 0
+						}, pageThat)
+					})
+					
+					!msg.data.shield && that.newMessageNotice(pushNickname, msg.data.lastMessage.last_message, pushAvatar, false);
+				}
+				
+				if ((page.route == 'pages/session-info/session-info') && (msg.data.sessionInfo.id == that.pageThat.info.id) && (sessionType == 'single' || sessionType == 'group' || (sessionType == 'service' && msg.data.sessionInfo.chat_id == 3))) {
+					if (msg.data.messageData.type != 'system') {
+						msg.data.messageData.sender = (msg.data.sessionInfo.pushUser.id == that.initializeData.userinfo.id) ? 'me' : 'you'
+						msg.data.messageData = that.buildMessage(msg.data.messageData)
+						msg.data.messageData.status = that.messageStatus(3, msg.data.messageData.id)
+						if (sessionType == 'group') {
+							msg.data.messageData.pushUser = {
+								id: msg.data.sessionInfo.pushUser.id,
+								avatar: that.imgUrl(msg.data.sessionInfo.pushUser.avatar),
+								nickname: (msg.data.messageData.sender == 'me') ? '':msg.data.sessionInfo.pushUser.nickname
+							}
+						}
+					}
+					
+					let messageListIndex = that.pageThat.messageList.length - 1
+					if (that.pageThat.messageList[messageListIndex]) {
+						that.pageThat.messageList[messageListIndex].data.push(msg.data.messageData);
+					} else {
+						that.pageThat.messageList = that.pageThat.messageList.concat({
+							datetime: '刚刚',
+							data: [msg.data.messageData]
+						});
+					}
+					that.pageThat.scrollIntoFooter(800, 99994)
+					readMessage()
+				} else if ((page.route == 'pages/session-info/notice-session-info') && (msg.data.sessionInfo.id == parseInt(that.pageThat.session_id)) && (sessionType == 'service') && (msg.data.sessionInfo.chat_id == 2 || msg.data.sessionInfo.chat_id == 1)) {
+					that.pageThat.pageDataLoad()
+					readMessage()
+				} else {
+					!msg.data.shield && that.newMessageNotice(pushNickname, msg.data.lastMessage.last_message, pushAvatar, true);
+				}
+			}],
+			['load_session', () => {
+				that.messageShow.push((pageThat = that.pageThat) => {
+					that.imSession({
+						sessionInfo: {
+							id: msg.data.info.id,
+							top: msg.data.info.top
+						},
+						lastMessage: false,
+						unreadMessagesNumber: 0
+					}, pageThat, false)
+				})
+				
+				if (parseInt(that.initializeData.config.user_config.send_message_key) == 0) {
+					// 回车键发送
+					that.pageThat.sendButtonType = 'send';
+				}
+				
+				if (msg.data.info.windowType == 'fake_tourist') {
+					// uni-app端无需游客概念
+					that.pageThat.$refs.uToast.show({
+						title: '抱歉,加载异常,请重新登录~',
+						type: 'error'
+					})
+					setTimeout(res => {
+						that.logout()
+					}, 2000)
+					return ;
+				} else if (msg.data.info.windowType == 'message' || msg.data.info.windowType == 'csr_message') {
+					msg.data.info.user.avatar = that.imgUrl(msg.data.info.user.avatar)
+					msg.data.info.sessionUser.avatar = that.imgUrl(msg.data.info.sessionUser.avatar)
+					that.pageThat.userId = that.initializeData.userinfo.id
+					that.pageThat.info = msg.data.info
+					that.pageThat.loadRecordsData = msg.data.nextpage ? msg.data.data : false
+					
+					if (msg.data.info.type == 'group' && msg.data.unreadFixedMsg && parseInt(msg.data.unreadFixedMsg.popup) == 1) {
+						that.pageThat.showNotice = true
+						that.pageThat.noticeInfo = msg.data.unreadFixedMsg
+					}
+					
+					for (var i = 0; i < msg.data.message.length; i++) {
+						for (var y = 0; y < msg.data.message[i].data.length; y++) {
+							if (msg.data.message[i].data[y].type != 'system') {
+								
+								msg.data.message[i].data[y] = that.buildMessage(msg.data.message[i].data[y])
+								
+								if (msg.data.info.type == 'group') {
+									msg.data.message[i].data[y].pushUser = {
+										id: msg.data.message[i].data[y].userInfo.id,
+										avatar: that.imgUrl(msg.data.message[i].data[y].userInfo.avatar)
+									}
+									if (msg.data.message[i].data[y].sender_id == that.initializeData.userinfo.id) {
+										if (msg.data.message[i].data[y].read_number >= (msg.data.info.sessionUser.user_count - 1)) {
+											msg.data.message[i].data[y].status = that.messageStatus(6, msg.data.message[i].data[y].id);
+										} else if (msg.data.message[i].data[y].read_number > 0) {
+											msg.data.message[i].data[y].status = that.messageStatus(5, msg.data.message[i].data[y].id, msg.data.message[i].data[y].read_number)
+										} else {
+											msg.data.message[i].data[y].status = that.messageStatus(msg.data.message[i].data[y].status, msg.data.message[i].data[y].id)
+										}
+										
+									} else {
+										msg.data.message[i].data[y].pushUser.nickname = msg.data.message[i].data[y].userInfo.nickname
+									}
+									
+								} else if (msg.data.info.type == 'single' || (msg.data.info.type == 'service' && msg.data.info.chat_id == 3)) {
+									if (msg.data.message[i].data[y].sender == 'me') {
+										msg.data.message[i].data[y].status = that.messageStatus(msg.data.message[i].data[y].status, msg.data.message[i].data[y].id)
+									} else {
+										msg.data.message[i].data[y].status = that.messageStatus(3, msg.data.message[i].data[y].id)
+									}
+								}
+							}
+						}
+					}
+					if (parseInt(msg.data.data.page) == 1) {
+						that.pageThat.messageList = msg.data.message
+						that.pageThat.scrollIntoFooter(300, 99990);
+					} else {
+						for (let i = msg.data.message.length - 1; i >= 0; i--) {
+							msg.data.message[i].data.reverse()
+							that.pageThat.messageList.unshift(msg.data.message[i]);
+						}
+						
+						setTimeout(function() {
+							let wrapper = uni.createSelectorQuery().select('#wrapper');
+							wrapper.fields({
+								scrollOffset: true
+							}, data => {
+								that.pageThat.wrapperScrollTop = data.scrollHeight - that.pageThat.wrapperScrollHeight
+								that.pageThat.wrapperWithAnimation = true
+							}).exec()
+						}, 300)
+					}
+					
+					if (msg.data.info.sessionUser.speak && parseInt(msg.data.info.sessionUser.speak) == 1) {
+						// 禁止发言
+						that.pageThat.imMessage = '管理员已禁止发言'
+						that.pageThat.messageContenteditable = true;
+					} else {
+						that.pageThat.messageContenteditable = false;
+					}
+				} else if (msg.data.info.windowType == 'new_friends_message' || msg.data.info
+					.windowType == 'group_notice') {
+					uni.setNavigationBarTitle({
+						title: msg.data.info.sessionUser.nickname
+					})
+					that.pageThat.serviceMessageData = msg.data.nextpage ? msg.data.data : false
+
+					if (msg.data.message.length) {
+						var res = [];
+						for (var i = 0; i < msg.data.message.length; i++) {
+							for (var y = 0; y < msg.data.message[i].data.length; y++) {
+								if (msg.data.info.windowType == 'group_notice') {
+									res.push(that.serviceMessageInfo({
+										messageData: msg.data.message[i].data[y]
+									}, 'group_notice', {
+										userInfo: msg.data.message[i].data[y].userInfo,
+										groupChatInfo: msg.data.message[i].data[y]
+											.groupChatInfo
+									}))
+								} else {
+									res.push(that.serviceMessageInfo({
+										messageData: msg.data.message[i].data[y]
+									}, 'new_friends_message', {
+										userInfo: msg.data.message[i].data[y].userInfo
+									}))
+								}
+							}
+						}
+
+						if (parseInt(msg.data.data.page) <= 1) {
+							that.pageThat.messages = res
+						} else {
+							that.pageThat.messages = that.pageThat.messages.concat(res)
+						}
+						if (!msg.data.nextpage) {
+							that.pageThat.loadStatus = 'nomore'
+						}
+					} else {
+						that.pageThat.loadStatus = 'nomore'
+					}
+				}
+			}],
+			['session-setting', () => {
+				msg.data.session_info.sessionUser.avatar = that.imgUrl(msg.data.session_info.sessionUser.avatar)
+				if (msg.data.session_info.type == 'group') {
+					msg.data.session_info.oldnickname = msg.data.session_info.nickname
+					that.pageFun(function(){
+						that.send({
+							c: 'Message',
+							a: 'groupChatMember',
+							data: {
+								'id': msg.data.session_info.sessionUser.id,
+								'method': 'get',
+								'limit': 11
+							}
+						});
+					}, that.pageThat)
+				}
+				that.pageThat.info = msg.data.session_info
+			}],
+			['group_chat_member', () => {
+				if (msg.data.data.lettersort && !msg.data.data.keywords) {
+					// 按字母排序
+					var initialPinyinArr = msg.data.group_member.initialPinyinArr
+					that.pageThat.indexList = msg.data.group_member.initialPinyinIndex
+					for (let i in initialPinyinArr) {
+						for (let u in initialPinyinArr[i]) {
+							initialPinyinArr[i][u].avatar = that.imgUrl(initialPinyinArr[i][u].avatar)
+						}
+					}
+					that.pageThat.users = initialPinyinArr
+					msg.data.group_member.leader.avatar = that.imgUrl(msg.data.group_member.leader.avatar)
+					that.pageThat.leader = [msg.data.group_member.leader]
+					
+					that.pageThat.showSearchRes = msg.data.data.keywords ? true:false
+					return ;
+				}
+				
+				for (let gm in msg.data.group_member) {
+					msg.data.group_member[gm].avatar = that.imgUrl(msg.data.group_member[gm].avatar)
+				}
+				that.pageThat.groupChatMember = msg.data.group_member
+				that.pageThat.showSearchRes = msg.data.data.keywords ? true:false
+			}],
+			['notice_info', () => {
+				that.pageThat.info = msg.data.info
+			}],
+			['create_group', () => {
+				that.pageRefresh.message = true
+				that.pageThat.$refs.uToast.show({
+					title: '群聊已创建~',
+					type: 'success'
+				})
+				that.showMsgCallback = null
+				setTimeout(function(){
+					that.pageThat.submitButtonStatus = false
+					uni.switchTab({
+						url: '/pages/message/message'
+					})
+				}, 2000)
+			}],
+			['user-shield', () => {
+				that.pageThat.loadData = msg.data.nextpage ? msg.data.data : false
+				for (let u in msg.data.shield_users) {
+					msg.data.shield_users[u].avatar = that.imgUrl(msg.data.shield_users[u].avatar)
+				}
+				if (parseInt(msg.data.data.page) == 1) {
+					that.pageThat.users = msg.data.shield_users
+				} else {
+					that.pageThat.users = that.pageThat.users.concat(msg.data.shield_users)
+				}
+			}],
+			['shield_session', () => {
+				that.pageThat.users = []
+				that.pageThat.pageDataLoad()
+			}],
+			['input_status', () => {
+				let pages = getCurrentPages(), page = pages[pages.length - 1]
+				if (page.route == 'pages/session-info/session-info' && that.pageThat.info.id == msg.data.session_id) {
+					if (msg.data.status == 'start') {
+						that.pageThat.sessionUserInputStatus = true
+						// 获得输入框高度
+						let imWrite = uni.createSelectorQuery().select('.im-write');
+						imWrite.fields({
+							size: true
+						}, data => {
+							that.pageThat.imWriteHeight = data.height + 10
+						}).exec()
+					} else {
+						that.pageThat.sessionUserInputStatus = false
+					}
+				}
+			}],
+			['del-group-member', () => {
+				if (msg.data.member_id) {
+					that.pageThat.$refs.uToast.show({
+						title: '已删除群成员~',
+						type: 'success'
+					})
+					that.pageFun(that.pageThat.pageDataLoad, that.pageThat);
+				}
+			}],
+			['session-operation-tips', () => {
+				that.pageThat.$refs.uToast.show({
+					title: msg.data.msg,
+					type: msg.data.type
+				})
+				
+				let pages = getCurrentPages(), page = pages[pages.length - 1]
+				var messageShow = function (pageThat = that.pageThat) {
+					pageThat.ws.pageFun(pageThat.pageDataLoad, pageThat);
+				}
+				if (page.route == 'pages/message/message') {
+					messageShow()
+					messageShow = null
+				} else {
+					that.messageShow.push(messageShow)
+				}
+			}],
+			['upload_multipart', () => {
+				if (msg.data.multipart) {
+					that.initializeData.config.upload.multipart = msg.data.multipart
+					let mu = new Object();
+					for (let i in msg.data.multipart) {
+						mu[i] = msg.data.multipart[i]
+					}
+					that.pageThat.uploadFormData = mu
+				}
+			}],
+			['default', () => {
+				console.log('收到新的ws消息')
+			}]
+		]);
+		
+		let onMessageCallBackAction = this.onMessageCallBack.get(msg.event)
+		if (onMessageCallBackAction) {
+			onMessageCallBackAction.call(that, msg);
+			this.onMessageCallBack.delete(msg.event)
+		}
+
+		let action = msgFun.get(msg.event) || msgFun.get('default')
+		return action.call(that);
+	},
+	buildMessage: function(data, buildType = 'record') {
+		var that = this
+		if (data.type == 'file') {
+			data.message.size = data.message.size ? data.message.size : '未知大小';
+			data.suffixImg = that.buildUrl('suffix', that.initializeData.tokens.im_tourists_token, data.message.suffix)
+		} else if (data.type == 'audio') {
+			data.play = false
+			data.duration = false
+			data.currentTime = '00:00'
+		} else if (data.type == 'voice') {
+			data.play = false
+		} else if(data.type == 'video') {
+			data.play = false
+			data.duration = false
+			data.currentTime = '00:00'
+		} else if (data.type == 'link') {
+			if (buildType == 'collection') {
+				data.message.link_name = (data.message.link_name ? data.message.link_name + ' ': '') + data.message.link_url
+			}
+			data.message.link_name = data.message.link_name ? data.message.link_name : data.message.link_url
+		}
+		return data;
+	},
+	playVideo: function(item, pageThat, page = 'session') {
+		var videoContext = uni.createVideoContext('video-' + item.id, pageThat)
+		if (!item.play) {
+			this.pauseAudio('hide', pageThat, page)
+			item.play = true
+			videoContext.play()
+			setTimeout(() => {
+				videoContext.requestFullScreen({direction:0}) // 自动全屏
+			}, 300)
+		} else {
+			item.play = false
+			videoContext.pause()
+		}
+		this.playVideoItem = item
+	},
+	playAudioInit: function (data) {
+		var that = this
+		
+		that.playAudioContext = uni.createInnerAudioContext();
+		that.playAudioContextId = data.id
+		that.playAudioContext.autoplay = true;
+		that.playAudioContext.src = data.message.url ? data.message.url:data.message
+		
+		that.playAudioContext.onPlay(() => {
+			if (that.playAudioContext.currentTime) {
+				data.currentTime = that.sToHs(Math.floor(that.playAudioContext.currentTime))
+			}
+		})
+		
+		that.playAudioContext.onTimeUpdate(function() {
+			data.currentTime = that.sToHs(Math.floor(that.playAudioContext.currentTime))
+			if (!data.duration && that.playAudioContext.duration > 0 && that.playAudioContext.duration != Infinity) {
+				data.duration = that.sToHs(Math.floor(that.playAudioContext.duration))
+			}
+		});
+		
+		that.playAudioContext.onEnded(() => {
+			data.play = false
+			data.currentTime = '00:00'
+		});
+		
+		that.playAudioContext.onError((e) => {
+			console.log(e)
+			// data.play = false
+			/* uni.showToast({
+				title: '播放失败,请重试~',
+				icon: 'none'
+			}) */
+		})
+	},
+	// 播放音频,全局只播放一个音频文件
+	// 独立于新消息通知的音频播放
+	playAudio: function (data, index, m, pageThat, page = 'session') {
+		var that = this
+		
+		// IOS低端机不能直接通过data更新数据
+		// 在此处保存要更新数据的 index
+		var obj = {
+			index: index,
+			id: data.id,
+			m: m
+		}
+
+		// 记录播放
+		if (that.playAudioObj) {
+			var exist = false
+			for (let a in that.playAudioObj) {
+				if (that.playAudioObj[a].id == data.id) {
+					exist = a
+					break;
+				}
+			}
+			if (exist === false) {
+				that.playAudioObj.push(obj)
+			}
+		} else {
+			that.playAudioObj = [obj]
+		}
+		
+		for (let a in that.playAudioObj) {
+			let item = that.playAudioObj[a]
+			// 有正在播放的音频
+			if (page == 'session') {
+				if (pageThat.messageList[item.index].data[item.m].play) {
+					pageThat.messageList[item.index].data[item.m].play = false;
+					if (pageThat.messageList[item.index].data[item.m].id == data.id) {
+						// 暂停
+						that.playAudioContext.pause()
+						return false;
+					} else {
+						// 停止
+						pageThat.messageList[item.index].data[item.m].currentTime = '00:00'
+						that.playAudioContext.destroy();
+					}
+					break;
+				}
+			} else if (page == 'collection') {
+				if (pageThat.collections[item.index].play) {
+					pageThat.collections[item.index].play = false;
+					if (pageThat.collections[item.index].id == data.id) {
+						// 暂停
+						that.playAudioContext.pause()
+						return false;
+					} else {
+						// 停止
+						pageThat.collections[item.index].currentTime = '00:00'
+						that.playAudioContext.destroy();
+					}
+					break;
+				}
+			}
+		}
+		
+		data.play = true
+		
+		if (that.playAudioContext && that.playAudioContextId && that.playAudioContextId == data.id) {
+			// 播放上次播放的音频
+			that.playAudioContext.play()
+		} else {
+			that.playAudioInit(data)
+		}
+	},
+	pauseAudio: function (type, pageThat, page = 'session') {
+		var that = this
+		// 正在播放的音频
+		if (that.playAudioObj && that.playAudioContext) {
+			
+			if (type == 'unload') {
+				that.playAudioContext.destroy();
+				that.playAudioContext = null
+			}
+			
+			for (let a in that.playAudioObj) {
+				let item = that.playAudioObj[a]
+				// 有正在播放的音频
+				if (page == 'session') {
+					// IOS低端机不能直接通过data更新数据
+					// 此处保存有需更新数据的 index
+					if (pageThat.messageList[item.index].data[item.m].play) {
+						pageThat.messageList[item.index].data[item.m].play = false;
+						if (type == 'unload') {
+							break;
+						} else {
+							// 暂停
+							that.playAudioContext.pause()
+							break;
+						}
+					}
+				} else if (page == 'collection') {
+					if (pageThat.collections[item.index].play) {
+						pageThat.collections[item.index].play = false;
+						if (type == 'unload') {
+							break;
+						} else {
+							// 暂停
+							that.playAudioContext.pause()
+							break;
+						}
+					}
+				}
+			}
+		}
+		that.playAudioObj = null
+	},
+	sToHs: function (s) {
+		let h;
+		h = Math.floor(s / 60);
+		s = s % 60;
+		h += '';
+		s += '';
+		h = (h.length === 1) ? '0' + h : h;
+		s = (s.length === 1) ? '0' + s : s;
+		return h + ':' + s;
+	},
+	serviceMessageInfo: function(info, type, extendData = false) {
+		var that = this,
+			messageItem = {},
+			userObj = {}
+		var resultObj = {
+			agree: '已同意',
+			refuse: '已拒绝',
+			autoagree: '已自动同意'
+		};
+
+		messageItem.id = info.messageData.id
+		messageItem.note = info.messageData.message.note
+		messageItem.source = (info.messageData.message.source ? '来自' + info.messageData.message.source : '')
+
+		if (extendData.userInfo) {
+			userObj = {
+				type: 'user',
+				id: extendData.userInfo.id,
+				nickname: extendData.userInfo.nickname
+			};
+		}
+
+		if (type == 'new_friends_message') {
+			messageItem.avatar = that.imgUrl(extendData.userInfo.avatar)
+			messageItem.avatar_type = 'user'
+			messageItem.avatar_user = extendData.userInfo.id
+			messageItem.friendship = info.messageData.friendship
+			
+			if (info.messageData.message.method == 'notice') {
+				if (info.messageData.message.result == 'refuse') {
+					if (info.messageData.sender_id == that.initializeData.userinfo.id) {
+						messageItem.title = [{
+								type: 'text',
+								content: '你已经拒绝了'
+							},
+							userObj,
+							{
+								type: 'text',
+								content: '的好友申请。'
+							}
+						];
+					} else {
+						messageItem.title = [
+							userObj,
+							{
+								type: 'text',
+								content: '拒绝了你的好友申请。'
+							}
+						];
+					}
+				} else {
+					messageItem.title = [
+						{
+							type: 'text',
+							content: info.messageData.message.msg
+						}
+					];
+				}
+				messageItem.result = ' '
+			} else if (info.messageData.message.method == 'apply') {
+				// 对 result 进行处理
+				if (info.messageData.message.result) {
+					if (resultObj[info.messageData.message.result]) {
+						messageItem.result = resultObj[info.messageData.message.result]
+					} else if (info.messageData.sender_id == that.initializeData.userinfo.id) {
+						messageItem.result = '等待验证'
+					} else {
+						messageItem.result = false
+					}
+				} else {
+					messageItem.result = false
+				}
+				
+				if (info.messageData.sender_id == that.initializeData.userinfo.id) {
+					messageItem.title = [{
+							type: 'text',
+							content: '申请添加'
+						},
+						userObj,
+						{
+							type: 'text',
+							content: '为好友'
+						}
+					];
+				} else {
+					messageItem.title = [
+						userObj,
+						{
+							type: 'text',
+							content: '申请添加你为好友'
+						}
+					]
+				}
+			}
+		} else if (type == 'group_notice') {
+			messageItem.type = info.messageData.type
+			messageItem.avatar = that.imgUrl(extendData.groupChatInfo.avatar)
+			messageItem.avatar_type = 'group'
+			messageItem.avatar_user = extendData.groupChatInfo.id
+
+			var groupObj = {
+				type: 'group',
+				id: extendData.groupChatInfo.id,
+				nickname: extendData.groupChatInfo.nickname
+			};
+
+			// 对 result 进行处理
+			if (info.messageData.message.result) {
+				if (resultObj[info.messageData.message.result]) {
+					messageItem.result = resultObj[info.messageData.message.result]
+				} else if (info.messageData.sender_id == that.initializeData.userinfo.id) {
+					messageItem.result = '等待管理员验证'
+				} else {
+					messageItem.result = false
+				}
+			} else {
+				messageItem.result = false
+			}
+
+			// 申请入群
+			if (info.messageData.type == 'group_apply') {
+				if (info.messageData.sender_id == that.initializeData.userinfo.id) {
+					messageItem.title = [{
+							type: 'text',
+							content: '申请加入群聊'
+						},
+						groupObj
+					]
+				} else {
+					messageItem.title = [
+						userObj,
+						{
+							type: 'text',
+							content: '申请加入群聊'
+						},
+						groupObj
+					]
+				}
+			} else if (info.messageData.type == 'group_invitation') {
+				// 入群邀请
+				var invitationUserObj = {
+					type: 'user',
+					id: info.messageData.invitationUser.id,
+					nickname: info.messageData.invitationUser.nickname
+				}
+
+				var leader_result = parseInt(info.messageData.message.leader_result),
+					user_result = parseInt(info.messageData.message.user_result);
+				
+				messageItem.result = false
+				if (info.messageData.message.identity == 'admin') {
+					// xx邀请xx加入群聊xx
+					let invitedUserObj = {
+						type: 'user',
+						id: info.messageData.invitedUser.id,
+						nickname: info.messageData.invitedUser.nickname
+					}
+					
+					messageItem.title = [
+						invitationUserObj,
+						{
+							type:'text',
+							content: '邀请'
+						},
+						invitedUserObj,
+						{
+							type:'text',
+							content: '加入群聊'
+						},
+						groupObj
+					];
+
+					if (leader_result == 3) {
+						messageItem.result = '失效'
+					} else if (leader_result == 1 && user_result == 1) {
+						messageItem.result = '已同意'
+					} else if (leader_result == 2) {
+						messageItem.result = '已拒绝'
+					} else if (user_result == 2) {
+						messageItem.result = '用户已拒绝'
+					} else if (leader_result == 1 && user_result == 0) {
+						messageItem.result = '等待被邀请人验证'
+					} else if (leader_result == 0) {
+						messageItem.result = false
+					}
+				} else if (info.messageData.message.identity == 'user') {
+					// xx邀请你加入群聊xx
+					if (user_result == 3) {
+						messageItem.result = '失效'
+					} else if (leader_result == 1 && user_result == 1) {
+						messageItem.result = '已同意'
+					} else if (leader_result == 2) {
+						messageItem.result = '群管理员已拒绝'
+					} else if (user_result == 2) {
+						messageItem.result = '已拒绝'
+					} else if (leader_result == 0 && user_result == 1) {
+						messageItem.result = '等待管理员验证'
+					} else if (user_result == 0) {
+						messageItem.result = false
+					}
+					messageItem.title = [
+						invitationUserObj,
+						{
+							type:'text',
+							content: '邀请你加入群聊'
+						},
+						groupObj
+					];
+				}
+			} else if (info.messageData.type == 'group_notice') {
+				messageItem.result = ' '
+				if (info.messageData.message.action && info.messageData.message.action == 'dismiss') {
+					messageItem.title = [
+						groupObj
+					]
+					messageItem.note = info.messageData.message.notice
+				} else if (info.messageData.message.action && info.messageData.message.action == 'quit') {
+					messageItem.title = [
+						userObj,
+						{
+							type: 'text',
+							content: '退出了群聊'
+						},
+						groupObj
+					]
+				} else if (info.messageData.message.action && info.messageData.message.action == 'propose') {
+					messageItem.title = [
+						{
+							type: 'text',
+							content: '管理员已将你请出群聊'
+						},
+						groupObj
+					]
+				} else if (info.messageData.message.action && info.messageData.message.action == 'refuse') {
+					var leader = parseInt(extendData.groupChatInfo.leader); // 群管理
+					var sender = parseInt(info.messageData.sender_id); // 执行拒绝操作的用户
+					if (that.initializeData.userinfo.id == parseInt(info.messageData.message.refuse_user)) {
+						if (sender == leader) {
+							messageItem.title = [
+								{
+									type: 'text',
+									content: '管理员拒绝你加入群聊'
+								},
+								groupObj
+							]
+						} else if (sender == that.initializeData.userinfo.id) {
+							messageItem.title = [
+								{
+									type: 'text',
+									content: '你拒绝加入群聊'
+								},
+								groupObj
+							]
+						}
+					} else if (that.initializeData.userinfo.id == leader) {
+						if (sender == leader) {
+							messageItem.title = [
+								{
+									type: 'text',
+									content: '你拒绝了'
+								},
+								userObj,
+								{
+									type: 'text',
+									content: '加入群聊'
+								},
+								groupObj
+							]
+						} else {
+							messageItem.title = [
+								userObj,
+								{
+									type: 'text',
+									content: '拒绝加入群聊'
+								},
+								groupObj
+							]
+						}
+					} else {
+						if (sender == leader) {
+							messageItem.title = [
+								{
+									type: 'text',
+									content: '管理员拒绝了'
+								},
+								userObj,
+								{
+									type: 'text',
+									content: '加入群聊'
+								},
+								groupObj
+							]
+						} else {
+							messageItem.title = [
+								userObj,
+								{
+									type: 'text',
+									content: '拒绝加入群聊'
+								},
+								groupObj
+							]
+						}
+					}
+				}
+			}
+		}
+
+		messageItem.option = (messageItem.result && messageItem.result != 'pending') ? false : true;
+		return messageItem;
+	},
+	getEmoji: function() {
+		var that = this
+		return new Promise(function(resolve, reject) {
+			if (that.initializeEmoji) {
+				return resolve(that.initializeEmoji)
+			}
+			
+			var token = ''
+			if (that.initializeData.tokens) {
+				token = that.initializeData.tokens.im_tourists_token
+			} else {
+				let info = uni.getStorageSync('userinfo');
+				token = info.token
+			}
+			
+			that.that.$u.get(that.buildUrl('emoji', token), {}).then(res => {
+				if (res.code == 1) {
+					that.initializeEmoji = res.data.emoji
+					return resolve(res.data.emoji)
+				} else {
+					return reject()
+				}
+			})
+		})
+	},
+	clearPageRefresh: function() {
+		for (let p in this.pageRefresh) {
+			this.pageRefresh[p] = false
+		}
+	},
+	messageStatus: function (status, messageId, readNumber = 0) {
+		var statusClass = '';
+		switch (status) {
+			case 0:
+				status = '未读', statusClass = 'fastim-color-blue';
+				break;
+			case 1:
+				status = '已读', statusClass = '';
+				break;
+			case 4:
+				status = '失败', statusClass = 'fastim-color-red send-message-fail fail-message-' + messageId;
+				break;
+			case 5:
+				status = readNumber + '人已读', statusClass = '';
+				break;
+			case 6:
+				status = '全部已读', statusClass = '';
+				break;
+			default:
+				status = '', statusClass = 'fastim-hidden';
+				break;
+		}
+		
+		return {
+			status: status,
+			statusClass: statusClass
+		}
+	},
+	imgUrl: function(url) {
+		var ret = /^http(s)?:\/\//;
+		var retDataImage = /^data:image/;
+		if (ret.test(url) || retDataImage.test(url)) {
+			return url;
+		} else {
+			if (this.initializeData.config && this.initializeData.config.__CDN__) {
+				return this.initializeData.config.__CDN__ + url;
+			} else {
+				return this.buildUrl('default') + url;
+			}
+		}
+	},
+	buildUrl: function(type, token = false, data = null) {
+		var that = this
+		var protocol = imConfig.httpsSwitch ? 'https://' : 'http://';
+		var port = imConfig.httPort ? ':' + imConfig.httPort : '';
+
+		var buildFun = new Map([
+			['initialize', () => {
+				return protocol + imConfig.baseUrl + port +
+					'/addons/fastim/index/initialize?referrer=uni-app&im_tourists_token=' + token;
+			}],
+			['ws', () => {
+				let protocol = parseInt(that.initializeData.config.wss_switch) == 1 ? 'wss://' :
+					'ws://';
+				return protocol + imConfig.baseUrl + ':' + that.initializeData.config
+				.websocket_port;
+			}],
+			['upload', () => {
+				if (that.initializeData.config && that.initializeData.config.upload.uploadurl) {
+					return that.initializeData.config.upload.uploadurl + '?im_tourists_token=' +
+						token;
+				}
+				return protocol + imConfig.baseUrl + port +
+					'/addons/fastim/index/upload?im_tourists_token=' + token;
+			}],
+			['get-config-item-data', () => {
+				return protocol + imConfig.baseUrl + port +
+					'/addons/fastim/index/getConfigItemData?im_tourists_token=' + token;
+			}],
+			['suffix', () => {
+				return protocol + imConfig.baseUrl + port +
+					'/addons/fastim/index/icon/suffix/' + data + '?im_tourists_token=' + token;
+			}],
+			['emoji', () => {
+				return protocol + imConfig.baseUrl + port +
+					'/addons/fastim/index/getEmoji?im_tourists_token=' + token;
+			}],
+			['message_prompt', () => {
+				if (that.initializeData.config && that.initializeData.config.upload.cdnurl) {
+					return that.initializeData.config.upload.cdnurl + that.initializeData.config.ringing;
+				}
+				return protocol + imConfig.baseUrl + port + '/addons/fastim/index/loadMessagePrompt?im_tourists_token=' + token;
+			}],
+			['default', () => {
+				return protocol + imConfig.baseUrl + port
+			}]
+		]);
+
+		let action = buildFun.get(type) || buildFun.get('default')
+		return action.call(this);
+	},
+	logout: function () {
+		var that = this
+		try {
+			that.closeCallback = res => {
+				uni.hideToast()
+				that.initializeData = false
+				uni.reLaunch({
+					url: '/pages/center/login'
+				})
+			}
+			that.needReconnect = false
+			if (that.socketTask && that.socketOpen) {
+				that.socketTask.close()
+			} else {
+				that.closeCallback()
+				that.closeCallback = null
+			}
+		    uni.clearStorageSync()
+			that.pageRefresh.message = true
+		} catch (e) {
+			console.log(e)
+			uni.showToast({
+				title: '注销失败,请重试!',
+				icon: 'none'
+			})
+		}
+	},
+	pageFun: function(fun, pageThat = null) {
+		this.pageThat = pageThat ? pageThat:this.pageThat
+		if (this.ready) {
+			typeof fun == 'function' && fun()
+		} else {
+			this.connectSuccess = fun
+		}
+	},
+	checkNetwork: function (pageThat = null) {
+		this.pageThat = pageThat ? pageThat:this.pageThat
+		if (!this.socketOpen || !this.ready) {
+			this.pageThat.commonTips = '网络不给力,正在自动重连~'
+		} else if (this.pageThat.commonTips) {
+			this.pageThat.commonTips = ''
+		}
+	}
+};
+
+export default {
+	ws
+}

+ 37 - 0
addons/fastim/uniapp/components/common/common.vue

@@ -0,0 +1,37 @@
+<template>
+	<view>
+		<view v-if="tips" :style="{position: navbarHeight ? 'fixed':'unset', top: navbarHeight + 'px'}" class="tips">{{tips}}</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name:"common",
+		props: {
+			tips: {
+				type: String,
+				default: ''
+			},
+			navbarHeight: {
+				type: [String, Number],
+				default: 0
+			}
+		}
+	}
+</script>
+
+<style>
+.tips {
+	position: fixed;
+	width: 100%;
+	min-height: 80rpx;
+	background: rgba(247, 76, 49, 0.9);
+	color: #FFFFFF;
+	font-size: 28rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	padding: 10rpx 30rpx;
+	z-index: 99999;
+}
+</style>

+ 44 - 0
addons/fastim/uniapp/components/link-message/link-message.vue

@@ -0,0 +1,44 @@
+<template>
+	<view>
+		<!-- #ifdef APP-PLUS || H5 -->
+		<view class="link-message" @click="showLinkPopup(value.message.link_url)">{{value.message.link_name ? value.message.link_name:value.message.link_url}}</view>
+		<!-- #endif -->
+		<!-- #ifdef MP -->
+		<u-link :color="value.sender == 'me' ? '#f74c31':'#6388fb'" :href="value.message.link_url">{{value.message.link_name ? value.message.link_name:value.message.link_url}}</u-link>
+		<!-- #endif -->
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "link-message",
+		props: {
+			value: {
+				type: Object,
+				required: true
+			},
+			LinkPopupShow: {
+				default: false
+			},
+			linkPopupUrl: {
+				default: ''
+			}
+		},
+		methods: {
+			showLinkPopup: function (url) {
+				this.$emit('update:LinkPopupShow', true)
+				this.$emit('update:linkPopupUrl', url)
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+.wrapper .message-item.me .link-message {
+	color: $--red;
+}
+.link-message {
+	font-size: 30rpx;
+	color: $--blue;
+}
+</style>

+ 238 - 0
addons/fastim/uniapp/components/message/message.vue

@@ -0,0 +1,238 @@
+<template>
+	<view>
+		<block v-if="value.type == 'default'">
+			<u-parse :show-with-animation="true" :tag-style="{img:'width:30px;height:30px;'}" :selectable="true" :html="value.message"></u-parse>
+		</block>
+		<block v-else-if="value.type == 'image'">
+			<image @click="preimage(value.message)" class="image-message" :src="value.message" mode="widthFix"></image>
+		</block>
+		<block v-else-if="value.type == 'file'">
+			<view class="file-message" @click="url(value.message.url, '下载地址')">
+				<image class="file-suffix" :src="value.suffixImg"></image>
+				<view class="file-message-box">
+					<view class="file-name">{{value.message.suffix}}文件</view>
+					<view class="file-size">{{value.message.size}}</view>
+				</view>
+				<view class="down-file">
+					<u-button type="primary" @click="url(value.message.url, '下载地址')" size="mini">下载</u-button>
+				</view>
+			</view>
+		</block>
+		<block v-else-if="value.type == 'kbs_list'">
+			
+			<view class="fastim-kbs">
+				<view class="kbs-title">{{value.message.title}}</view>
+					<view class="csr-message-box">
+					
+						<block v-if="value.message.type == 'distribution-group-select'">
+							<view v-if="value.message.csr_group.length" v-for="(item, index) in value.message.csr_group" :key="index">
+								<view @click="distributionGroupSelect(item.id)" class="fastim-color-blue">
+									<text>{{item.name}}({{item.csr_count}})</text>
+								</view>
+							</view>
+							<view v-if="!value.message.csr_group.length" class="csr-group-none">没有可以选择的客服代表分组,请联系管理员添加!</view>
+						</block>
+						<block v-else-if="value.message.type == 'csrs'">
+							<view v-for="(item, index) in value.message.csrs" :key="index">
+								<view class="csr-group-name">{{item.name ? item.name:'无分组'}}</view>
+								<view @click="openSession(user.id)" v-for="(user,idx) in item.csrs" :key="idx">
+									<text class="csr-message-i-el">{{idx + 1}}、</text><text class="fastim-color-blue">{{user.nickname}}</text>
+								</view>
+							</view>
+						</block>
+						<block v-else-if="value.message.type == 'included'">
+							<view class="kbs-items">
+								<view v-for="(item, index) in value.message.kbs" :key="index">
+									<view @click="kbsClick(item.id)" class="kbs-item">
+										<text>{{index + 1}}、</text>
+										<text class="kbs-item-title fastim-color-blue">{{item.title}}</text>
+									</view>
+								</view>
+							</view>
+						</block>
+						<block v-else-if="value.message.type == 'not-included'">
+							<view @click="manualCsr" class="fastim-color-blue not-included">联系人工客服</view>
+						</block>
+				
+				</view>
+			</view>
+		</block>
+		<block v-else-if="value.type == 'group_chat_notice'">
+			<view @click="showNotice(value.message.id)">
+				<view class="group-chat-notice-title">群公告</view>
+				<view class="group-chat-notice-content">{{value.message.content}}</view>
+				<view class="group-chat-notice-button">查看详情</view>
+			</view>
+		</block>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "message",
+		props: {
+			value: {
+				type: Object,
+				required: true
+			}
+		},
+		methods: {
+			preimage: function (url) {
+				uni.previewImage({
+					urls: [url],
+					fail: function() {
+						uni.showToast({
+							title: '预览图片失败,请重试!',
+							icon: 'none'
+						})
+					}
+				});
+			},
+			url: function (url, title = '链接') {
+				// #ifdef H5
+				window.open(url);
+				return;
+				// #endif
+				
+				uni.setClipboardData({
+				    data: url,
+				    success: function () {
+						uni.showToast({
+							title: title + '已复制到剪切板,请在浏览器中打开',
+							icon: 'none'
+						})
+				    }
+				});
+			},
+			distributionGroupSelect: function(id) {
+				var that = this
+				that.ws.pageFun(function(){
+					that.ws.send({
+						c: 'Message',
+						a: 'distributionCsr',
+						data: {
+							group_id: id
+						}
+					})
+				})
+			},
+			openSession: function(id) {
+				this.ws.pageFun(() => {
+					this.ws.send({ c: 'Message', a: 'openSession', data: { 'id': id, 'type': 'user' } })
+				})
+			},
+			kbsClick: function(id) {
+				this.ws.pageFun(() => {
+					this.ws.send({
+						c: 'Message',
+						a: 'loadKbs',
+						data: {
+							id: id
+						}
+					})
+				})
+			},
+			manualCsr: function() {
+				this.ws.pageFun(() => {
+					this.ws.send({
+						c: 'Message',
+						a: 'distributionCsr',
+						data: {}
+					})
+				})
+			},
+			showNotice: function (id) {
+				this.ws.pageFun(() => {
+					this.ws.send({
+						c: 'Message',
+						a: 'groupChatNoticeOpt',
+						data: {
+							'id': id,
+							'type': 'get'
+						}
+					})
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+.image-message {
+	display: block;
+	width: 320rpx;
+	margin: -10rpx;
+}
+.file-suffix {
+	width: 100rpx !important;
+	height: 100rpx !important;
+	padding: 8rpx;
+}
+.file-message {
+	height: 100rpx;
+	display: flex;
+	align-items: center;
+}
+.file-message view {
+	margin: 6px 0 0 6px;
+	font-size: 28rpx;
+}
+.file-message .file-name {
+    font-weight: bold;
+    margin: 0;
+    color: $--black;
+}
+.file-message .file-size {
+    margin: 0;
+    color: $--gray;
+}
+.wrapper .message-item.me .file-name {
+    color: $--white;
+}
+.wrapper .message-item.me .file-size {
+    color: $--grey;
+}
+.file-message .down-file {
+    font-size: 12px;
+    padding: 0 10px;
+}
+.fastim-kbs {
+	font-size: 30rpx;
+}
+.csr-group-none {
+	font-weight: bold;
+	font-size: 28rpx;
+	margin-top: 20rpx;
+}
+.csr-group-name {
+	padding-top: 18rpx;
+	font-weight: bold;
+	font-size: 28rpx;
+}
+.kbs-items, .not-included {
+	padding-top: 20rpx;
+}
+.fastim-color-blue {
+    color: $--blue !important;
+}
+.group-chat-notice-title {
+	color: $--gray;
+	font-size: 28rpx;
+	font-weight: bold;
+}
+.group-chat-notice-content {
+	padding-top: 10rpx;
+	padding-bottom: 16rpx;
+	border-bottom: 1px solid $--grey;
+}
+.group-chat-notice-button {
+	min-width: 300rpx;
+	color: $--black;
+	font-size: 28rpx;
+	font-weight: bold;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	padding-top: 20rpx;
+}
+</style>

+ 118 - 0
addons/fastim/uniapp/components/session/session.vue

@@ -0,0 +1,118 @@
+<template>
+	<view>
+		<view hover-class="session-item-hover" :class="item.top" class="session-item">
+			<image class="session-avatar" :class="item.avatar_gray" :src="item.avatar" mode="aspectFill"></image>
+			<view class="session-info-right">
+				<view class="session-info-item">
+					<view class="session-info-name">{{item.nickname}}</view>
+					<view class="session-info-time im-color-gray">{{item.last_time}}</view>
+				</view>
+				<view class="session-info-item">
+					<view class="session-last-message">
+						<text class="session-user-status">{{(item.user_status ? item.user_status:'')}}</text>
+						<text v-if="item.unread_fixed_msg" class="unread_fixed_msg">[{{item.unread_fixed_msg}}]</text>
+						{{item.last_message}}
+					</view>
+					<view class="session-unread-count" v-if="item.unreadMessagesNumber > 0" :class="item.shield ? 'session-shield':''">{{item.unreadMessagesNumber}}</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "session",
+		props: {
+			item: {
+				type: Object,
+				required: true
+			}
+		},
+		data() {
+			return {
+
+			};
+		}
+	}
+</script>
+
+<style lang="scss">
+.session-item {
+	padding: 24rpx 4%;
+	display: flex;
+	align-items: center;
+	overflow: hidden;
+}
+.session-avatar {
+	height: 100rpx;
+	width: 100rpx;
+	border-radius: 16rpx;
+}
+.session-info-right {
+	margin-left: 20rpx;
+	flex: 1;
+	overflow: hidden;
+}
+.session-info-item {
+	display: flex;
+	height: 48rpx;
+	line-height: 48rpx;
+	justify-content: space-between;
+}
+.session-info-name {
+	font-size: 32rpx;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+	max-width: 400rpx;
+}
+.session-info-time {
+	overflow: hidden;
+	white-space: nowrap;
+	max-width: 180rpx;
+}
+.im-color-gray {
+	color: $--gray !important;
+}
+.session-last-message {
+	font-size: 30rpx;
+	color: $--gray;
+	display: inline-block;
+	width: 80%;
+	overflow: hidden;
+	white-space: nowrap;
+	vertical-align: middle;
+	text-overflow: ellipsis;
+}
+.session-unread-count {
+	height: 34rpx;
+	margin-top: 3rpx;
+	padding: 0 10rpx;
+	background: $--red;
+	text-align: center;
+	line-height: 34rpx;
+	border-radius: 50%;
+	color: $--white;
+}
+.im-img-gray {
+    -webkit-filter: grayscale(100%);
+    -moz-filter: grayscale(100%);
+    -ms-filter: grayscale(100%);
+    -o-filter: grayscale(100%);
+    filter: grayscale(100%);
+    filter: gray;
+}
+.session-shield {
+    background: #D0CFD1 !important;
+}
+.session-top {
+    background: #F8F9F9 !important;
+}
+.session-item-hover {
+	background: #EDEDED !important;
+}
+.unread_fixed_msg {
+	color: $--blue;
+}
+</style>

+ 272 - 0
addons/fastim/uniapp/js_sdk/wa-permission/permission.js

@@ -0,0 +1,272 @@
+/**
+ * 本模块封装了Android、iOS的应用权限判断、打开应用权限设置界面、以及位置系统服务是否开启
+ */
+
+var isIos
+// #ifdef APP-PLUS
+isIos = (plus.os.name == "iOS")
+// #endif
+
+// 判断推送权限是否开启
+function judgeIosPermissionPush() {
+	var result = false;
+	var UIApplication = plus.ios.import("UIApplication");
+	var app = UIApplication.sharedApplication();
+	var enabledTypes = 0;
+	if (app.currentUserNotificationSettings) {
+		var settings = app.currentUserNotificationSettings();
+		enabledTypes = settings.plusGetAttribute("types");
+		console.log("enabledTypes1:" + enabledTypes);
+		if (enabledTypes == 0) {
+			console.log("推送权限没有开启");
+		} else {
+			result = true;
+			console.log("已经开启推送功能!")
+		}
+		plus.ios.deleteObject(settings);
+	} else {
+		enabledTypes = app.enabledRemoteNotificationTypes();
+		if (enabledTypes == 0) {
+			console.log("推送权限没有开启!");
+		} else {
+			result = true;
+			console.log("已经开启推送功能!")
+		}
+		console.log("enabledTypes2:" + enabledTypes);
+	}
+	plus.ios.deleteObject(app);
+	plus.ios.deleteObject(UIApplication);
+	return result;
+}
+
+// 判断定位权限是否开启
+function judgeIosPermissionLocation() {
+	var result = false;
+	var cllocationManger = plus.ios.import("CLLocationManager");
+	var status = cllocationManger.authorizationStatus();
+	result = (status != 2)
+	console.log("定位权限开启:" + result);
+	// 以下代码判断了手机设备的定位是否关闭,推荐另行使用方法 checkSystemEnableLocation
+	/* var enable = cllocationManger.locationServicesEnabled();
+	var status = cllocationManger.authorizationStatus();
+	console.log("enable:" + enable);
+	console.log("status:" + status);
+	if (enable && status != 2) {
+		result = true;
+		console.log("手机定位服务已开启且已授予定位权限");
+	} else {
+		console.log("手机系统的定位没有打开或未给予定位权限");
+	} */
+	plus.ios.deleteObject(cllocationManger);
+	return result;
+}
+
+// 判断麦克风权限是否开启
+function judgeIosPermissionRecord() {
+	var result = false;
+	var avaudiosession = plus.ios.import("AVAudioSession");
+	var avaudio = avaudiosession.sharedInstance();
+	var permissionStatus = avaudio.recordPermission();
+	console.log("permissionStatus:" + permissionStatus);
+	if (permissionStatus == 1684369017 || permissionStatus == 1970168948) {
+		console.log("麦克风权限没有开启");
+	} else {
+		result = true;
+		console.log("麦克风权限已经开启");
+	}
+	plus.ios.deleteObject(avaudiosession);
+	return result;
+}
+
+// 判断相机权限是否开启
+function judgeIosPermissionCamera() {
+	var result = false;
+	var AVCaptureDevice = plus.ios.import("AVCaptureDevice");
+	var authStatus = AVCaptureDevice.authorizationStatusForMediaType('vide');
+	console.log("authStatus:" + authStatus);
+	if (authStatus == 3) {
+		result = true;
+		console.log("相机权限已经开启");
+	} else {
+		console.log("相机权限没有开启");
+	}
+	plus.ios.deleteObject(AVCaptureDevice);
+	return result;
+}
+
+// 判断相册权限是否开启
+function judgeIosPermissionPhotoLibrary() {
+	var result = false;
+	var PHPhotoLibrary = plus.ios.import("PHPhotoLibrary");
+	var authStatus = PHPhotoLibrary.authorizationStatus();
+	console.log("authStatus:" + authStatus);
+	if (authStatus == 3) {
+		result = true;
+		console.log("相册权限已经开启");
+	} else {
+		console.log("相册权限没有开启");
+	}
+	plus.ios.deleteObject(PHPhotoLibrary);
+	return result;
+}
+
+// 判断通讯录权限是否开启
+function judgeIosPermissionContact() {
+	var result = false;
+	var CNContactStore = plus.ios.import("CNContactStore");
+	var cnAuthStatus = CNContactStore.authorizationStatusForEntityType(0);
+	if (cnAuthStatus == 3) {
+		result = true;
+		console.log("通讯录权限已经开启");
+	} else {
+		console.log("通讯录权限没有开启");
+	}
+	plus.ios.deleteObject(CNContactStore);
+	return result;
+}
+
+// 判断日历权限是否开启
+function judgeIosPermissionCalendar() {
+	var result = false;
+	var EKEventStore = plus.ios.import("EKEventStore");
+	var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(0);
+	if (ekAuthStatus == 3) {
+		result = true;
+		console.log("日历权限已经开启");
+	} else {
+		console.log("日历权限没有开启");
+	}
+	plus.ios.deleteObject(EKEventStore);
+	return result;
+}
+
+// 判断备忘录权限是否开启
+function judgeIosPermissionMemo() {
+	var result = false;
+	var EKEventStore = plus.ios.import("EKEventStore");
+	var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(1);
+	if (ekAuthStatus == 3) {
+		result = true;
+		console.log("备忘录权限已经开启");
+	} else {
+		console.log("备忘录权限没有开启");
+	}
+	plus.ios.deleteObject(EKEventStore);
+	return result;
+}
+
+// Android权限查询
+function requestAndroidPermission(permissionID) {
+	return new Promise((resolve, reject) => {
+		plus.android.requestPermissions(
+			[permissionID], // 理论上支持多个权限同时查询,但实际上本函数封装只处理了一个权限的情况。有需要的可自行扩展封装
+			function(resultObj) {
+				var result = 0;
+				for (var i = 0; i < resultObj.granted.length; i++) {
+					var grantedPermission = resultObj.granted[i];
+					console.log('已获取的权限:' + grantedPermission);
+					result = 1
+				}
+				for (var i = 0; i < resultObj.deniedPresent.length; i++) {
+					var deniedPresentPermission = resultObj.deniedPresent[i];
+					console.log('拒绝本次申请的权限:' + deniedPresentPermission);
+					result = 0
+				}
+				for (var i = 0; i < resultObj.deniedAlways.length; i++) {
+					var deniedAlwaysPermission = resultObj.deniedAlways[i];
+					console.log('永久拒绝申请的权限:' + deniedAlwaysPermission);
+					result = -1
+				}
+				resolve(result);
+				// 若所需权限被拒绝,则打开APP设置界面,可以在APP设置界面打开相应权限
+				// if (result != 1) {
+				// gotoAppPermissionSetting()
+				// }
+			},
+			function(error) {
+				console.log('申请权限错误:' + error.code + " = " + error.message);
+				resolve({
+					code: error.code,
+					message: error.message
+				});
+			}
+		);
+	});
+}
+
+// 使用一个方法,根据参数判断权限
+function judgeIosPermission(permissionID) {
+	if (permissionID == "location") {
+		return judgeIosPermissionLocation()
+	} else if (permissionID == "camera") {
+		return judgeIosPermissionCamera()
+	} else if (permissionID == "photoLibrary") {
+		return judgeIosPermissionPhotoLibrary()
+	} else if (permissionID == "record") {
+		return judgeIosPermissionRecord()
+	} else if (permissionID == "push") {
+		return judgeIosPermissionPush()
+	} else if (permissionID == "contact") {
+		return judgeIosPermissionContact()
+	} else if (permissionID == "calendar") {
+		return judgeIosPermissionCalendar()
+	} else if (permissionID == "memo") {
+		return judgeIosPermissionMemo()
+	}
+	return false;
+}
+
+// 跳转到**应用**的权限页面
+function gotoAppPermissionSetting() {
+	if (isIos) {
+		var UIApplication = plus.ios.import("UIApplication");
+		var application2 = UIApplication.sharedApplication();
+		var NSURL2 = plus.ios.import("NSURL");
+		// var setting2 = NSURL2.URLWithString("prefs:root=LOCATION_SERVICES");		
+		var setting2 = NSURL2.URLWithString("app-settings:");
+		application2.openURL(setting2);
+
+		plus.ios.deleteObject(setting2);
+		plus.ios.deleteObject(NSURL2);
+		plus.ios.deleteObject(application2);
+	} else {
+		// console.log(plus.device.vendor);
+		var Intent = plus.android.importClass("android.content.Intent");
+		var Settings = plus.android.importClass("android.provider.Settings");
+		var Uri = plus.android.importClass("android.net.Uri");
+		var mainActivity = plus.android.runtimeMainActivity();
+		var intent = new Intent();
+		intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+		var uri = Uri.fromParts("package", mainActivity.getPackageName(), null);
+		intent.setData(uri);
+		mainActivity.startActivity(intent);
+	}
+}
+
+// 检查系统的设备服务是否开启
+// var checkSystemEnableLocation = async function () {
+function checkSystemEnableLocation() {
+	if (isIos) {
+		var result = false;
+		var cllocationManger = plus.ios.import("CLLocationManager");
+		var result = cllocationManger.locationServicesEnabled();
+		console.log("系统定位开启:" + result);
+		plus.ios.deleteObject(cllocationManger);
+		return result;
+	} else {
+		var context = plus.android.importClass("android.content.Context");
+		var locationManager = plus.android.importClass("android.location.LocationManager");
+		var main = plus.android.runtimeMainActivity();
+		var mainSvr = main.getSystemService(context.LOCATION_SERVICE);
+		var result = mainSvr.isProviderEnabled(locationManager.GPS_PROVIDER);
+		console.log("系统定位开启:" + result);
+		return result
+	}
+}
+
+module.exports = {
+	judgeIosPermission: judgeIosPermission,
+	requestAndroidPermission: requestAndroidPermission,
+	checkSystemEnableLocation: checkSystemEnableLocation,
+	gotoAppPermissionSetting: gotoAppPermissionSetting
+}

+ 23 - 0
addons/fastim/uniapp/main.js

@@ -0,0 +1,23 @@
+import Vue from 'vue'
+import App from './App'
+
+Vue.config.productionTip = false
+
+App.mpType = 'app'
+
+// 引入全局uView
+import uView from 'uview-ui'
+Vue.use(uView);
+
+const app = new Vue({
+    ...App
+})
+
+import httpInterceptor from '@/common/http.interceptor.js'
+Vue.use(httpInterceptor, app)
+
+import webSocket from '@/common/websocket.js'
+webSocket.ws.that = app
+Vue.prototype.ws = webSocket.ws
+
+app.$mount()

+ 130 - 0
addons/fastim/uniapp/manifest.json

@@ -0,0 +1,130 @@
+{
+    "name" : "FastIm",
+    "appid" : "__UNI__D5AC11E",
+    "description" : "FastAdmin企业IM客服系统",
+    "versionName" : "1.0.0",
+    "versionCode" : "100",
+    "transformPx" : false,
+    /* 5+App特有相关 */
+    "app-plus" : {
+        "safearea" : {
+            "bottom" : {
+                "offset" : "none"
+            }
+        },
+        "usingComponents" : true,
+        "nvueCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        /* 模块配置 */
+        "modules" : {
+            "VideoPlayer" : {},
+            "Push" : {}
+        },
+        /* 应用发布信息 */
+        "distribute" : {
+            /* android打包配置 */
+            "android" : {
+                "permissions" : [
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ]
+            },
+            /* ios打包配置 */
+            "ios" : {},
+            /* SDK配置 */
+            "sdkConfigs" : {
+                "push" : {
+                    "unipush" : {}
+                },
+                "ad" : {}
+            },
+            "icons" : {
+                "android" : {
+                    "hdpi" : "",
+                    "xhdpi" : "",
+                    "xxhdpi" : "",
+                    "xxxhdpi" : ""
+                },
+                "ios" : {
+                    "appstore" : "",
+                    "ipad" : {
+                        "app" : "",
+                        "app@2x" : "",
+                        "notification" : "",
+                        "notification@2x" : "",
+                        "proapp@2x" : "",
+                        "settings" : "",
+                        "settings@2x" : "",
+                        "spotlight" : "",
+                        "spotlight@2x" : ""
+                    },
+                    "iphone" : {
+                        "app@2x" : "",
+                        "app@3x" : "",
+                        "notification@2x" : "",
+                        "notification@3x" : "",
+                        "settings@2x" : "",
+                        "settings@3x" : "",
+                        "spotlight@2x" : "",
+                        "spotlight@3x" : ""
+                    }
+                }
+            }
+        }
+    },
+    /* 快应用特有相关 */
+    "quickapp" : {},
+    /* 小程序特有相关 */
+    "mp-weixin" : {
+        "appid" : "wx4552387af63efaec",
+        "setting" : {
+            "urlCheck" : false
+        },
+        "usingComponents" : true
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "h5" : {
+        "template" : "template.h5.html",
+        "router" : {
+            "mode" : "history",
+            "base" : "/h5/"
+        },
+        "devServer" : {
+            "https" : false
+        }
+    }
+}

+ 284 - 0
addons/fastim/uniapp/pages.json

@@ -0,0 +1,284 @@
+{
+	"easycom": {
+		"^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
+	},
+	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+		{
+		    "path" : "pages/message/message",
+		    "style" :
+		    {
+		        "navigationBarTitleText": "消息",
+		        "enablePullDownRefresh": true
+		    }
+		},
+		{
+			"path": "pages/center/index",
+			"style": {
+				"navigationBarTitleText": "我",
+				"enablePullDownRefresh": true
+			}
+		},
+		{
+            "path" : "pages/address-list/address-list",
+            "style" :
+            {
+                "navigationBarTitleText": "通讯录",
+                "enablePullDownRefresh": true
+            }
+        },
+		{
+            "path" : "pages/center/login",
+            "style" :
+            {
+                "navigationBarTitleText": "登录",
+                "enablePullDownRefresh": true,
+				"navigationStyle": "custom",
+				"navigationBarTextStyle": "white"
+            }
+        },
+		{
+            "path" : "pages/center/register",
+            "style" :
+            {
+                "navigationBarTitleText": "注册",
+                "enablePullDownRefresh": false,
+				"navigationStyle": "custom",
+				"navigationBarTextStyle": "white"
+            }
+        },
+		{
+            "path" : "pages/search/search",
+            "style" :
+            {
+                "navigationBarTitleText": "搜索",
+                "enablePullDownRefresh": false
+            }
+        },
+		{
+            "path" : "pages/center/to-do",
+            "style" :
+            {
+                "navigationBarTitleText": "待办",
+                "enablePullDownRefresh": true
+            }
+        },
+		{
+            "path" : "pages/search/new-contact",
+            "style" :
+            {
+                "navigationBarTitleText": "添加好友/群聊",
+                "enablePullDownRefresh": false
+            }
+        },
+		{
+            "path" : "pages/center/info",
+            "style" :
+            {
+                "navigationBarTitleText": "",
+                "enablePullDownRefresh": true,
+				"navigationBarBackgroundColor": "#f8f8f8"
+            }
+        },
+		{
+            "path" : "pages/search/add-contact",
+            "style" :
+            {
+                "navigationBarTitleText": "添加好友",
+                "enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#f8f8f8"
+            }
+        },
+		{
+            "path" : "pages/pick-user/pick-user",
+            "style" :
+            {
+                "navigationBarTitleText": "选择好友",
+                "enablePullDownRefresh": false
+            }
+        },
+		{
+            "path" : "pages/center/collection",
+            "style" :
+            {
+                "navigationBarTitleText": "我的收藏",
+                "enablePullDownRefresh": true
+            }
+        },
+		{
+            "path" : "pages/center/general-settings",
+            "style" :
+            {
+                "navigationBarTitleText": "通用设置",
+                "enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#f8f8f8"
+            }
+        },
+		{
+            "path" : "pages/center/privacy-settings",
+            "style" :
+            {
+                "navigationBarTitleText": "隐私设置",
+                "enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#f8f8f8"
+            }
+        },
+		{
+            "path" : "pages/center/quick-reply",
+            "style" :
+            {
+                "navigationBarTitleText": "快捷回复",
+                "enablePullDownRefresh": false
+            }
+        },
+		{
+            "path" : "pages/center/report",
+            "style" :
+            {
+                "navigationBarTitleText": "反馈问题",
+                "enablePullDownRefresh": false
+            }
+        },
+		{
+            "path" : "pages/center/edit-quick-reply",
+            "style" :
+            {
+                "navigationBarTitleText": "编辑快捷回复",
+                "enablePullDownRefresh": false
+            }
+        },
+		{
+            "path" : "pages/session-info/session-info",
+            "style" :
+            {
+                "navigationBarTitleText": "会话",
+                "enablePullDownRefresh": false,
+				"navigationStyle": "custom",
+				"navigationBarTextStyle": "black"
+            }
+        },
+		{
+            "path" : "pages/address-list/top-contacts",
+            "style" :
+            {
+                "navigationBarTitleText": "常用联系人",
+                "enablePullDownRefresh": true
+            }
+        },
+		{
+            "path" : "pages/address-list/group-chat",
+            "style" :
+            {
+                "navigationBarTitleText": "群聊",
+                "enablePullDownRefresh": true
+            }
+        },
+		{
+            "path" : "pages/session-info/notice-session-info",
+            "style" :
+            {
+                "navigationBarTitleText": "申请通知",
+                "enablePullDownRefresh": true
+            }
+        },
+		{
+            "path" : "pages/session-info/notice-message-info",
+            "style" :
+            {
+                "navigationBarTitleText": "通知消息",
+                "enablePullDownRefresh": false
+            }
+        },
+		{
+            "path" : "pages/center/edit-info",
+            "style" :
+            {
+                "navigationBarTitleText": "编辑资料",
+                "enablePullDownRefresh": false
+            }
+        },
+		{
+            "path" : "pages/session-info/chat-setting",
+            "style" :
+            {
+                "navigationBarTitleText": "聊天设置",
+                "enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#f8f8f8"
+            }
+        },
+		{
+            "path" : "pages/center/group_chat_users",
+            "style" :
+            {
+                "navigationBarTitleText": "群聊成员",
+                "enablePullDownRefresh": true,
+				"navigationBarBackgroundColor": "#f8f8f8"
+            }
+        },
+		{
+            "path" : "pages/center/user-shield",
+            "style" :
+            {
+                "navigationBarTitleText": "屏蔽的联系人",
+                "enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#f8f8f8"
+            }
+        },{
+            "path" : "pages/center/group_chat_notice",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "群公告",
+                "enablePullDownRefresh": true,
+				"navigationBarBackgroundColor": "#f8f8f8"
+            }
+        },
+		{
+            "path" : "pages/center/post_group_chat_notice",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "发布公告",
+                "enablePullDownRefresh": false,
+				"navigationBarBackgroundColor": "#f8f8f8"
+            }
+        },
+		{
+            "path" : "pages/center/group_chat_notice_users",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "用户列表",
+                "enablePullDownRefresh": true,
+				"navigationBarBackgroundColor": "#f8f8f8"
+            }
+        }
+    ],
+	"globalStyle": {
+		"navigationBarTextStyle": "black",
+		"navigationBarTitleText": "FastIm",
+		"navigationBarBackgroundColor": "#FFFFFF",
+		"backgroundColor": "#F8F8F8"
+	},
+	"tabBar": {
+	    "color": "#b2b2b2",
+	    "selectedColor": "#31a4f9",
+	    "borderStyle": "white",
+	    "backgroundColor": "#ffffff",
+	    "list": [
+			{
+			    "pagePath": "pages/message/message",
+			    "iconPath": "static/icon/message.png",
+			    "selectedIconPath": "static/icon/message-selected.png",
+			    "text": "消息"
+			},
+			{
+			    "pagePath": "pages/address-list/address-list",
+			    "iconPath": "static/icon/address-list.png",
+			    "selectedIconPath": "static/icon/address-list-selected.png",
+			    "text": "通讯录"
+			},
+			{
+	        "pagePath": "pages/center/index",
+	        "iconPath": "static/icon/center.png",
+	        "selectedIconPath": "static/icon/center-selected.png",
+	        "text": "我"
+	    }]
+	}
+}

+ 207 - 0
addons/fastim/uniapp/pages/address-list/address-list.vue

@@ -0,0 +1,207 @@
+<template>
+	<view>
+		<u-toast ref="uToast" />
+		<common :tips='commonTips'></common>
+		<u-mask z-index="1000" :custom-style="{background: 'rgba(0, 0, 0, 0.1)'}" :show="maskShow" @click="maskClick"></u-mask>
+		<!-- 顶部搜索栏-start -->
+		<view class="search">
+			<view class="search-box">
+				<u-search @search="search()" @custom="search()" v-model="keywords" class="search-box-u-search" shape="square" placeholder="搜索其实很简单" :clearabled="true" :show-action="false"></u-search>
+				<view @click="toggleMessageMenu" class="message-menu">
+					<u-icon class="message-menu-icon" name="plus" :class="messageMenu.show ? 'im-bg-grey':''" color="#3f3f3f" size="40"></u-icon>
+					<view v-if="messageMenu.show" :style="{top: messageMenu.top, left: messageMenu.left}" class="message-menu-box popup-menu">
+						<navigator class="popup-menu-item" url="/pages/search/new-contact" open-type="navigate">加好友/群</navigator>
+						<navigator url="/pages/pick-user/pick-user?action=create-group" open-type="navigate">
+							<view class="popup-menu-item">创建群聊</view>
+						</navigator>
+					</view>
+				</view>
+			</view>
+		</view>
+		<!-- 顶部搜索栏-end -->
+		<!-- 联系人-start -->
+		<view class="address-list">
+			<navigator class="address-item" url="/pages/session-info/notice-session-info?service_id=1">
+				<image class="address-avatar" src="../../static/img/new_friends.png" mode="widthFix"></image>
+				<view class="address-info">新的朋友</view>
+			</navigator>
+			<navigator class="address-item" url="/pages/address-list/top-contacts">
+				<image class="address-avatar" src="../../static/img/top-contacts.png" mode="widthFix"></image>
+				<view class="address-info">常用联系人</view>
+			</navigator>
+			<navigator class="address-item" url="/pages/address-list/group-chat">
+				<image class="address-avatar" src="../../static/img/group.png" mode="widthFix"></image>
+				<view class="address-info">群聊</view>
+			</navigator>
+		</view>
+		<u-index-list :scrollTop="scrollTop" :indexList="indexList">
+			<view v-for="(item, index) in indexList" :key="index">
+				<u-index-anchor :index="item" />
+				<navigator :url="'/pages/center/info?id=' + friend.friend_id" v-for="(friend,friendIdx) in friends[item]" :key="friendIdx" class="address-item">
+					<image class="address-avatar" :src="friend.avatar" mode="widthFix"></image>
+					<view class="address-info">{{friend.nickname}}</view>
+				</navigator>
+			</view>
+		</u-index-list>
+		<!-- 联系人-end -->
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				keywords: '',
+				maskShow: false,
+				messageMenu: {
+					show: false
+				},
+				scrollTop: 0,
+				indexList: [],
+				friends: [],
+				commonTips: ''
+			}
+		},
+		onLoad() {
+			this.ws.pageFun(this.pageDataLoad, this);
+		},
+		onShow:function(){
+			this.ws.checkNetwork(this)
+			if(this.ws.pageRefresh.addressList) {
+				// 被其他页面通知刷新此页
+				this.ws.pageFun(this.pageDataLoad, this);
+				this.ws.pageRefresh.addressList = false
+				return ;
+			}
+		},
+		onPullDownRefresh: function () {
+			this.ws.pageFun(this.pageDataLoad(true), this);
+			this.ws.onMessageCallBack.set('load_contact', (msg) => {
+				if (msg.data.data.method == 'all-friend' && msg.data.data.refresh) {
+					this.$refs.uToast.show({
+						title: '刷新成功~',
+						type: 'success'
+					})
+					uni.stopPullDownRefresh()
+				}
+			})
+		},
+		onPageScroll(e) {
+			this.scrollTop = e.scrollTop;
+		},
+		methods: {
+			pageDataLoad: function (refresh = false) {
+				this.ws.send({
+					c: 'User',
+					a: 'loadContact',
+					data: {
+						'method': 'all-friend',
+						'refresh': refresh
+					}
+				})
+			},
+			maskClick: function() {
+				this.messageMenu.show && this.toggleMessageMenu()
+			},
+			toggleMessageMenu: function(e) {
+				var that = this
+				if (that.messageMenu.show) {
+					that.maskShow = false
+					that.messageMenu.show = false
+					return ;
+				}
+				that.messageMenu = {
+					show: true,
+					top: (e.detail.y + 14) + 'px',
+					left: (e.detail.x - 120) + 'px'
+				}
+				that.maskShow = true
+			},
+			search: function() {
+				if (!this.keywords) {
+					this.$refs.uToast.show({
+						title: '请输入关键词~',
+						type: 'error'
+					})
+					return ;
+				}
+				uni.navigateTo({
+					url: '/pages/search/search?keywords=' + this.keywords,
+					success: () => {
+						this.keywords = ''
+					}
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+page {
+	background: #FFFFFF;
+}
+.search {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	background: #FFFFFF;
+}
+.search-box {
+	display: flex;
+	align-items: center;
+	padding: 20rpx 0 10rpx 0;
+}
+.search-box .search-box-u-search {
+	width: 616rpx;
+}
+.search-box .message-menu {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	margin-left: 20rpx;
+}
+.message-menu .message-menu-icon {
+	padding: 6rpx;
+	z-index: 1001;
+	border-radius: 4px;
+}
+.message-menu-box {
+	position: absolute;
+	background: $--white;
+	box-shadow: 0 0 20rpx rgba(0, 0, 0, .1);
+	border-radius: 4px;
+	padding: 10rpx 20rpx;
+	z-index: 1001;
+}
+.popup-menu .popup-menu-item {
+	padding: 20rpx 50rpx;
+	text-align: center;
+	border-bottom: 1px solid #F3F4F6;
+}
+.popup-menu .popup-menu-item:last-child {
+	border: none;
+}
+.address-item {
+	height: 120rpx;
+	display: flex;
+	align-items: center;
+	padding: 0rpx 4%;
+}
+.address-avatar {
+	height: 80rpx;
+	width: 80rpx;
+	border-radius: 16rpx;
+}
+.address-info {
+	flex: 1;
+	height: 120rpx;
+	line-height: 120rpx;
+	margin-left: 20rpx;
+	font-size: 32rpx;
+	border-bottom: 1px solid rgba(241, 241, 241, 0.6);
+}
+.im-bg-grey {
+	background-color: $--grey;
+	color: $--white;
+}
+</style>

+ 112 - 0
addons/fastim/uniapp/pages/address-list/group-chat.vue

@@ -0,0 +1,112 @@
+<template>
+	<view>
+		<u-toast ref="uToast" />
+		<common :tips='commonTips'></common>
+		<view class="search-box">
+			<u-search @search="search()" @custom="search()" v-model="keywords" class="search-box-u-search" shape="square" placeholder="搜索其实很简单" :clearabled="true" :animation="true"></u-search>
+		</view>
+		<view class="address-list">
+			<u-index-list :scrollTop="scrollTop" :indexList="indexList">
+				<view v-for="(item, index) in indexList" :key="index">
+					<u-index-anchor :index="item" />
+					<navigator :url="'/pages/center/info?type=group&id=' + friend.id" v-for="(friend,friendIdx) in friends[item]" :key="friendIdx" class="address-item">
+						<image class="address-avatar" :src="friend.avatar" mode="widthFix"></image>
+						<view class="address-info">{{friend.nickname}}</view>
+					</navigator>
+				</view>
+			</u-index-list>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				scrollTop: 0,
+				indexList: [],
+				friends: [],
+				keywords: '',
+				commonTips: ''
+			}
+		},
+		onLoad() {
+			this.ws.pageFun(this.pageDataLoad, this);
+		},
+		onShow:function(){
+			this.ws.checkNetwork(this)
+			if(this.ws.pageRefresh.addressList) {
+				// 被其他页面通知刷新此页
+				this.ws.pageFun(this.pageDataLoad, this);
+				this.ws.pageRefresh.addressList = false
+				return ;
+			}
+		},
+		onPullDownRefresh: function () {
+			this.ws.pageFun(this.pageDataLoad(true), this);
+			this.ws.onMessageCallBack.set('load_contact', (msg) => {
+				if (msg.data.data.method == 'all-group-chat' && msg.data.data.refresh) {
+					this.$refs.uToast.show({
+						title: '刷新成功~',
+						type: 'success'
+					})
+					uni.stopPullDownRefresh()
+				}
+			})
+		},
+		methods: {
+			pageDataLoad: function (refresh = false) {
+				this.ws.send({
+					c: 'User',
+					a: 'loadContact',
+					data: {
+						'method': 'all-group-chat',
+						'refresh': refresh
+					}
+				})
+			},
+			onPageScroll(e) {
+				this.scrollTop = e.scrollTop;
+			},
+			search: function () {
+				uni.navigateTo({
+					url: '/pages/search/search?keywords=' + this.keywords
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+.search-box {
+	display: flex;
+	align-items: center;
+	padding: 20rpx 4vw;
+	background: #FFFFFF;
+}
+.search-box .search-box-u-search {
+	flex: 1;
+}
+.address-list {
+	background: #FFFFFF;
+}
+.address-item {
+	height: 120rpx;
+	display: flex;
+	align-items: center;
+	padding: 0rpx 4%;
+}
+.address-avatar {
+	height: 80rpx;
+	width: 80rpx;
+	border-radius: 16rpx;
+}
+.address-info {
+	flex: 1;
+	height: 120rpx;
+	line-height: 120rpx;
+	margin-left: 20rpx;
+	font-size: 32rpx;
+	border-bottom: 1px solid rgba(241, 241, 241, 0.6);
+}
+</style>

+ 111 - 0
addons/fastim/uniapp/pages/address-list/top-contacts.vue

@@ -0,0 +1,111 @@
+<template>
+	<view>
+		<common :tips='commonTips'></common>
+		<view class="search-box">
+			<u-search @search="search()" @custom="search()" v-model="keywords" class="search-box-u-search" shape="square" placeholder="搜索其实很简单" :clearabled="true" :animation="true"></u-search>
+		</view>
+		<u-toast ref="uToast" />
+		<view class="address-list">
+			<u-index-list :scrollTop="scrollTop" :indexList="indexList">
+				<view v-for="(item, index) in indexList" :key="index">
+					<u-index-anchor :index="item" />
+					<navigator :url="'/pages/center/info?id=' + friend.friend_id" v-for="(friend,friendIdx) in friends[item]" :key="friendIdx" class="address-item">
+						<image class="address-avatar" :src="friend.avatar" mode="widthFix"></image>
+						<view class="address-info">{{friend.nickname}}</view>
+					</navigator>
+				</view>
+			</u-index-list>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				scrollTop: 0,
+				indexList: [],
+				friends: [],
+				keywords: '',
+				commonTips: ''
+			}
+		},
+		onLoad() {
+			this.ws.pageFun(this.pageDataLoad, this);
+		},
+		onShow:function(){
+			this.ws.checkNetwork(this)
+			if(this.ws.pageRefresh.addressList) {
+				// 被其他页面通知刷新此页
+				this.ws.pageFun(this.pageDataLoad, this);
+				return ;
+			}
+		},
+		onPullDownRefresh: function () {
+			this.ws.pageFun(this.pageDataLoad(true), this);
+			this.ws.onMessageCallBack.set('load_contact', (msg) => {
+				if(msg.data.data.method == 'top-friend' && msg.data.data.refresh) {
+					this.$refs.uToast.show({
+						title: '刷新成功~',
+						type: 'success'
+					})
+					uni.stopPullDownRefresh()
+				}
+			})
+		},
+		methods: {
+			pageDataLoad: function (refresh = false) {
+				this.ws.send({
+					c: 'User',
+					a: 'loadContact',
+					data: {
+						'method': 'top-friend',
+						'refresh': refresh
+					}
+				})
+			},
+			onPageScroll(e) {
+				this.scrollTop = e.scrollTop;
+			},
+			search: function () {
+				uni.navigateTo({
+					url: '/pages/search/search?keywords=' + this.keywords
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+.search-box {
+	display: flex;
+	align-items: center;
+	padding: 20rpx 4vw;
+	background: #FFFFFF;
+}
+.search-box .search-box-u-search {
+	flex: 1;
+}
+.address-list {
+	background: #FFFFFF;
+}
+.address-item {
+	height: 120rpx;
+	display: flex;
+	align-items: center;
+	padding: 0rpx 4%;
+}
+.address-avatar {
+	height: 80rpx;
+	width: 80rpx;
+	border-radius: 16rpx;
+}
+.address-info {
+	flex: 1;
+	height: 120rpx;
+	line-height: 120rpx;
+	margin-left: 20rpx;
+	font-size: 32rpx;
+	border-bottom: 1px solid rgba(241, 241, 241, 0.6);
+}
+</style>

+ 407 - 0
addons/fastim/uniapp/pages/center/collection.vue

@@ -0,0 +1,407 @@
+<template>
+	<view>
+		<u-toast ref="uToast" />
+		<common :tips='commonTips'></common>
+		<u-popup width="94%" border-radius="16" mode="center" :closeable="true" v-model="LinkPopupShow">
+			<view class="link-popup-box">
+				<view class="link-popup-title">你将要访问的网址是:</view>
+				<view class="link-popup-url">{{linkPopupUrl}}</view>
+				<view class="link-popup-tis">请确认是否继续访问,并注意您的账户和财产安全!</view>
+				<view class="link-popup-buttons">
+					<u-button :custom-style="{padding: '0 2rpx'}" class="link-popup-button-item" hover-class="none" type="primary" size="medium" plain>
+						<u-link :href="linkPopupUrl"><text class="link-popup-url-button">访问</text></u-link>
+					</u-button>
+					<u-button @click="LinkPopupShow = false" class="link-popup-button-item" type="default" size="medium">取消</u-button>
+				</view>
+			</view>
+		</u-popup>
+		
+		<u-action-sheet :list="actionList" @click="clickSwipeAction" v-model="actionShow"></u-action-sheet>
+		<u-modal v-model="modelShow" :mask-close-able="true" :show-cancel-button="true" @confirm="delConfirm" :content="modelContent"></u-modal>
+		
+		<view class="search-box">
+			<u-search @search="search()" @custom="search()" v-model="keywords" class="search-box-u-search" shape="square" placeholder="搜索收藏" :clearabled="true" :animation="true"></u-search>
+		</view>
+		<view class="tabs-box">
+			<u-tabs :list="tabs" :is-scroll="true" :current="tabCurrent" @change="tabChange"></u-tabs>
+		</view>
+		
+		<u-checkbox-group>
+			<view
+			:index="index"
+			v-for="(item, index) in collections"
+			:key="item.id"
+			class="collection-llst">
+				<view @longpress="openSwipeAction(index)" class="collection-item">
+					<u-checkbox v-if="action == 'send'" @change="collectionSelect" shape="circle" v-model="item.checked" :name="index">
+						<view class="collection-wrap">
+							<link-message v-if="item.type == 'link'" :value="item" :LinkPopupShow.sync="LinkPopupShow" :linkPopupUrl.sync="linkPopupUrl"></link-message>
+							<view v-else-if="item.type == 'video'" @click="playVideo(item)" class="video-message-box">
+								<view v-show="!item.play" class="video-message-cover">
+									<view class="video-message-cover-play">
+										<u-icon name="play-right-fill" size="40" color="#ffffff"></u-icon>
+										<view>{{item.currentTime + (item.duration ? ' / ' + item.duration:'')}}</view>
+									</view>
+								</view>
+								<!-- #ifdef MP-WEIXIN -->
+								<video @timeupdate="timeUpdate" :style="{width: item.play ? '400rpx':'1rpx'}" @fullscreenchange="fullScreenChange" :id="'video-' + item.id" :ref="'video-' + item.id" class="video-message-wx" :src="item.message" object-fit="contain" controls></video>
+								<!-- #endif -->
+								<!-- #ifndef MP-WEIXIN -->
+								<video @timeupdate="timeUpdate" :style="{display: item.play ? 'block':'none'}" @fullscreenchange="fullScreenChange" :id="'video-' + item.id" :ref="'video-' + item.id" class="video-message" :src="item.message" object-fit="contain" controls></video>
+								<!-- #endif -->
+							</view>
+							<view v-else-if="item.type == 'audio'" @click="playAudio(item, index)" class="audio-message">
+								<u-icon :name="item.play ? 'pause':'play-right-fill'" size="36" color="#999999"></u-icon>
+								<text class="audio-message-text">{{item.currentTime + (item.duration ? ' / ' + item.duration:'')}}</text>
+							</view>
+							<message v-else :value="item"></message>
+							<view class="collection-footer">{{ item.createtime + ' | ' + item.from}}</view>
+						</view>
+					</u-checkbox>
+					<block v-else>
+						<view class="collection-wrap">
+							<link-message v-if="item.type == 'link'" :value="item" :LinkPopupShow.sync="LinkPopupShow" :linkPopupUrl.sync="linkPopupUrl"></link-message>
+							<view v-else-if="item.type == 'video'" @click="playVideo(item)" class="video-message-box">
+								<view v-show="!item.play" class="video-message-cover">
+									<view class="video-message-cover-play">
+										<u-icon name="play-right-fill" size="40" color="#ffffff"></u-icon>
+										<view>{{item.currentTime + (item.duration ? ' / ' + item.duration:'')}}</view>
+									</view>
+								</view>
+								<!-- #ifdef MP-WEIXIN -->
+								<video @timeupdate="timeUpdate" :style="{width: item.play ? '400rpx':'1rpx'}" @fullscreenchange="fullScreenChange" :id="'video-' + item.id" :ref="'video-' + item.id" class="video-message-wx" :src="item.message" object-fit="contain" controls></video>
+								<!-- #endif -->
+								<!-- #ifndef MP-WEIXIN -->
+								<block v-if="userPlatform == 'ios'">
+									<video @timeupdate="timeUpdate" v-if="item.play" @fullscreenchange="fullScreenChange" :id="'video-' + item.id" :ref="'video-' + item.id" class="video-message" :src="item.message" object-fit="contain" controls></video>
+								</block>
+								<video v-else @timeupdate="timeUpdate" :style="{display: item.play ? 'block':'none'}" @fullscreenchange="fullScreenChange" :id="'video-' + item.id" :ref="'video-' + item.id" class="video-message" :src="item.message" object-fit="contain" controls></video>
+								<!-- #endif -->
+							</view>
+							<view v-else-if="item.type == 'audio'" @click="playAudio(item, index)" class="audio-message">
+								<u-icon :name="item.play ? 'pause':'play-right-fill'" size="36" color="#999999"></u-icon>
+								<text class="audio-message-text">{{item.currentTime + (item.duration ? ' / ' + item.duration:'')}}</text>
+							</view>
+							<message v-else :value="item"></message>
+							<view class="collection-footer">{{ item.createtime + ' | ' + item.from}}</view>
+						</view>
+					</block>
+				</view>
+			</view>
+		</u-checkbox-group>
+		<view v-if="!collections.length" class="im-data-none">没有更多了~</view>
+		
+		<view v-if="action == 'send'" class="send-btn">
+			<u-button @click="send" shape="square" size="medium" type="primary" :disabled="sendBtnDisabled">{{sendBtn}}</u-button>
+		</view>
+		
+		<view v-if="action == 'send'" class="seat"></view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				action: '',
+				keywords: '',
+				tabs: [],
+				tabCurrent: 0,
+				collections: [],
+				loadGroup: false,
+				collectionSelected: new Set(),
+				collectionSelectData: [],
+				sendBtn: '发送收藏',
+				sendBtnDisabled: true,
+				LinkPopupShow: false,
+				linkPopupUrl: '',
+				actionList: [{
+					text: '转发'
+				},
+				{
+					text: '删除',
+					color: '#f74c31'
+				}],
+				actionShow: false,
+				actionIndex: 0,
+				modelShow: false,
+				modelContent: '',
+				userPlatform: '',
+				commonTips: ''
+			}
+		},
+		onLoad:function(query){
+			this.action = query.action ? query.action:'browse'
+			this.ws.pageFun(this.pageDataLoad, this);
+			
+			if (this.action == 'send') {
+				uni.setNavigationBarTitle({
+					title: '请选择收藏'
+				})
+			}
+		},
+		onShow() {
+			this.ws.checkNetwork(this)
+		},
+		onPullDownRefresh: function () {
+			this.ws.pageFun(this.pageDataLoad(true), this);
+			this.ws.onMessageCallBack.set('get_groups', (msg) => {
+				if (msg.data.data.type == 'collection' && msg.data.data.refresh) {
+					this.$refs.uToast.show({
+						title: '刷新成功~',
+						type: 'success'
+					})
+					uni.stopPullDownRefresh()
+				}
+			})
+		},
+		onReachBottom: function () {
+			if (this.loadGroup) {
+				this.loadCollection(this.loadGroup.id, parseInt(this.loadGroup.page)+1)
+			}
+		},
+		onHide: function (){
+			this.ws.pauseAudio('hide', this, 'collection')
+		},
+		onUnload: function () {
+			this.ws.pauseAudio('unload', this, 'collection')
+		},
+		methods: {
+			timeUpdate: function (e) {
+				this.ws.playVideoItem.duration = this.ws.sToHs(Math.floor(e.detail.duration))
+				this.ws.playVideoItem.currentTime = this.ws.sToHs(Math.floor(e.detail.currentTime))
+			},
+			fullScreenChange: function (e) {
+				if (!e.detail.fullScreen) {
+					var videoContext = uni.createVideoContext('video-' + this.ws.playVideoItem.id, this)
+					videoContext.pause()
+					this.ws.playVideoItem.play = false
+				}
+			},
+			playVideo: function(item){
+				this.ws.playVideo(item, this, 'collection')
+			},
+			playAudio: function(item, index) {
+				this.ws.playAudio(item, index, 0, this, 'collection')
+			},
+			pageDataLoad: function (refresh = false) {
+				let message = {
+					c: 'ImBase',
+					a: 'getGroups',
+					data: {
+						'type': 'collection',
+						'refresh': refresh
+					}
+				}
+				this.ws.send(message);
+				this.userPlatform = this.ws.userPlatform
+			},
+			loadCollection: function (id, page, keywords = '') {
+				var that = this
+				that.ws.pageFun(function(){
+					that.ws.send({
+						c: 'User',
+						a: 'loadGroup',
+						data: {
+							id: id,
+							page: page,
+							keywords: keywords,
+							method: 'load-collection'
+						}
+					})
+				}, that)
+			},
+			search: function(value) {
+				this.loadCollection(this.tabs[this.tabCurrent].id, 1, value)
+			},
+			tabChange: function (index) {
+				this.ws.pauseAudio('unload', this, 'collection')
+				this.tabCurrent = index
+				this.loadCollection(this.tabs[index].id, 1)
+			},
+			openSwipeAction (index) {
+				this.actionIndex = index
+				this.actionShow = true
+			},
+			delConfirm: function () {
+				var that = this, item = that.collections[that.actionIndex]
+				that.ws.pageFun(function(){
+					let message = {
+						c: 'User',
+						a: 'collection',
+						data: {
+							id: item.id,
+							action: 'del',
+							index: that.actionIndex
+						}
+					}
+					that.ws.send(message);
+				}, that)
+			},
+			clickSwipeAction (index) {
+				var that = this
+				if(index == 1) {
+					that.modelShow = true
+					that.modelContent = '确定删除收藏吗?'
+				} else if (index == 0) {
+					uni.navigateTo({
+						url: '/pages/pick-user/pick-user?action=message-forward&forward_type=collection&message_id=' + that.collections[that.actionIndex].id
+					})
+				}
+			},
+			collectionSelect: function (e) {
+				var that = this, collection = that.collections[e.name]
+				if (e.value) {
+					that.collectionSelected.add(collection.id)
+					that.collectionSelectData.push(collection)
+				} else {
+					that.collectionSelected.delete(collection.id)
+				}
+				that.sendBtn = '发送收藏(' + that.collectionSelected.size + ')';
+				that.sendBtnDisabled = that.collectionSelected.size ? false:true
+			},
+			send: function () {
+				var selected = []
+				for (let c of this.collectionSelected) {
+					for (let pc in this.collectionSelectData) {
+						if (this.collectionSelectData[pc].id == c) {
+							selected.push(this.collectionSelectData[pc])
+						}
+					}
+				}
+				this.ws.collectionSelected = selected
+				uni.navigateBack({
+					delta: 1
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+.search-box {
+	display: flex;
+	align-items: center;
+	padding: 20rpx 4vw;
+	background: #fff;
+}
+.search-box .search-box-u-search {
+	flex: 1;
+}
+.collection-wrap {
+	padding: 0 20rpx;
+}
+.collection-llst {
+	background: #FFFFFF;
+}
+.collection-item {
+	font-size: 30rpx;
+	display: flex;
+	width: 100vw;
+	align-items: center;
+	padding: 20rpx;
+	border-bottom: 1px solid #F2F3F4;
+}
+.collection-image {
+	max-width: 200rpx;
+	max-height: 200rpx;
+	margin-left: 20rpx;
+}
+.collection-footer {
+	font-size: 28rpx;
+	color: #999999;
+	margin-top: 20rpx;
+}
+.send-btn {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	height: 100rpx;
+	width: 100vw;
+	position: fixed;
+	bottom: 0;
+	z-index: 9999;
+}
+.seat {
+	height: 100rpx;
+	width: 100vw;
+}
+
+.link-popup-box {
+	padding: 80rpx 20rpx 20rpx 20rpx;
+	text-align: center;
+	font-size: 28rpx;
+}
+.link-popup-title {
+	font-weight: bold;
+	font-size: 30rpx;
+}
+.link-popup-url {
+	display: block;
+	width: 90%;
+	margin: 0 auto;
+	color: $--blue;
+	padding: 20rpx 0;
+	word-break: break-all;
+	word-wrap: break-word;
+}
+.link-popup-tis {
+	color: $--gray;
+}
+.link-popup-buttons {
+	padding-top: 20rpx;
+	display: flex;
+	height: 160rpx;
+	align-items: center;
+	justify-content: center;
+}
+.link-popup-button-item {
+	margin-right: 40rpx;
+}
+.link-popup-url-button {
+	display: inline-block;
+	height: 70rpx;
+	line-height: 70rpx;
+	padding: 0 80rpx;
+}
+.video-message-box {
+	position: relative;
+	min-height: 400rpx;
+	min-width: 400rpx;
+	overflow: hidden;
+}
+.video-message {
+	width: 400rpx;
+	height: 400rpx;
+}
+.video-message-wx {
+	width: 400rpx;
+	height: 400rpx;
+	position: absolute;
+}
+.video-message-cover {
+	height: 400rpx;
+	width: 400rpx;
+	background: $--gray;
+	color: $--white;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+.video-message-cover-play {
+	text-align: center;
+	font-size: 30rpx;
+}
+.audio-message {
+	display: flex;
+	align-items: center;
+	font-size: 30rpx;
+	color: $--gray;
+	border: 1px solid $--grey;
+	border-radius: 16rpx;
+	padding: 20rpx;
+}
+.audio-message-text {
+	padding-left: 20rpx;
+}
+</style>

+ 363 - 0
addons/fastim/uniapp/pages/center/edit-info.vue

@@ -0,0 +1,363 @@
+<template>
+	<view>
+		<u-toast ref="uToast" />
+		<common :tips='commonTips'></common>
+		<u-form class="info-form">
+
+			<u-form-item v-if="showAvatarUpload" label-width="180" label="头  像">
+				<view>
+					<u-upload ref="uUpload" @on-remove="avatarOnRemove" @on-change="avatarOnChange" :size-type="['compressed']"
+					name="file" width="160" height="160" :form-data="uploadFormData" :action="uploadAction" :max-count="avatarFileNumber"
+					:file-list="avatarFileList"></u-upload>
+				</view>
+			</u-form-item>
+
+			<block v-for="(item, index) in detail" :key="index">
+				<u-form-item v-if="item.type == 'input' || item.type == 'textarea'" label-width="180"
+					:label="item.title">
+					<u-input :type="item.type" v-model="item.value" :placeholder="'请输入' + (item.placeholderTitle ? item.placeholderTitle:item.title)" />
+				</u-form-item>
+				<u-form-item v-if="item.type == 'select'" label-width="180" :label="item.title">
+					<u-select :default-value="item.value" v-model="item.show" mode="single-column" :list="item.data"
+						@confirm="confirmSelect"></u-select>
+					<view @click="openSelect(index)">{{item.data[item.value[0]].label}}</view>
+				</u-form-item>
+				<u-form-item v-if="item.type == 'date'" label-width="180" :label="item.title">
+					<u-picker :default-time="item.value[0] + '-' + item.value[1] + '-' + item.value[2]"
+						v-model="item.show" mode="time" @confirm="confirmData"></u-picker>
+					<view @click="openSelect(index, index)">
+						{{item.value[0] + '-' + item.value[1] + '-' + item.value[2]}}
+					</view>
+				</u-form-item>
+			</block>
+			<u-button v-if="detail.length" :loading="submitButtonStatus" :disabled="submitButtonStatus"
+				class="submit-button" type="primary" @click="submit">{{submitButtonText}}</u-button>
+		</u-form>
+	</view>
+</template>
+
+<script>
+	var userIdsStr = ''
+	export default {
+		data() {
+			return {
+				id: 0,
+				type: '',
+				detail: [],
+				openSelectName: false,
+				showAvatarUpload: false,
+				uploadAction: '',
+				uploadFormData: new Object(),
+				avatarFileList: [],
+				userToken: '',
+				avatarFileNumber: 2,
+				newAvatar: false,
+				submitButtonStatus: false,
+				submitButtonText: '保存',
+				commonTips: ''
+			}
+		},
+		onLoad: function(query) {
+			this.id = query.id ? query.id : 0
+			this.type = query.type ? query.type : 'user'
+			userIdsStr = query.ids ? query.ids : ''
+			let userinfo = uni.getStorageSync('userinfo');
+			this.userToken = userinfo.token
+		},
+		onShow() {
+			this.ws.checkNetwork(this)
+		},
+		onReady: function(query) {
+			var that = this
+			that.uploadAction = that.ws.buildUrl('upload', that.userToken)
+			
+			if (this.type == 'create-group') {
+				uni.setNavigationBarTitle({
+					title: '创建群聊'
+				});
+				that.showAvatarUpload = true
+				that.detail = [{
+					title: '群  名',
+					placeholderTitle: '群名',
+					type: 'input',
+					name: 'nickname',
+					value: ''
+				},
+				{
+					title: '群聊简介',
+					type: 'textarea',
+					name: 'bio',
+					value: ''
+				},
+				{
+					title: '加群模式',
+					type: 'select',
+					name: 'add_mode',
+					data: [{
+							value: '0',
+							label: '需管理员审核',
+							extra: '2'
+						},
+						{
+							value: '1',
+							label: '无需审核',
+							extra: '2'
+						}
+					],
+					value: [0],
+					show: false
+				},
+				{
+					title: '邀请免审',
+					type: 'select',
+					name: 'invite_join_group',
+					data: [{
+							value: '0',
+							label: '成员邀请好友需审核',
+							extra: '3'
+						},
+						{
+							value: '1',
+							label: '成员邀请好友免审核',
+							extra: '3'
+						},
+					],
+					value: [0],
+					show: false
+				},
+				{
+					title: '历史消息',
+					type: 'select',
+					name: 'history_message',
+					data: [{
+							value: '0',
+							label: '不允许新入群用户查看',
+							extra: '4'
+						},
+						{
+							value: '1',
+							label: '允许新入群用户查看',
+							extra: '4'
+						},
+					],
+					value: [0],
+					show: false
+				},
+				{
+					title: '成员发言',
+					type: 'select',
+					name: 'speak',
+					data: [{
+							value: '0',
+							label: '允许发言',
+							extra: '5'
+						},
+						{
+							value: '1',
+							label: '禁止发言',
+							extra: '5'
+						},
+					],
+					value: [0],
+					show: false
+				},
+				{
+					title: '检索设置',
+					type: 'select',
+					name: 'retrieval_settings',
+					data: [{
+						value: '0',
+						label: '禁用搜素加群',
+						extra: '6'
+					},
+					{
+						value: '1',
+						label: '允许被搜素到',
+						extra: '6'
+					}],
+					value: [0],
+					show: false
+				}]
+				
+				that.submitButtonText = '创建群聊'
+				
+				this.ws.pageFun(() => {
+					this.ws.send({
+						c: 'ImBase',
+						a: 'getUploadMultipart'
+					});
+				}, this)
+			} else {
+				// time需要在ready赋值
+				this.ws.pageFun(this.pageDataLoad, this);
+			}
+		},
+		methods: {
+			openSelect: function(index, name = false) {
+				this.detail[index].show = true
+				this.openSelectName = name ? name : false
+			},
+			confirmData: function(value) {
+				this.detail[this.openSelectName].value = [
+					value.year,
+					value.month,
+					value.day
+				]
+			},
+			confirmSelect: function(value) {
+				var valueIndex = 0,
+					data = this.detail[value[0].extra].data;
+				for (var i = 0; i < data.length; i++) {
+					if (data[i].value == value[0].value) {
+						valueIndex = i
+					}
+				}
+				this.detail[value[0].extra].value = [valueIndex]
+			},
+			pageDataLoad: function() {
+				var that = this
+				let message = {
+					c: 'User',
+					a: 'infoDetail',
+					data: {
+						id: that.id,
+						type: that.type,
+						method: that.type + '-edit'
+					}
+				}
+				that.ws.send(message);
+				
+				that.ws.send({
+					c: 'ImBase',
+					a: 'getUploadMultipart'
+				});
+			},
+			avatarOnSuccess: function(data) {
+				this.newAvatar = this.ws.imgUrl(data.data.fullurl)
+				if (this.avatarFileNumber >= 2) {
+					this.$refs.uUpload.clear();
+					this.avatarFileList = [{
+						url: this.newAvatar
+					}]
+					this.avatarFileNumber = 1
+				}
+			},
+			avatarOnChange: function(res, index, lists) {
+				res = JSON.parse(res.data);
+				if (res.code == 1) {
+					this.avatarOnSuccess(res)
+				} else {
+					this.ws.pageFun(() => {
+						this.ws.send({
+							c: 'ImBase',
+							a: 'getUploadMultipart'
+						})
+					}, this)
+					
+					this.$refs.uUpload.remove(index);
+					
+					uni.showModal({
+						title: '温馨提示',
+						content: res.msg,
+						showCancel: false
+					})
+				}
+			},
+			avatarOnRemove: function() {
+				this.avatarFileNumber = 1
+			},
+			submit: function() {
+				var that = this
+				that.submitButtonStatus = true
+				var values = {};
+				for (var i = 0; i < that.detail.length; i++) {
+					if (that.detail[i].type == 'date') {
+						values[that.detail[i].name] = that.detail[i].value[0] + '-' + that.detail[i].value[1] + '-' +
+							that.detail[i].value[2]
+					} else if (that.detail[i].type == 'select') {
+						values[that.detail[i].name] = that.detail[i].data[that.detail[i].value[0]].value
+					} else {
+						values[that.detail[i].name] = that.detail[i].value
+					}
+				}
+
+				if (that.newAvatar) {
+					values.avatar = that.newAvatar
+				}
+
+				var nicknameErrorTip = '', message = {}
+				if (that.type == 'user') {
+					nicknameErrorTip = '请输入正确的昵称~'
+					values.method = 'post-user-edit'
+					values.type = 'user'
+					values.id = that.id
+					message = {
+						c: 'User',
+						a: 'infoDetail',
+						data: values
+					}
+				} else if (that.type == 'group') {
+					nicknameErrorTip = '请输入正确的群聊名称~'
+					values.method = 'post-group-edit'
+					values.type = 'group'
+					values.id = that.id
+					message = {
+						c: 'User',
+						a: 'infoDetail',
+						data: values
+					}
+				} else if (that.type == 'create-group') {
+					nicknameErrorTip = '请输入正确的群聊名称~'
+					message = {
+						c: 'Message',
+						a: 'createGroup',
+						data: {
+							pickuser: userIdsStr,
+							data: values
+						}
+					}
+				}
+
+				if (!values['nickname']) {
+					that.$refs.uToast.show({
+						title: nicknameErrorTip,
+						type: 'error'
+					})
+					return;
+				}
+
+				that.ws.pageFun(function() {
+					that.ws.send(message);
+					that.ws.showMsgCallback = function() {
+						if (that.type != 'create-group') {
+							setTimeout(function() {
+								that.submitButtonStatus = false
+								that.ws.pageRefresh.centerInfo = true
+								uni.navigateBack({
+									delta: 1
+								})
+							}, 2000)
+						}
+					}
+				}, that)
+			}
+		}
+	}
+</script>
+
+<style>
+	page {
+		background: #FFFFFF;
+	}
+	.info-form {
+		display: block;
+		width: 92vw;
+		margin: 0 auto;
+	}
+
+	.submit-button {
+		width: 60vw;
+		display: block;
+		margin: 40rpx auto;
+	}
+</style>

+ 119 - 0
addons/fastim/uniapp/pages/center/edit-quick-reply.vue

@@ -0,0 +1,119 @@
+<template>
+	<view>
+		<u-toast ref="uToast" />
+		<common :tips='commonTips'></common>
+		<u-form class="quick-reply" :model="form" ref="uForm">
+			<u-form-item label="标题" label-position="top" prop="title">
+				<u-input placeholder="请输入快捷回复标题" v-model="form.title" />
+			</u-form-item>
+			<u-form-item label="内容" label-position="top" prop="content">
+				<u-input type="textarea" placeholder="请输入快捷回复内容" v-model="form.content" />
+			</u-form-item>
+			<u-form-item :border-bottom="false" label="状态">
+				<u-switch active-value="1" inactive-value="0" slot="right" v-model="form.status"></u-switch>
+			</u-form-item>
+			<u-button class="quick-reply-button" :loading="submitButtonStatus" :disabled="submitButtonStatus" type="primary" @click="submit">提交</u-button>
+		</u-form>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				id: 0,
+				form: {
+					title: '',
+					content: '',
+					status: true
+				},
+				rules: {
+					title: [{
+						required: true,
+						message: '请输入标题',
+						trigger: 'change'
+					}],
+					content: [{
+						required: true,
+						message: '请输入标题',
+						trigger: 'change'
+					}]
+				},
+				submitButtonStatus: false,
+				commonTips: ''
+			}
+		},
+		onLoad: function(data) {
+			this.id = data.id ? data.id:0
+			var title = (data.type == 'add') ? '添加':'编辑'
+			uni.setNavigationBarTitle({
+			    title: title + '快捷回复'
+			});
+			
+			if (this.id) {
+				this.ws.pageFun(this.pageDataLoad, this);
+			}
+		},
+		onShow() {
+			this.ws.checkNetwork(this)
+		},
+		onReady() {
+			this.$refs.uForm.setRules(this.rules);
+		},
+		methods: {
+			pageDataLoad: function () {
+				let message = {
+					c: 'ImBase',
+					a: 'fastReply',
+					data: {
+						method: 'edit',
+						id: this.id,
+						form: 'uni-app'
+					}
+				}
+				this.ws.send(message);
+			},
+			submit: function() {
+				var that = this
+				this.$refs.uForm.validate(valid => {
+					if (valid) {
+						that.submitButtonStatus = true
+						that.ws.pageFun(function() {
+							that.form.status = that.form.status ? 1:0;
+							let message = {
+								c: 'ImBase',
+								a: 'fastReply',
+								data: {
+									method: 'update',
+									page: 1,
+									form: 'uni-app',
+									data: {
+										id: that.form.id ? that.form.id:0,
+										title: that.form.title,
+										content: that.form.content,
+										status: that.form.status
+									}
+								}
+							}
+							that.ws.send(message);
+						}, that)
+					}
+				});
+			}
+		}
+	}
+</script>
+
+<style>
+.quick-reply {
+	display: block;
+	width: 92vw;
+	margin: 0 auto;
+}
+.quick-reply-button {
+	width: 60vw;
+	display: block;
+	margin: 0 auto;
+	margin-top: 200rpx;
+}
+</style>

+ 170 - 0
addons/fastim/uniapp/pages/center/general-settings.vue

@@ -0,0 +1,170 @@
+<template>
+	<view>
+		<u-toast ref="uToast" />
+		<common :tips='commonTips'></common>
+		<view v-if="loadingShow" class="u-loading">
+			<u-loading color="#6388fb" mode="circle"></u-loading>
+		</view>
+		<u-form ref="uForm">
+			<block v-for="(item, index) in form" :key="index">
+				<view class="form-item-box">
+					<u-form-item :border-bottom="item.borderBottom" v-if="item.type == 'select'" label-position="top" :label="item.title">
+						<u-select :default-value="item.value" v-model="item.show" mode="single-column" :list="item.data" @confirm="confirmSelect"></u-select>
+						<view @click="openSelect(index)">{{item.data[item.value[0]].label}}</view>
+					</u-form-item>
+					
+					<u-form-item :border-bottom="item.name == 'busy_reply' ? false:true" v-if="item.type == 'text' || item.type == 'textarea'" label-position="top" :label="item.title">
+						<u-input :type="item.type" v-model="item.value" :placeholder="'请输入' + item.title" />
+					</u-form-item>
+					
+					<view v-if="item.name == 'busy_reply'" class="box20rpx"></view>
+					
+					<u-form-item :border-bottom="item.borderBottom" v-if="item.type == 'radio'" label-position="top" :label="item.title">
+						<u-radio-group v-model="item.value">
+							<u-radio v-for="(item, indexr) in item.content" :key="indexr" :name="indexr">
+								{{ item }}
+							</u-radio>
+						</u-radio-group>
+					</u-form-item>
+				</view>
+			</block>
+			<u-button v-if="form.length" :loading="submitButtonStatus" :disabled="submitButtonStatus" class="submit-button" type="primary" @click="submit">保存</u-button>
+		</u-form>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				userinfo: [],
+				form: {
+					
+				},
+				openSelectName: false,
+				submitButtonStatus: false,
+				loadingShow: true,
+				commonTips: ''
+			}
+		},
+		onLoad:function(){
+			this.userinfo = uni.getStorageSync('userinfo');
+			this.pageDataLoad()
+		},
+		onShow: function () {
+			this.ws.checkNetwork(this)
+		},
+		methods: {
+			submit: function () {
+				var  that = this
+				that.submitButtonStatus = true
+				var values = {};
+				for (var i = 0; i < that.form.length; i++) {
+					if (that.form[i].type == 'select') {
+						values[that.form[i].name] = that.form[i].data[that.form[i].value[0]].value
+					} else {
+						values[that.form[i].name] = that.form[i].value
+					}
+				}
+				
+				that.ws.pageFun(function() {
+					var message = { c: 'ImBase', a: 'updateUserConfig', data: values }
+					that.ws.send(message);
+					that.ws.showMsgCallback = function () {
+						that.ws.initializeData.config.user_config.new_message_shake = values.new_message_shake
+						that.ws.initializeData.config.user_config.new_message_sound = values.new_message_sound
+						that.ws.initializeData.config.user_config.send_message_key = values.send_message_key
+						setTimeout(function(){
+							that.submitButtonStatus = false
+							uni.navigateBack({
+								delta: 1
+							})
+						}, 2000)
+					}
+				}, that)
+			},
+			openSelect: function(index, name = false){
+				this.form[index].show = true
+				this.openSelectName = name ? name:false
+			},
+			confirmSelect: function (value) {
+				var valueIndex = 0, data = this.form[value[0].extra].data;
+				for (var i = 0; i < data.length; i++) {
+					if (data[i].value == value[0].value) {
+						valueIndex = i
+					}
+				}
+				this.form[value[0].extra].value = [valueIndex]
+			},
+			pageDataLoad: function () {
+				var that = this
+				that.$u.get(that.ws.buildUrl('get-config-item-data', that.userinfo.token), {}).then(res => {
+					
+					that.loadingShow = false
+					var general = res.data.imConfigGroup.general.list, lastGeneral = (general.length - 1);
+					
+					for (let g = 0; g < general.length; g++) {
+						
+						if (general[g].name == 'send_message_key') {
+							general[g].content[1] = '发送键'
+						}
+						if (general[g].name == 'login_status') {
+							general[g].title = '登录后状态'
+						}
+						
+						general[g].borderBottom = (g == lastGeneral) ? false:true
+						
+						if (general[g].type == 'select') {
+							general[g].show = false
+							general[g].data = []
+							
+							for (let i = 0; i < general[g].content.length; i++) {
+								general[g].data.push({ value: i, label: general[g].content[i], extra: g });
+								if (parseInt(general[g].value[0]) == i) {
+									general[g].value = [i]
+								}
+							}
+						} else if(general[g].type == 'text') {
+							general[g].type = 'textarea'
+						} else if(general[g].type == 'string') {
+							general[g].type = 'text'
+						}
+					}
+					
+					that.form = general
+				})
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+page{
+	background-color: $--bg-color;
+}
+.form-item-box {
+	width: 100vw;
+	background-color: $--white;
+	padding: 0 5vw;
+}
+.form-item {
+	border-bottom: 1px solid red;
+}
+.box20rpx {
+	height: 20rpx;
+	width: 100vw;
+	background-color: $--bg-color;
+	margin-left: -5vw;
+}
+.submit-button {
+	width: 60vw;
+	display: block;
+	margin: 40rpx auto;
+}
+.u-loading {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	margin-top: 60rpx;
+}
+</style>

+ 278 - 0
addons/fastim/uniapp/pages/center/group_chat_notice.vue

@@ -0,0 +1,278 @@
+<template>
+	<view>
+		<u-toast ref="uToast" />
+		<common :tips='commonTips'></common>
+		<u-popup width="600" v-model="showNotice" :closeable="true" mode="center" border-radius="12">
+			<view class="group-notice-title">群公告</view>
+			<view v-if="noticeInfo" class="group-notice-content">
+				<view>
+					{{noticeInfo.content}}
+				</view>
+				<view class="im-group-notice-imgs">
+					<image v-for="(img, imgindex) in noticeInfo.images" :key="imgindex" :src="img" mode="widthFix"></image>
+				</view>
+				<view class="group-notice-footer">
+					<text>{{noticeInfo.publisher.nickname}}<text v-if="noticeInfo.publisher.remark">({{noticeInfo.publisher.remark}})</text><text class="footer-createtime">发表于{{noticeInfo.createtime}}</text></text>
+					<text class="notice-item-footer-text">{{noticeInfo.reading_number}}人已读</text>
+				</view>
+				<view class="group-notice-button">
+					<u-button v-if="noticeInfo.receipt" :type="noticeInfo.receipted ? 'default':'primary'"  @click="receiptNotice(noticeInfo.id, noticeInfo.receipt, noticeInfo.receipted)">{{noticeInfo.receipted ? '您已确认':'确认收到'}}</u-button>
+					<u-button v-if="!noticeInfo.receipt" type="default" @click="showNotice = false;">关闭</u-button>
+				</view>
+			</view>
+		</u-popup>
+		<u-action-sheet @click="clickOperation" :list="operationList" v-model="showOperation"></u-action-sheet>
+		<view v-if="isLeader" class="add-notice-box">
+			<u-button type="primary" class="add-notice" @click="post" size="mini">发布公告</u-button>
+		</view>
+		<view class="notice-list">
+			<view v-for="(item, index) in noticeList" :key="index" class="notice-item">
+				<view class="notice-item-content">
+					<view @click="openNotice(item.id)">
+						<view v-if="item.top" class="blue-tag">置顶</view>
+						<view v-if="item.popup" class="blue-tag">弹窗展示</view>
+						{{item.content}}
+						<view class="im-group-notice-imgs">
+							<image v-for="(img, imgindex) in item.images" :key="imgindex" :src="img" mode="widthFix"></image>
+						</view>
+					</view>
+					<view class="notice-item-footer">
+						<text>{{item.publisher.nickname}}<text v-if="item.remark">({{item.remark}})</text><text class="footer-createtime">发表于{{item.createtime}}</text></text>
+						<text @click="gotoPage(item.id, 'read-number')" class="notice-item-footer-text">{{item.reading_number}}人已读</text>
+						<text @click="gotoPage(item.id, 'receipted-number')" v-if="(item.receipt && item.receipted)" class="notice-item-footer-text">{{item.receiptedCount}}人已确认</text>
+						<view v-if="item.receipt && !item.receipted && !isLeader" @click="receiptNotice(item.id, item.receipt, item.receipted)" class="notice-item-footer-right">确认收到</view>
+						<view v-if="isLeader" @click="operation(item.id)" class="notice-item-footer-right">操作</view>
+					</view>
+				</view>
+			</view>
+			<view class="im-data-none" v-if="loadStatus">{{loadStatus}}</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				group_id: 0,
+				session_id: 0,
+				noticeList: [],
+				noticeInfo: null,
+				loadGroupChatNotice: [],
+				loadStatus: '',
+				isLeader: false,
+				showOperation: false,
+				operationId: 0,
+				operationList: [{
+					text: '编辑',
+					color: '#FF9C4E'
+				},
+				{
+					text: '删除',
+					color: '#f74c31'
+				}],
+				showNotice: false,
+				commonTips: ''
+			}
+		},
+		onLoad(query) {
+			var that = this
+			that.group_id = query.group_id
+			that.session_id = query.session_id
+		},
+		onShow() {
+			this.ws.checkNetwork(this)
+			this.ws.pageFun(this.pageDataLoad, this);
+		},
+		onPullDownRefresh: function () {
+			this.ws.pageFun(this.pageDataLoad(1, true), this);
+			this.ws.onMessageCallBack.set('group_chat_notice', (msg) => {
+				if (parseInt(msg.data.data.group_id) == parseInt(this.group_id) && msg.data.data.refresh) {
+					this.$refs.uToast.show({
+						title: '刷新成功~',
+						type: 'success'
+					})
+					uni.stopPullDownRefresh()
+				}
+			})
+		},
+		methods: {
+			openNotice: function (id) {
+				var that = this
+				that.ws.pageFun(function() {
+					that.ws.send({
+						c: 'Message',
+						a: 'groupChatNoticeOpt',
+						data: {
+							'id': id,
+							'type': 'get'
+						}
+					});
+				}, that);
+			},
+			gotoPage: function (id, name) {
+				uni.navigateTo({
+					url: '/pages/center/group_chat_notice_users?id=' + id + '&name=' + name
+				})
+			},
+			clickOperation: function (index) {
+				var that = this
+				if (index == 0) {
+					uni.navigateTo({
+						url: '/pages/center/post_group_chat_notice?group_id=' + this.group_id + '&id=' + this.operationId
+					})
+				} else if (index == 1) {
+					uni.showModal({
+						title:'温馨提示',
+						content: '您确认要删除公告吗?',
+						success(res) {
+							if (res.confirm) {
+								that.ws.pageFun(function () {
+									that.ws.send({
+										c: 'Message',
+										a: 'groupChatNoticeOpt',
+										data: {
+											'id': that.operationId,
+											'type': 'del'
+										}
+									})
+								}, that)
+							}
+						}
+					})
+				}
+			},
+			receiptNotice: function (id, receipt, receipted) {
+				var that = this
+				that.showNotice = false
+				if (!receipt || (receipt && receipted)) {
+					return false;
+				}
+				that.ws.pageFun(function() {
+					that.ws.send({
+						c: 'Message',
+						a: 'receiptGroupChatNotice',
+						data: {
+							'id': id,
+							'session_id': that.session_id,
+							'type': 'list'
+						}
+					});
+				}, that);
+			},
+			operation: function(id){
+				this.showOperation = true
+				this.operationId = id
+			},
+			pageDataLoad: function (page = 1, refresh = false) {
+				var that = this
+				this.ws.send({
+					c: 'Message',
+					a: 'groupChatNotice',
+					data: {
+						'group_id': that.group_id,
+						'method': 'list',
+						'page': page,
+						'refresh': refresh
+					}
+				})
+			},
+			onReachBottom: function () {
+				if (this.loadGroupChatNotice) {
+					this.pageDataLoad(parseInt(this.loadGroupChatNotice.page)+1)
+				}
+			},
+			post: function () {
+				uni.navigateTo({
+					url: '/pages/center/post_group_chat_notice?group_id=' + this.group_id
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+page {
+	background: #F8F8F8;
+}
+.add-notice-box {
+	height: 80rpx;
+	padding-top: 20rpx;
+}
+.add-notice {
+	float: right;
+	margin-right: 4%;
+}
+.notice-list {
+	width: 94%;
+	margin: 20rpx auto;
+}
+.notice-item {
+	border-radius: 12rpx;
+	margin-bottom: 20rpx;
+	padding: 16rpx;
+	background: #FFFFFF;
+	padding-bottom: 0;
+}
+.notice-item-content {
+	min-height: 120rpx;
+}
+.blue-tag {
+	display: inline-block;
+	line-height: 40rpx;
+	color: $--white;
+	background: $--blue;
+	padding: 0 12rpx;
+	border-radius: 8rpx;
+	margin-right: 8rpx;
+}
+.im-group-notice-imgs {
+    display: flex;
+	flex-wrap: wrap;
+	padding-top: 20rpx;
+}
+.im-group-notice-imgs image {
+	width: 130rpx;
+	height: 130rpx;
+}
+.notice-item-footer,.group-notice-footer {
+	font-size: 27rpx;
+	color: $--gray;
+	padding-top: 6rpx;
+	padding-bottom: 10rpx;
+	height: 70rpx;
+	line-height: 70rpx;
+}
+.group-notice-footer {
+	margin-top: 10rpx;
+}
+.notice-item-footer-text {
+	padding-left: 26rpx;
+}
+.notice-item-footer-right {
+	float: right;
+	height: 50rpx;
+	line-height: 50rpx;
+	background: #2979FF;
+	padding: 0 20rpx;
+	border-radius: 12rpx;
+	color: $--white;
+}
+.footer-createtime {
+	padding-left: 10rpx;
+}
+.group-notice-title {
+	padding: 30rpx;
+	padding-bottom: 20rpx;
+	font-size: 30rpx;
+	font-weight: bold;
+}
+.group-notice-content {
+	padding: 0 30rpx;
+}
+.group-notice-button button {
+	width: 280rpx;
+	display: block;
+	margin: 20rpx auto;
+}
+</style>

+ 130 - 0
addons/fastim/uniapp/pages/center/group_chat_notice_users.vue

@@ -0,0 +1,130 @@
+<template>
+	<view>
+		<u-toast ref="uToast" />
+		<common :tips='commonTips'></common>
+		<view class="users">
+			<view class="user-item">
+				<view class="user-info">
+					<text class="head">昵称</text>
+				</view>
+				<view class="createtime">{{createtimeText}}</view>
+			</view>
+			<view v-for="(item, index) in users" :key="index" class="user-item user-item-box">
+				<view class="user-info">
+					<image class="avatar" :src="item.avatar" mode="widthFix"></image>
+					<text class="nickname">{{item.nickname}}</text>
+				</view>
+				<view class="createtime">{{item.createtime}}</view>
+			</view>
+		</view>
+		<view class="im-data-none" v-if="loadStatus">{{loadStatus}}</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				id: 0,
+				name: '',
+				createtimeText: '',
+				users: [],
+				loadStatus: '',
+				commonTips: ''
+			}
+		},
+		onLoad(query) {
+			var that = this
+			that.id = query.id
+			that.name = query.name
+			if (this.name == 'receipted-number') {
+				uni.setNavigationBarTitle({
+				    title: '已确认用户列表'
+				});
+				that.createtimeText = '确认时间'
+			} else {
+				uni.setNavigationBarTitle({
+				    title: '已阅读用户列表'
+				});
+				that.createtimeText = '阅读时间'
+			}
+			that.ws.pageFun(that.pageDataLoad, that);
+		},
+		onShow() {
+			this.ws.checkNetwork(this)
+		},
+		onPullDownRefresh: function () {
+			this.ws.pageFun(this.pageDataLoad(1, true), this);
+			this.ws.onMessageCallBack.set('group-notice-users', (msg) => {
+				if (parseInt(msg.data.data.id) == parseInt(this.id) && msg.data.data.refresh) {
+					this.$refs.uToast.show({
+						title: '刷新成功~',
+						type: 'success'
+					})
+					uni.stopPullDownRefresh()
+				}
+			})
+		},
+		onReachBottom: function () {
+			if (this.loadGroupNoticeUsers) {
+				this.pageDataLoad(parseInt(this.loadGroupNoticeUsers.page)+1)
+			}
+		},
+		methods: {
+			pageDataLoad: function(page = 1, refresh = false) {
+				var that = this
+				that.ws.send({
+					c: 'Message',
+					a: 'groupChatNoticeOpt',
+					data: {
+						'id': that.id,
+						'method': that.name,
+						'page': page,
+						'type': 'get-users',
+						'refresh': refresh
+					}
+				});
+			},
+		}
+	}
+</script>
+
+<style>
+page {
+	background: #F8F8F8;
+}
+.users {
+	display: block;
+	width: 94%;
+	margin: 0 auto;
+}
+.head {
+	margin-left: 40rpx;
+}
+.user-item {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+.user-item-box {
+	margin-top: 10rpx;
+	padding: 10rpx;
+	padding-right: 0;
+}
+.user-info {
+	display: flex;
+	align-items: center;
+	flex: 8;
+}
+.createtime {
+	flex: 4;
+	text-align: center;
+}
+.avatar {
+	height: 60rpx;
+	width: 60rpx;
+}
+.nickname {
+	margin-left: 6rpx;
+}
+</style>

+ 195 - 0
addons/fastim/uniapp/pages/center/group_chat_users.vue

@@ -0,0 +1,195 @@
+<template>
+	<view>
+		<u-toast ref="uToast" />
+		<common :tips='commonTips'></common>
+		<u-action-sheet @click="clickLongpressAction" :list="longPressActionList" v-model="longPressActionShow"></u-action-sheet>
+		<view class="search-box">
+			<u-search bg-color="#ffffff" @search="search()" v-model="keywords" class="search-box-u-search" shape="square" placeholder="搜索其实很简单" :clearabled="true" :show-action="false"></u-search>
+			<view @click="search(false)" v-if="showSearchRes" class="search-button">取消</view>
+			<view @click="search(true)" v-else class="search-button">搜索</view>
+		</view>
+		<view v-if="!showSearchRes" class="group-chat-users">
+			<navigator :url="'/pages/center/info?id=' + user.id" v-for="(user, userIdx) in leader" :key="userIdx" class="user-item">
+				<image class="user-avatar" :src="user.avatar" mode="widthFix"></image>
+				<view class="user-info"><u-tag class="leader-tag" text="群主" size="mini" shape="circle" type="warning" />{{user.nickname ? user.nickname_origin + '(' + user.nickname + ')':user.nickname_origin}}</view>
+				<u-button v-if="!user.is_friend" @click.stop="addFriend(user.id)" size="mini" class="user-button">加好友</u-button>
+			</navigator>
+			<u-index-list :scrollTop="scrollTop" :indexList="indexList">
+				<view v-for="(item, index) in indexList" :key="index">
+					<u-index-anchor :index="item" />
+					<navigator @longpress="longpressUser(user.id, user.nickname ? user.nickname_origin + '(' + user.nickname + ')':user.nickname_origin)" :url="'/pages/center/info?id=' + user.id" v-for="(user, userIdx) in users[item]" :key="userIdx" class="user-item">
+						<image class="user-avatar" :src="user.avatar" mode="widthFix"></image>
+						<view class="user-info">{{user.nickname ? user.nickname_origin + '(' + user.nickname + ')':user.nickname_origin}}</view>
+						<u-button v-if="!user.is_friend" @click.stop="addFriend(user.id)" size="mini" class="user-button">加好友</u-button>
+					</navigator>
+				</view>
+			</u-index-list>
+		</view>
+		<view v-if="showSearchRes" class="search-res">
+			<navigator @longpress="longpressUser(user.id, user.nickname ? user.nickname_origin + '(' + user.nickname + ')':user.nickname_origin)" :url="'/pages/center/info?id=' + user.id" v-for="(user, userIdx) in groupChatMember" :key="userIdx" class="user-item">
+				<image class="user-avatar" :src="user.avatar" mode="widthFix"></image>
+				<view class="user-info">{{user.nickname ? user.nickname_origin + '(' + user.nickname + ')':user.nickname_origin}}</view>
+				<u-button v-if="!user.is_friend" @click.stop="addFriend(user.id)" size="mini" class="user-button">加好友</u-button>
+			</navigator>
+			<view v-if="!groupChatMember.length" class="im-data-none">没有搜索结果~</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				id: 0,
+				keywords: '',
+				scrollTop: 0,
+				indexList: [],
+				users: [],
+				leader: [],
+				showSearchRes: false,
+				groupChatMember: [],
+				longPressActionID: 0,
+				longPressActionShow: false,
+				longPressActionList: [],
+				commonTips: ''
+			}
+		},
+		onLoad:function(query){
+			this.id = query.id ? query.id:0
+			this.ws.pageFun(this.pageDataLoad, this);
+		},
+		onShow() {
+			this.ws.checkNetwork(this)
+		},
+		onPullDownRefresh: function () {
+			this.ws.pageFun(this.pageDataLoad(true), this);
+			this.ws.onMessageCallBack.set('group_chat_member', (msg) => {
+				if (parseInt(msg.data.data.id) == parseInt(this.id) && msg.data.data.refresh) {
+					this.$refs.uToast.show({
+						title: '刷新成功~',
+						type: 'success'
+					})
+					uni.stopPullDownRefresh()
+				}
+			})
+		},
+		methods: {
+			pageDataLoad: function(refresh = false) {
+				var that = this
+				that.ws.send({
+					c: 'Message',
+					a: 'groupChatMember',
+					data: {
+						'id': this.id,
+						'method': 'get',
+						'lettersort': 'lettersort',
+						'refresh': refresh
+					}
+				});
+			},
+			onPageScroll(e) {
+				this.scrollTop = e.scrollTop;
+			},
+			search: function (show = true) {
+				var that = this
+				if (!show) {
+					that.groupChatMember = []
+					that.showSearchRes = false
+					return ;
+				}
+				if (!that.keywords) {
+					that.$refs.uToast.show({
+						title: '请输入关键词~',
+						type: 'error'
+					})
+					return ;
+				}
+				that.ws.pageFun(function(){
+					that.ws.send({
+						c: 'Message',
+						a: 'groupChatMember',
+						data: {
+							'id': that.id,
+							'method': 'get',
+							'keywords': that.keywords
+						}
+					})
+				}, that)
+			},
+			addFriend: function (id) {
+				uni.navigateTo({
+					url: '/pages/search/add-contact?type=user&id=' + id
+				})
+			},
+			longpressUser: function (id, username) {
+				this.longPressActionID = id
+				this.longPressActionShow = true
+				this.longPressActionList = [{
+					text: '删除成员',
+					color: '#f74c31',
+					subText: username
+				}]
+			},
+			clickLongpressAction: function (index) {
+				var that = this
+				if (index == 0) {
+					that.ws.pageFun(function(){
+						that.ws.pageRefresh.chatSetting = true
+						that.ws.pageRefresh.sessionInfo = true
+						that.ws.send({
+							c: 'Message',
+							a: 'delGroupMember',
+							data: {
+								'id': that.id,
+								'member_id': that.longPressActionID
+							}
+						})
+					}, that)
+				}
+			}
+		}
+	}
+</script>
+
+<style>
+page {
+	background: #F8F8F8;
+}
+.search-box {
+	display: flex;
+	align-items: center;
+	padding: 20rpx 4vw;
+}
+.search-box .search-box-u-search {
+	flex: 1;
+}
+.search-button {
+	padding-left: 30rpx;
+}
+.user-item {
+	display: flex;
+	align-items: center;
+	background: #FFFFFF;
+	height: 100rpx;
+	padding: 20rpx;
+}
+.user-avatar {
+	height: 90rpx;
+	width: 90rpx;
+	margin-right: 20rpx;
+	border-radius: 16rpx;
+}
+.user-info {
+	display: flex;
+	font-size: 30rpx;
+	width: 62%;
+	overflow: hidden;
+}
+.leader-tag {
+	margin-right: 10rpx;
+}
+.user-button {
+	margin-left: auto;
+	margin-right: 30rpx;
+}
+</style>

+ 150 - 0
addons/fastim/uniapp/pages/center/index.vue

@@ -0,0 +1,150 @@
+<template>
+	<view>
+		<u-toast ref="uToast" />
+		<common :tips='commonTips'></common>
+		<u-modal v-model="logoutShow" :show-cancel-button="true" @confirm="logout()" confirm-text="注销" content="确实要注销登录吗?"></u-modal>
+		<navigator url="/pages/center/info">
+			<view class="u-flex user-box u-p-l-30 u-p-r-20 u-p-b-30 u-p-t-30">
+				<view class="u-m-r-10">
+					<u-avatar :src="info.avatar" size="140"></u-avatar>
+				</view>
+				<view class="u-flex-1">
+					<view class="u-font-18 u-p-b-20">{{info.nickname}}</view>
+					<view class="u-font-14 u-tips-color">账号:{{info.id}}</view>
+				</view>
+				<view class="u-m-l-10 u-p-10 user-status" :class="'user-status-' + info.status.value">•<text>{{info.status.chinese}}</text></view>
+				<view class="u-m-l-10 u-p-10">
+					<u-icon name="arrow-right" color="#969799" size="28"></u-icon>
+				</view>
+			</view>
+		</navigator>
+		
+		<view class="u-m-t-20 to-do">
+			<navigator url="/pages/center/to-do">
+				<u-cell-item :border-top="false">
+					<view class="to-do-title" slot="title">
+						<image class="to-do-icon" src="/static/icon/to-do.png" mode="widthFix"></image>
+						<text>{{'待办 • ' + TODOCount}}</text>
+					</view>
+				</u-cell-item>
+			</navigator>
+			<navigator url="/pages/center/collection">
+				<u-cell-item :border-bottom="false" icon="star" :title="'收藏 • ' + collectionCount"></u-cell-item>
+			</navigator>
+		</view>
+		
+		<view class="u-m-t-20">
+			<u-cell-group :border="false">
+				<navigator :border-top="false" url="/pages/center/general-settings">
+					<u-cell-item icon="setting" title="通用设置"></u-cell-item>
+				</navigator>
+				<navigator url="/pages/center/privacy-settings">
+					<u-cell-item icon="lock" title="隐私设置"></u-cell-item>
+				</navigator>
+				<navigator v-if="info.type == 'csr'" url="/pages/center/quick-reply">
+					<u-cell-item icon="zhuanfa" title="快捷回复"></u-cell-item>
+				</navigator>
+				<navigator url="/pages/center/report?title=反馈">
+					<u-cell-item :border-bottom="false" icon="question-circle" title="反馈问题"></u-cell-item>
+				</navigator>
+			</u-cell-group>
+		</view>
+		
+		<view class="u-m-t-20">
+			<u-cell-group :border="false">
+				<u-cell-item @click="logoutConfirm" :border-bottom="false" :border-top="false" icon="close-circle" title="注销登录"></u-cell-item>
+			</u-cell-group>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				info: {},
+				TODOCount: 0,
+				collectionCount: 0,
+				logoutShow: false,
+				commonTips: ''
+			}
+		},
+		onLoad() {
+			this.info = uni.getStorageSync('userinfo');// 防止需要在pageDataLoad使用到用户ID
+			this.info.avatar = this.ws.imgUrl(this.info.avatar)
+			this.info.status = {chinese: '加载中', value: 0}
+		},
+		onShow:function(){
+			this.ws.checkNetwork(this)
+			this.ws.pageFun(this.pageDataLoad, this);
+		},
+		onPullDownRefresh: function () {
+			this.ws.pageFun(this.pageDataLoad(true), this);
+			this.ws.onMessageCallBack.set('center', (msg) => {
+				if (msg.data.data.refresh) {
+					this.$refs.uToast.show({
+						title: '刷新成功~',
+						type: 'success'
+					})
+					uni.stopPullDownRefresh()
+				}
+			})
+		},
+		methods: {
+			pageDataLoad: function (refresh = false) {
+				var that = this
+				this.ws.send({
+					c: 'User',
+					a: 'center',
+					data: {
+						'id': that.info.id,
+						'refresh': refresh
+					}
+				})
+			},
+			logoutConfirm: function () {
+				this.logoutShow = true
+			},
+			logout: function () {
+				var that = this
+				// #ifdef APP-PLUS
+				if (!this.ws.socketOpen || parseInt(that.ws.initializeData.config.uni_push_switch) == 0) {
+					that.ws.logout()
+				} else {
+					that.ws.pushCid('logout')
+				}
+				// #endif
+				
+				// #ifndef APP-PLUS
+				that.ws.logout()
+				// #endif
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+page{
+	background-color: #ededed;
+}
+.user-box, .to-do {
+	background-color: $--white;
+}
+.user-status {
+	font-size: 30rpx;
+}
+.user-status text {
+	padding-left: 10rpx;
+}
+.to-do u-cell-item u-icon {
+	margin-right: 10rpx;
+}
+.to-do-title {
+	display: flex;
+	align-items: center;
+}
+.to-do-icon {
+	width: 28rpx;
+	margin: 0 12rpx 0 6rpx;
+}
+</style>

+ 317 - 0
addons/fastim/uniapp/pages/center/info.vue

@@ -0,0 +1,317 @@
+<template>
+	<view>
+		<u-toast ref="uToast" />
+		<common :tips='commonTips'></common>
+		<u-modal v-model="modelShow" :mask-close-able="true" :show-cancel-button="true" @confirm="delConfirm" :content="modelContent"></u-modal>
+		<u-action-sheet v-if="type == 'user'" :list="userStatusList" :safe-area-inset-bottom="true" @click="changeUserStatus" v-model="userStatusBool"></u-action-sheet>
+		<view class="user-box">
+			<image :src="info.avatar" class="user-avatar" mode="aspectFill"></image>
+			<view class="user-right">
+				<view class="user-right-item user-name">
+					<text class="nickname-text">{{info.nickname_origin ? info.nickname_origin:info.nickname}}</text>
+					<text v-if="info.remark">(<text class="nickname-text">{{info.remark}}</text>)</text>
+				</view>
+				<view class="user-right-item">{{info.bio}}</view>
+				<view v-if="type == 'user'" @click="userStatusBool = (info.id == userinfo.id ? true:false)" :class="'user-status-' + info.status.value" class="user-right-item user-status"> • {{info.status.chinese}}</view>
+				<view v-if="type == 'group' || type == 'service'" class="user-right-item user-status">{{info.other}}</view>
+			</view>
+			<view v-if="info.friend && (info.id != userinfo.id)" class="collection-user">
+				<u-icon @click="collectionUser" color="#6388fb" :name="(info.group == 'common') ? 'star-fill':'star'"></u-icon>
+			</view>
+		</view>
+		<u-cell-group v-if="(type == 'group' && !info.deletetime) || type == 'user' || type == 'service'" :border="false" class="user-info-box">
+			
+			<block v-for="(item,index) in detail" :key="index">
+				<!-- 在u-cell-item内的v-if失效了? -->
+				<u-cell-item v-if="item.type && item.type == 'leader'" hover-class="none" :border-top="false" :border-bottom="false" :arrow="false" :title="item.title">
+					<navigator :url="'/pages/center/info?type=user&id=' + item.leader.id" class="leader-avatar-box" slot="right-icon">
+						<image :src="item.leader.avatar" class="leader-avatar" mode="widthFix"></image>
+						<u-icon name="arrow-right"></u-icon>
+					</navigator>
+				</u-cell-item>
+				<u-cell-item v-else-if="item.name == 'id'" @click="clipboardId(item.value)" hover-class="none" :border-top="false" :border-bottom="false" :arrow="false" :title="item.title">
+					<view slot="right-icon">{{item.value}}</view>
+				</u-cell-item>
+				<u-cell-item v-else hover-class="none" :border-top="false" :border-bottom="false" :arrow="false" :title="item.title">
+					<view slot="right-icon">{{item.value}}</view>
+				</u-cell-item>
+			</block>
+			
+			<navigator v-if="type == 'group'" :url="'/pages/center/group_chat_notice?group_id=' + info.id">
+				<u-cell-item hover-class="none" :border-top="false" :border-bottom="false" title="群公告"></u-cell-item>
+			</navigator>
+			<navigator v-if="type == 'group'" :url="'/pages/center/group_chat_users?id=' + info.id">
+				<u-cell-item hover-class="none" :border-top="false" :border-bottom="false" title="群成员列表"></u-cell-item>
+			</navigator>
+			
+			<u-cell-item v-if="(type == 'user') && info.friend && (info.id != userinfo.id)" hover-class="none" :border-bottom="false" :border-top="false" :arrow="true" title="备注">
+				<view slot="right-icon">
+					<u-input @blur="saveRemark" placeholder="请输入备注内容" :clearable="false" input-align="right" type="text" v-model="info.remark" :border="false" />
+				</view>
+			</u-cell-item>
+		</u-cell-group>
+		<view class="im-data-none" v-if="type == 'group' && info.deletetime">群聊已解散~</view>
+		<view class="user-buttons">
+			<u-button @click="userAction(item.action, item.data, item.type, item.opt)" v-for="(item,index) in buttons" :key="index" :type="item.btype">{{item.name}}</u-button>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				id: 0,
+				type: '',
+				userStatusBool: false,
+				userStatusList: [{
+					text: '在线',
+					color: '#21E589'
+				}, {
+					text: '忙碌',
+					color: '#FF647E'
+				}, {
+					text: '隐身',
+					color: '#FF9C4E'
+				}],
+				info: {
+					status: {
+						value: 0,
+						chinese: '加载中'
+					}
+				},
+				detail: [],
+				buttons: [],
+				userinfo: [],
+				modelShow: false,
+				modelContent: '',
+				commonTips: ''
+			}
+		},
+		onLoad:function(query){
+			this.id = query.id ? query.id:0
+			this.type = query.type ? query.type:'user'
+			this.userinfo = uni.getStorageSync('userinfo');// 防止需要在pageDataLoad使用到用户ID
+			this.ws.pageFun(this.pageDataLoad, this);
+		},
+		onShow() {
+			this.ws.checkNetwork(this)
+			if (this.ws.pageRefresh.centerInfo) {
+				this.ws.pageRefresh.centerInfo = false
+				this.ws.pageFun(this.pageDataLoad, this);
+			}
+		},
+		onPullDownRefresh: function () {
+			this.ws.pageFun(this.pageDataLoad(true), this);
+			this.ws.onMessageCallBack.set('info-detail', (msg) => {
+				if (msg.data.data.refresh) {
+					this.$refs.uToast.show({
+						title: '刷新成功~',
+						type: 'success'
+					})
+					uni.stopPullDownRefresh()
+				}
+			})
+		},
+		methods: {
+			pageDataLoad: function (refresh = false) {
+				var that = this
+				this.ws.send({
+					c: 'User',
+					a: 'infoDetail',
+					data: {
+						'method': 'get',
+						'type': that.type,
+						'id': that.id ? that.id:that.userinfo.id,
+						'requestor': that.userinfo.id,
+						'refresh': refresh
+					}
+				})
+			},
+			changeUserStatus: function (index) {
+				var that = this
+				that.ws.pageFun(function () {
+					that.ws.send({ c: 'ImBase', a: 'changeStatus', data: { 'status': (index + 1) } });
+				}, that);
+			},
+			userAction: function (action, data, type, opt) {
+				if (action == 'del-contact') {
+					switch (type) {
+						case 'user':
+							this.modelContent = '你确定要删除此好友吗?';
+							break;
+						case 'group':
+							this.modelContent = '你确定要退出群聊吗?';
+							break;
+						case 'dissolution-group':
+							this.modelContent = '你确定要解散群聊吗?';
+							type = 'group';
+							break;
+					}
+					this.modelShow = true
+					this.info.delType = type
+				} else if(action == 'open-session') {
+					let message = { c: 'Message', a: 'openSession', data: { 'id': data, 'type': type } }
+					this.ws.send(message);
+				} else if(action == 'add-friends') {
+					uni.navigateTo({
+						url: '/pages/search/add-contact?id=' + data + '&type=' + type
+					})
+				} else if(action == 'userinfo-opt') {
+					if (opt == 'edit') {
+						uni.navigateTo({
+							url: '/pages/center/edit-info?id=' + data + '&type=' + type
+						})
+					} else if(opt == 'join') {
+						uni.navigateTo({
+							url: '/pages/search/add-contact?id=' + data + '&type=' + type
+						})
+					}
+				} else if (action == 'close') {
+					uni.navigateBack({
+						delta: 1
+					})
+				}
+			},
+			delConfirm: function () {
+				var that = this
+				that.ws.pageFun(function(){
+					let message = {
+						c: 'User',
+						a: 'delContact',
+						data: {
+							'id': that.info.id,
+							'type': that.info.delType
+						}
+					}
+					that.ws.send(message);
+				}, that)
+			},
+			saveRemark: function (value) {
+				if (value != this.info.oldRemark) {
+					let message = {
+						c: 'User',
+						a: 'updateFriendInfo',
+						data: {
+							id: this.info.id,
+							new_remark: value,
+							method: 'update_remark'
+						}
+					}
+					this.ws.send(message);
+				}
+			},
+			collectionUser: function () {
+				var that = this
+				that.ws.pageFun(function(){
+					var new_group = 'common'
+					if (that.info.group == 'common') {
+						new_group = 'all_friends'
+					}
+					let message = {
+						c: 'User',
+						a: 'updateFriendInfo',
+						data: {
+							id: that.info.id,
+							new_group: new_group,
+							method: 'update_group',
+							source: 'uni-app'
+						}
+					}
+					that.ws.send(message);
+					that.info.group = new_group
+					that.ws.showMsgCallback = function () {
+						that.ws.pageRefresh.addressList = true
+					}
+				}, that)
+			},
+			clipboardId: function(id) {
+				var that = this
+				uni.setClipboardData({
+					data: id.toString(),
+					success: function () {
+						uni.hideToast()
+						that.$refs.uToast.show({
+							title: '账号已复制~',
+							type: 'success'
+						})
+					}
+				});
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+page {
+	background-color: $--bg-color;
+}
+.user-box {
+	display: flex;
+	padding: 20rpx 4vw;
+	align-items: center;
+}
+.user-avatar {
+	height: 170rpx;
+	width: 170rpx;
+}
+.collection-user {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+.user-right {
+	width: 70%;
+	padding-left: 20rpx;
+}
+.user-right-item {
+	display: block;
+	line-height: 32rpx;
+}
+.user-name {
+	line-height: 46rpx;
+	font-size: 30rpx;
+	overflow: hidden;
+	text-overflow:ellipsis;
+	white-space: nowrap;
+}
+.nickname-text {
+	display: inline-flex;
+	max-width: 46%;
+	overflow: hidden;
+	text-overflow:ellipsis;
+	white-space: nowrap;
+}
+.user-status {
+	color: $--gray;
+	line-height: 46rpx;
+}
+.user-info-box {
+	display: block;
+	width: 92vw;
+	margin: 0 auto;
+	padding-top: 10rpx;
+}
+.user-buttons {
+	padding: 40rpx 0;
+	display: flex;
+	align-items: center;
+	justify-content: space-around;
+}
+.leader-avatar-box {
+	display: flex;
+	align-items: center;
+}
+.leader-avatar {
+	height: 80rpx;
+	width: 80rpx;
+	margin-right: 10rpx;
+}
+.im-data-none {
+	display: block;
+	line-height: 100rpx;
+	text-align: center;
+	font-size: 28rpx;
+	color: $--gray;
+}
+</style>

+ 258 - 0
addons/fastim/uniapp/pages/center/login.vue

@@ -0,0 +1,258 @@
+<template>
+	<view>
+		<u-modal v-model="modelShow" :content="modelContent"></u-modal>
+		<common :tips='commonTips'></common>
+		<image class="login-bg" src="../../static/img/login-bg.png" mode="widthFix"></image>
+		<view class="login-menu-box">
+			<view class="login-menu">
+				<view @click="tab(0)" class="login-button" :class="tabIndex==0 ? 'login-button-active':''">账号登录</view>
+				<view @click="tab(1)" class="login-button" :class="tabIndex==1 ? 'login-button-active':''">手机登录</view>
+			</view>
+		</view>
+		<u-form :model="form" class="login-form" label-width="150" :label-style="{textAlign: 'justify', textAlignLast: 'justify', display: 'inline-block', paddingRight:'10px'}" ref="uForm">
+			<u-form-item v-if="tabIndex==0" label="账 号" prop="username">
+				<u-input v-model="form.username" placeholder="请输入账号" name="username" />
+			</u-form-item>
+			<u-form-item v-if="tabIndex==0" label="密 码" prop="password">
+				<u-input v-model="form.password" :password-icon="true" placeholder="请输入密码" type="password" name="password" />
+			</u-form-item>
+			<u-form-item v-if="tabIndex==0" label="验证码" prop="captcha">
+				<u-input v-model="form.captcha" placeholder="请输入验证码" type="number"></u-input>
+				<image @click="downloadCaptcha" class="captcha-img" slot="right" :src="captchaPath" mode="widthFix"></image>
+			</u-form-item>
+			
+			<u-form-item v-if="tabIndex==1" label="手机号" prop="mobile">
+				<u-input v-model="form.mobile" placeholder="请输入手机号" type="number" name="mobile" />
+			</u-form-item>
+			<u-form-item v-if="tabIndex==1" label="验证码" prop="code">
+				<u-input placeholder="请输入验证码" v-model="form.code" type="text"></u-input>
+				<u-button slot="right" type="primary" size="mini" @click="getCode">{{codeTips}}</u-button>
+			</u-form-item>
+			<u-button @click="submit" :custom-style="loginFormButtonStyle" :ripple="true" :disabled="loginFormButtonDisabled" ripple-bg-color="rgba(45,211,232, 0.8)" shape="circle">
+				<u-icon name="arrow-rightward"></u-icon>
+			</u-button>
+			<u-verification-code seconds="60" ref="uCode" @change="codeChange"></u-verification-code>
+		</u-form>
+		<view class="login-footer-box">
+			<view @click="goRegister" class="login-footer-box-left">注册新用户</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				tabIndex: 0,
+				form: {
+					mobile: '',
+					username: '',
+					password: '',
+					captcha: ''
+				},
+				rules: {
+					mobile: [
+						{
+							required: true, 
+							message: '请输入手机号',
+							trigger: ['change', 'blur']
+						},
+						{
+							validator: (rule, value, callback) => {
+								return this.$u.test.mobile(value);
+							},
+							message: '手机号码不正确',
+							trigger: ['blur'],
+						}
+					],
+					username: [
+						{
+							required: true, 
+							message: '请输入账号',
+							trigger: ['change', 'blur']
+						}
+					],
+					password: [
+						{
+							required: true,
+							min: 6, 
+							message: '请输入正确的密码', 
+							trigger: 'blur'
+						}
+					],
+					captcha: [
+						{
+							required: true,
+							min: 4,
+							message: '请输入正确的验证码',
+							trigger: ['change', 'blur']
+						}
+					]
+				},
+				loginFormButtonStyle: {
+					backgroundImage: "linear-gradient(to bottom right, #34E4E8, #1FAEE8)",
+					border: 'none',
+					color: '#ffffff',
+					outline: 'none',
+					marginTop: '80rpx',
+					height: '100rpx',
+					width: '100rpx'
+				},
+				loginFormButtonDisabled: false,
+				modelShow: false,
+				modelContent: '',
+				codeTips: '请输入验证码',
+				commonTips: '',
+				captchaPath: ''
+			}
+		},
+		onReady() {
+			this.downloadCaptcha()
+			this.$refs.uForm.setRules(this.rules);
+		},
+		methods: {
+			tab: function (index) {
+				this.tabIndex = index
+			},
+			downloadCaptcha: function() {
+				var that = this
+				that.form.captcha = ''
+				that.$u.post('/addons/fastim/api.user/captchaPre', {}).then(res => {
+					that.ws.captchaId = res.data.captcha_id;
+					that.captchaPath = this.ws.buildUrl('default') + '/addons/fastim/api.user/captcha?captcha_id=' + res.data.captcha_id
+				}).catch(res => {
+					that.$u.toast('验证码请求失败,请重试!');
+				})
+			},
+			getCode: function () {
+				var that = this
+				if (!that.form.mobile) {
+					that.$u.toast('请输入手机号');
+					return ;
+				}
+				if(that.$refs.uCode.canGetCode) {
+					uni.showLoading({
+						title: '正在获取验证码',
+						mask: true
+					});
+					that.$u.post('/api/sms/send', {
+						event: 'login',
+						mobile: that.form.mobile
+					}).then(res => {
+						if (res.code == 1) {
+							uni.hideLoading();
+							that.$u.toast('验证码已发送');
+							that.$refs.uCode.start();
+						} else {
+							that.modelContent = res.msg
+							that.modelShow = true
+						}
+					}).catch(res => {
+						that.$u.toast('请求失败,请重试!');
+					})
+				} else {
+					that.$u.toast('倒计时结束后再发送');
+				}
+			},
+			codeChange(text) {
+				this.codeTips = text;
+			},
+			submit: function() {
+				var that = this
+				that.$refs.uForm.validate(valid => {
+					if (valid) {
+						that.loginFormButtonDisabled = true
+						that.form.tab = that.tabIndex
+						that.form.captcha_id = that.ws.captchaId
+						that.form.account = (that.tabIndex == 0) ? that.form.username:that.form.mobile;
+						that.$u.post('/addons/fastim/api.user/login', that.form).then(res => {
+							that.loginFormButtonDisabled = false
+							if (res.code == 1) {
+								uni.setStorageSync('userinfo', res.data.userinfo);
+								that.ws.init(res.data.userinfo.token, res.data.userinfo.auth_token)
+								uni.reLaunch({
+									url: '/pages/message/message'
+								})
+							} else {
+								that.downloadCaptcha()
+								that.modelContent = res.msg
+								that.modelShow = true
+							}
+						}).catch(res => {
+							that.downloadCaptcha()
+							that.loginFormButtonDisabled = false
+							that.$u.toast('请求失败,请重试!');
+						})
+					}
+				});
+			},
+			goRegister: function() {
+				uni.navigateTo({
+					url: '/pages/center/register'
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+page {
+	background-color: $--bg-color;
+}
+.login-bg {
+	width: 100vw;
+}
+.login-menu-box {
+	position: relative;
+}
+.login-menu {
+	display: flex;
+	width: 80vw;
+	margin: 0 auto;
+	align-items: center;
+	background-color: $--white;
+	border-radius: 50rpx;
+	position: absolute;
+	top: -40rpx;
+	left: 10vw;
+	box-shadow: 0 0 10px rgba(0, 0, 0, .2);
+}
+.login-button {
+	height: 80rpx;
+	width: 50%;
+	font-size: 30rpx;
+	font-weight: bold;
+	text-align: center;
+	line-height: 80rpx;
+	color: $--gray;
+	border-radius: 50rpx;
+	background: $--white;
+}
+.login-button-active {
+	color: $--white;
+	background-image: linear-gradient(to bottom right, #34E4E8, #1FAEE8);
+}
+.login-form {
+	display: block;
+	width: 78vw;
+	padding-top: 140rpx;
+	margin: 0 auto;
+}
+.login-footer-box {
+	position: fixed;
+	bottom: 20rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	width: 100vw;
+}
+.login-footer-box-left {
+	text-decoration: underline;
+	font-size: 28rpx;
+	color: $--blue;
+}
+.captcha-img {
+	width: 200rpx;
+	max-height: 100rpx;
+}
+</style>

+ 181 - 0
addons/fastim/uniapp/pages/center/post_group_chat_notice.vue

@@ -0,0 +1,181 @@
+<template>
+	<view>
+		<u-toast ref="uToast" />
+		<common :tips='commonTips'></common>
+		<u-form class="post-form">
+			<view class="form-item">
+				<u-input maxlength="-1" v-model="form.content" name="content" class="post-textarea" placeholder="请输入公告内容" type="textarea" />
+				<view>
+					<u-upload @on-change="imagesOnChange" :form-data="uploadFormData" :file-list="filesArr" :action="action" ref="uUpload" :size-type="['compressed']" :max-count="10" name="file" width="160" height="160"></u-upload>
+				</view>
+				<view class="checkbox-group">
+					<u-checkbox-group name="checkboxs">
+						<u-checkbox v-model="item.checked" v-for="(item, cindex) in checkboxs" :key="cindex" :name="item.title">
+							{{ item.title }}
+						</u-checkbox>
+					</u-checkbox-group>
+				</view>
+			</view>
+			<u-button :loading="submitButtonStatus" :disabled="submitButtonStatus" class="submit-button" type="primary" @click="submit">发布</u-button>
+		</u-form>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				id: 0,
+				group_id: 0,
+				form: {
+					content: ''
+				},
+				action: '',
+				filesArr: [],
+				checkboxs: [{
+					name: 'popup',
+					title: '使用弹窗展示公告',
+					checked: true
+				},
+				{
+					name: 'receipt',
+					title: '需群成员确认收到',
+					checked: false
+				},
+				{
+					name: 'top',
+					title: '置顶公告',
+					checked: false
+				}],
+				submitButtonStatus: false,
+				uploadFormData: new Object(),
+				commonTips: ''
+			}
+		},
+		onLoad(query) {
+			var that = this
+			that.id = query.id
+			that.group_id = query.group_id
+			let userinfo = uni.getStorageSync('userinfo');
+			that.action = that.ws.buildUrl('upload', userinfo.token)
+			that.ws.pageFun(that.pageDataLoad, that);
+		},
+		onShow() {
+			this.ws.checkNetwork(this)
+		},
+		methods: {
+			pageDataLoad: function() {
+				var that = this
+				that.ws.send({
+					c: 'ImBase',
+					a: 'getUploadMultipart'
+				});
+				if (that.id) {
+					that.ws.send({
+						c: 'Message',
+						a: 'groupChatNoticeOpt',
+						data: {
+							id: that.id,
+							type: "edit"
+						}
+					});
+					uni.setNavigationBarTitle({
+					    title: '编辑公告'
+					});
+				}
+			},
+			imagesOnChange: function (res) {
+				res = JSON.parse(res.data);
+				if (res.code != 1) {
+					this.ws.pageFun(() => {
+						this.ws.send({
+							c: 'ImBase',
+							a: 'getUploadMultipart'
+						})
+					}, this)
+					
+					this.$refs.uUpload.remove(index);
+					
+					uni.showModal({
+						title: '温馨提示',
+						content: res.msg,
+						showCancel: false
+					})
+				}
+			},
+			submit: function () {
+				var that = this
+				that.submitButtonStatus = true
+				var values = that.form, noticeimages = '', message = '';
+				
+				for (var i = 0; i < that.$refs.uUpload.lists.length; i++) {
+					if (that.$refs.uUpload.lists[i].progress == 100) {
+						if (that.$refs.uUpload.lists[i].response) {
+							noticeimages += that.$refs.uUpload.lists[i].response.data.fullurl + ','
+						} else if (that.$refs.uUpload.lists[i].url) {
+							noticeimages += that.$refs.uUpload.lists[i].url + ','
+						}
+					}
+				}
+				
+				for (var i = 0; i < that.checkboxs.length; i++) {
+					values[that.checkboxs[i].name] = that.checkboxs[i].checked ? 1:0
+				}
+				
+				values.group_id = that.group_id
+				values.images = noticeimages
+				values.method = 'post'
+				
+				if (that.id) {
+					values.id = that.id
+					values.type = 'post-edit'
+					values.source = 'uni-app'
+					message = {c: 'Message', a: 'groupChatNoticeOpt', data: values}
+				} else {
+					message = {c: 'Message', a: 'groupChatNotice', data: values}
+				}
+				
+				that.ws.pageFun(function() {
+					that.ws.send(message);
+					that.ws.showMsgCallback = function() {
+						setTimeout(function() {
+							that.ws.pageRefresh.chatSetting = true
+							that.submitButtonStatus = false
+							uni.navigateBack({
+								delta: 1
+							})
+						}, 2000)
+					}
+				}, that);
+			}
+		}
+	}
+</script>
+
+<style>
+page {
+	background: #F8F8F8;
+}
+.post-form {
+	display: block;
+	width: 96vw;
+	margin: 20rpx auto;
+}
+.form-item {
+	display: block;
+	padding: 20rpx;
+	border-radius: 12rpx;
+	background: #FFFFFF;
+}
+.post-textarea,.checkbox-group {
+	display: block;
+	width: 96%;
+	margin: 0 auto;
+	padding-top: 20rpx;
+}
+.submit-button {
+	width: 60vw;
+	display: block;
+	margin: 60rpx auto;
+}
+</style>

+ 191 - 0
addons/fastim/uniapp/pages/center/privacy-settings.vue

@@ -0,0 +1,191 @@
+<template>
+	<view>
+		<u-toast ref="uToast" />
+		<common :tips='commonTips'></common>
+		<view v-if="loadingShow" class="u-loading">
+			<u-loading color="#6388fb" mode="circle"></u-loading>
+		</view>
+		<u-form ref="uForm">
+			<block v-for="(item, index) in form" :key="index">
+				<view class="form-item-box">
+					<u-form-item v-if="item.type == 'select'" label-position="top" :label="item.title">
+						<u-select :default-value="item.value" v-model="item.show" mode="single-column" :list="item.data" @confirm="confirmSelect"></u-select>
+						<view @click="openSelect(index)">{{item.data[item.value[0]].label}}</view>
+					</u-form-item>
+					
+					<u-form-item v-if="item.type == 'text' || item.type == 'textarea'" label-position="top" :label="item.title">
+						<u-input :type="item.type" v-model="item.value" :placeholder="'请输入' + item.title" />
+					</u-form-item>
+					
+					<u-form-item :border-bottom="item.name == 'input_status' ? false:true" v-if="item.type == 'radio'" label-position="top" :label="item.title">
+						<u-radio-group v-model="item.value">
+							<u-radio v-for="(item, indexr) in item.content" :key="indexr" :name="indexr">
+								{{ item }}
+							</u-radio>
+						</u-radio-group>
+					</u-form-item>
+					
+					<u-form-item v-if="item.type == 'checkbox'" label-position="top" :label="item.title">
+						<u-checkbox-group>
+							<u-checkbox v-model="item.checked" v-for="(item, cindex) in item.contents" :key="cindex" :name="item.name">
+								{{ item.name }}
+							</u-checkbox>
+						</u-checkbox-group>
+					</u-form-item>
+					
+					<view v-if="item.name == 'input_status'" class="box20rpx"></view>
+				</view>
+			</block>
+			<u-cell-group :border="false" v-if="form.length">
+				<navigator url="/pages/center/user-shield">
+					<u-cell-item :border-bottom="false" title="黑名单管理"></u-cell-item>
+				</navigator>
+			</u-cell-group>
+			<u-button v-if="form.length" :loading="submitButtonStatus" :disabled="submitButtonStatus" class="submit-button" type="primary" @click="submit">保存</u-button>
+		</u-form>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				userinfo: [],
+				form: {
+					
+				},
+				openSelectName: false,
+				submitButtonStatus: false,
+				loadingShow: true,
+				commonTips: ''
+			}
+		},
+		onLoad:function(){
+			this.userinfo = uni.getStorageSync('userinfo');
+			this.pageDataLoad()
+		},
+		onShow() {
+			this.ws.checkNetwork(this)
+		},
+		methods: {
+			submit: function () {
+				var  that = this
+				that.submitButtonStatus = true
+				var values = {};
+				for (var i = 0; i < that.form.length; i++) {
+					if (that.form[i].type == 'select') {
+						values[that.form[i].name] = that.form[i].data[that.form[i].value[0]].value
+					} else if (that.form[i].type == 'checkbox') {
+						var checkboxStr = '';
+						for (var y = 0; y < that.form[i].contents.length; y++) {
+							checkboxStr += that.form[i].contents[y].checked ? (that.form[i].contents[y].id + ','):''
+						}
+						values[that.form[i].name] = checkboxStr
+					} else {
+						values[that.form[i].name] = that.form[i].value
+					}
+				}
+				
+				that.ws.pageFun(function() {
+					var message = { c: 'ImBase', a: 'updateUserConfig', data: values }
+					that.ws.send(message);
+					that.ws.showMsgCallback = function () {
+						setTimeout(function(){
+							that.submitButtonStatus = false
+							uni.navigateBack({
+								delta: 1
+							})
+						}, 2000)
+					}
+				}, that)
+			},
+			openSelect: function(index, name = false){
+				this.form[index].show = true
+				this.openSelectName = name ? name:false
+			},
+			confirmSelect: function (value) {
+				var valueIndex = 0, data = this.form[value[0].extra].data;
+				for (var i = 0; i < data.length; i++) {
+					if (data[i].value == value[0].value) {
+						valueIndex = i
+					}
+				}
+				this.form[value[0].extra].value = [valueIndex]
+			},
+			pageDataLoad: function () {
+				var that = this
+				that.$u.get(that.ws.buildUrl('get-config-item-data', that.userinfo.token), {}).then(res => {
+					
+					that.loadingShow = false
+					var general = res.data.imConfigGroup.privacy.list;
+					
+					for (let g = 0; g < general.length; g++) {
+						
+						if (general[g].name == 'send_message_key' || general[g].name == 'ecs_exit') {
+							general[g].type = 'hidden'
+						}
+						
+						if (general[g].type == 'select') {
+							general[g].show = false
+							general[g].data = []
+							
+							for (let i = 0; i < general[g].content.length; i++) {
+								general[g].data.push({ value: i, label: general[g].content[i], extra: g });
+								if (parseInt(general[g].value[0]) == i) {
+									general[g].value = [i]
+								}
+							}
+						} else if(general[g].type == 'text') {
+							general[g].type = 'textarea'
+						} else if(general[g].type == 'string') {
+							general[g].type = 'text'
+						} else if(general[g].type == 'checkbox') {
+							var contents = []
+							for (var i = 0; i < general[g].content.length; i++) {
+								contents.push({
+									id: i,
+									name: general[g].content[i],
+									checked: general[g].value.indexOf(i.toString()) === -1 ? false:true
+								})
+							}
+							general[g].contents = contents
+						}
+					}
+					
+					that.form = general
+				})
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+page{
+	background-color: $--bg-color;
+}
+.form-item-box {
+	width: 100vw;
+	background-color: $--white;
+	padding: 0 5vw;
+}
+.form-item {
+	border-bottom: 1px solid red;
+}
+.box20rpx {
+	height: 20rpx;
+	width: 100vw;
+	background-color: $--bg-color;
+	margin-left: -5vw;
+}
+.submit-button {
+	width: 60vw;
+	display: block;
+	margin: 40rpx auto;
+}
+.u-loading {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	margin-top: 60rpx;
+}
+</style>

+ 139 - 0
addons/fastim/uniapp/pages/center/quick-reply.vue

@@ -0,0 +1,139 @@
+<template>
+	<view>
+		<u-toast ref="uToast" />
+		<common :tips='commonTips'></common>
+		<u-action-sheet :list="actionList" @click="clickSwipeAction" v-model="actionShow"></u-action-sheet>
+		<u-modal v-model="modelShow" :mask-close-able="true" :show-cancel-button="true" @confirm="delConfirm" :content="modelContent"></u-modal>
+		<view
+		:index="index"
+		v-for="(item, index) in quickReply"
+		:key="item.id"
+		class="quick-reply-llst">
+			<view @longpress="openSwipeAction(index)" class="quick-reply-item">
+				<view class="quick-reply-wrap">
+					<view class="u-line-2">{{ item.title }}</view>
+					<view class="quick-reply-footer" :class="item.status == 1 ? 'quick-reply-footer-1':''">{{item.status == 1 ? '已启用':'已关闭'}}</view>
+				</view>
+			</view>
+		</view>
+		<view v-if="!quickReply.length" class="im-data-none">没有更多了~</view>
+		<view class="footer-box"></view>
+		<div class="quick-reply-button">
+			<u-button @click="goAddQuickReply" type="primary">添加快捷回复</u-button>
+		</div>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				quickReply: [],
+				actionShow: false,
+				actionIndex: 0,
+				actionList: [{
+					text: '编辑',
+					color: '#007aff'
+				},
+				{
+					text: '删除',
+					color: '#f74c31'
+				}],
+				modelShow: false,
+				modelContent: '',
+				commonTips: ''
+			}
+		},
+		onShow: function(){
+			this.ws.checkNetwork(this)
+			this.ws.pageFun(this.pageDataLoad, this);
+		},
+		onReachBottom: function () {
+			if (this.loadFastReply) {
+				this.loadFastReply.page++;
+				let message = { c: 'ImBase', a: 'fastReply', data: this.loadFastReply }
+				this.ws.send(message);
+				this.loadFastReply = false
+			}
+		},
+		methods: {
+			pageDataLoad: function () {
+				let message = {
+					c: 'ImBase',
+					a: 'fastReply',
+					data: {
+						method: 'get',
+						page: 1,
+						form: 'uni-app'
+					}
+				}
+				this.ws.send(message);
+			},
+			openSwipeAction (index) {
+				this.actionShow = true
+				this.actionIndex = index
+			},
+			delConfirm: function () {
+				var that = this, item = that.quickReply[that.actionIndex]
+				that.ws.pageFun(function() {
+					that.ws.send({
+						c: 'ImBase',
+						a: 'fastReply',
+						data: {
+							form: 'uni-app',
+							method: 'del',
+							id: item.id
+						}
+					});
+					that.actionIndex = 0;
+				}, that)
+			},
+			clickSwipeAction (index) {
+				var that = this, item = this.quickReply[this.actionIndex]
+				if(index == 1) {
+					that.modelShow = true
+					that.modelContent = '确定删除快捷回复?'
+				} else {
+					uni.navigateTo({
+						url: '/pages/center/edit-quick-reply?type=edit&id=' + item.id
+					})
+					that.actionIndex = 0
+				}
+				that.actionShow = false
+			},
+			goAddQuickReply: function() {
+				uni.navigateTo({
+					url: '/pages/center/edit-quick-reply?type=add'
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+.quick-reply-item {
+	font-size: 30rpx;
+	display: flex;
+	align-items: center;
+	padding: 30rpx;
+	border-bottom: 1px solid #F2F3F4;
+}
+.quick-reply-footer {
+	font-size: 28rpx;
+	color: #999999;
+	margin-top: 20rpx;
+}
+.quick-reply-footer-1 {
+	color: #21E589;
+}
+.footer-box {
+	height: 160rpx;
+	display: block;
+}
+.quick-reply-button {
+	position: fixed;
+	bottom: 40rpx;
+	width: 60vw;
+	left: 20vw;
+}
+</style>

+ 209 - 0
addons/fastim/uniapp/pages/center/register.vue

@@ -0,0 +1,209 @@
+<template>
+	<view>
+		<common :tips='commonTips'></common>
+		<u-modal v-model="modelShow" :content="modelContent"></u-modal>
+		<image class="login-bg" src="../../static/img/login-bg.png" mode="widthFix"></image>
+		<view class="register-box">
+			<u-form :model="form" class="login-form" label-width="150" :label-style="{textAlign: 'justify', textAlignLast: 'justify', display: 'inline-block', paddingRight:'10px'}" ref="uForm">
+				<u-form-item label="账  号" prop="username">
+					<u-input v-model="form.username" placeholder="请输入登录名" name="username" />
+				</u-form-item>
+				<u-form-item label="密  码" prop="password">
+					<u-input v-model="form.password" :password-icon="true" placeholder="请输入密码" type="password" name="password" />
+				</u-form-item>
+				<u-form-item label="确认密码" prop="rePassword">
+					<u-input v-model="form.rePassword" :password-icon="true" placeholder="请输入确认密码" type="password" name="rePassword" />
+				</u-form-item>
+				<u-form-item label="手机号码" prop="mobile">
+					<u-input v-model="form.mobile" placeholder="请输入手机号" type="number" name="mobile" />
+				</u-form-item>
+				<u-form-item label="验证码" prop="captcha">
+					<u-input placeholder="请输入验证码" v-model="form.captcha" type="text"></u-input>
+					<u-button slot="right" type="primary" size="mini" @click="getCode">{{codeTips}}</u-button>
+				</u-form-item>
+				<u-button :custom-style="registerFormButtonStyle" :ripple="true" :disabled="registerFormButtonDisabled" ripple-bg-color="rgba(45,211,232, 0.8)" shape="circle" @click="submit">注册</u-button>
+				<u-verification-code seconds="60" ref="uCode" @change="codeChange"></u-verification-code>
+			</u-form>
+			
+			<view class="login-footer-box">
+				<view @click="goLogin" class="login-footer-box-left">返回登录</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				form: {
+					username: '',
+					mobile: '',
+					password: '',
+					rePassword: '',
+				},
+				rules: {
+					mobile: [
+						{
+							required: true, 
+							message: '请输入手机号',
+							trigger: ['change', 'blur']
+						},
+						{
+							validator: (rule, value, callback) => {
+								return this.$u.test.mobile(value);
+							},
+							message: '手机号码不正确',
+							trigger: ['blur'],
+						}
+					],
+					username: [
+						{
+							required: true, 
+							message: '请输入账号',
+							trigger: ['change', 'blur']
+						}
+					],
+					password: [
+						{
+							required: true,
+							min: 6, 
+							message: '请输入正确的密码', 
+							trigger: 'blur'
+						}
+					],
+					rePassword: [
+						{
+							required: true,
+							message: '请重新输入密码',
+							trigger: ['change','blur'],
+						},
+						{
+							validator: (rule, value, callback) => {
+								return value === this.form.password;
+							},
+							message: '两次输入的密码不相等',
+							trigger: ['change','blur'],
+						}
+					]
+				},
+				registerFormButtonStyle: {
+					backgroundImage: "linear-gradient(to bottom right, #34E4E8, #1FAEE8)",
+					border: 'none',
+					color: '#ffffff',
+					outline: 'none',
+					marginTop: '80rpx',
+					height: '90rpx'
+				},
+				registerFormButtonDisabled: false,
+				modelShow: false,
+				modelContent: '',
+				codeTips: '获取验证码',
+				commonTips: ''
+			}
+		},
+		methods: {
+			getCode: function () {
+				var that = this
+				if (!that.form.mobile) {
+					that.$u.toast('请输入手机号');
+					return ;
+				}
+				if(that.$refs.uCode.canGetCode) {
+					uni.showLoading({
+						title: '正在获取验证码',
+						mask: true
+					});
+					that.$u.post('/api/sms/send', {
+						event: 'register',
+						mobile: that.form.mobile
+					}).then(res => {
+						if (res.code == 1) {
+							uni.hideLoading();
+							that.$u.toast('验证码已发送');
+							that.$refs.uCode.start();
+						} else {
+							that.modelContent = res.msg
+							that.modelShow = true
+						}
+					}).catch(res => {
+						that.$u.toast('请求失败,请重试!');
+					})
+				} else {
+					that.$u.toast('倒计时结束后再发送');
+				}
+			},
+			codeChange(text) {
+				this.codeTips = text;
+			},
+			goLogin: function() {
+				uni.redirectTo({
+					url: '/pages/center/login'
+				})
+			},
+			submit: function(e) {
+				var that = this
+				that.$refs.uForm.validate(valid => {
+					if (valid) {
+						that.registerFormButtonDisabled = true
+						that.$u.post('/addons/fastim/api.user/register', that.form).then(res => {
+							that.registerFormButtonDisabled = false
+							if (res.code == 1) {
+								uni.setStorageSync('userinfo', res.data.userinfo);
+								that.ws.init(res.data.userinfo.token, res.data.userinfo.auth_token)
+								uni.switchTab({
+									url: '/pages/message/message'
+								})
+							} else {
+								that.modelContent = res.msg
+								that.modelShow = true
+							}
+						}).catch(res => {
+							that.registerFormButtonDisabled = false
+							that.$u.toast('请求失败,请重试!');
+						})
+					}
+				});
+			},
+			onReady() {
+				this.$refs.uForm.setRules(this.rules);
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+page {
+	background-color: $--bg-color;
+}
+.login-bg {
+	width: 100vw;
+}
+.login-form {
+	display: block;
+	width: 78vw;
+	padding-top: 40rpx;
+	margin: 0 auto;
+}
+.login-footer-box {
+	display: flex;
+	padding-top: 30rpx;
+	width: 78vw;
+	margin: 0 auto;
+}
+.login-footer-box-left {
+	text-decoration: underline;
+	font-size: 28rpx;
+	color: $--blue;
+}
+.register-box {
+	background-color: $--white;
+	position: relative;
+	top: -200rpx;
+	left: 5vw;
+	width: 90vw;
+	border-radius: 16rpx;
+	padding-bottom: 60rpx;
+	box-shadow: 0 0 10px rgba(0, 0, 0, .2);
+}
+</style>

+ 182 - 0
addons/fastim/uniapp/pages/center/report.vue

@@ -0,0 +1,182 @@
+<template>
+	<view>
+		<!-- title=反馈&举报&id=224&type=user -->
+		<!-- 举报时:id为会话ID,type当举报时可固定为user -->
+		<!-- 反馈时:仅需传递title=反馈 -->
+		<common :tips='commonTips'></common>
+		<u-toast ref="uToast" />
+		<u-form class="quick-reply" :model="form" ref="uForm">
+			<u-form-item :label="formTitle + '详情'" label-position="top" prop="describe">
+				<u-input type="textarea" :placeholder="describePlaceholder" v-model="form.describe" />
+			</u-form-item>
+			<u-form-item label="联系方式" label-position="top" prop="mobile">
+				<u-input placeholder="请输入联系方式" v-model="form.mobile" />
+			</u-form-item>
+			<u-form-item label="图片证据" label-position="top">
+				<view>
+					<u-upload @on-change="avatarOnChange" :form-data="uploadFormData" ref="uUpload" :size-type="['compressed']" name="file" :max-count="4" :show-tips="false" :action="uploadAction" :file-list="fileList" ></u-upload>
+				</view>
+			</u-form-item>
+			<u-button class="quick-reply-button" :loading="submitButtonStatus" :disabled="submitButtonStatus" type="primary" @click="submit">提交</u-button>
+		</u-form>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				id: 0,
+				type: 'user',
+				userToken: '',
+				form: {
+					describe: '',
+					mobile: '',
+					reportimage: ''
+				},
+				formTitle: '反馈',
+				uploadAction: '',
+				uploadFormData: new Object(),
+				fileList: [],
+				rules: {
+					describe: [{
+						required: true,
+						message: '请输入详情',
+						trigger: ['change', 'blur']
+					}],
+					mobile: [{
+						required: true,
+						message: '请输入联系方式',
+						trigger: ['change', 'blur']
+					}]
+				},
+				describePlaceholder: '',
+				submitButtonStatus: false,
+				commonTips: ''
+			}
+		},
+		onLoad(data) {
+			this.id = data.id ? data.id:0
+			this.type = data.type ? data.type:'feedback'
+			let userinfo = uni.getStorageSync('userinfo');
+			this.userToken = userinfo.token
+			
+			this.formTitle = data.title ? data.title:this.formTitle
+			this.describePlaceholder = '请输入' + this.formTitle + '详情'
+			uni.setNavigationBarTitle({
+			    title: this.formTitle
+			});
+			
+			this.ws.pageFun(this.pageDataLoad, this)
+		},
+		onShow() {
+			this.ws.checkNetwork(this)
+		},
+		onReady() {
+			this.$refs.uForm.setRules(this.rules);
+		},
+		methods: {
+			pageDataLoad: function () {
+				var that = this				
+				if (that.id) {
+					let message = {
+						c: 'ImBase',
+						a: 'report',
+						data: {
+							session_id: that.id,
+							type: 'user',
+							method: 'get'
+						}
+					}
+					this.ws.send(message);
+				}
+				
+				that.ws.send({
+					c: 'ImBase',
+					a: 'getUploadMultipart'
+				});
+				
+				that.uploadAction = this.ws.buildUrl('upload', that.userToken)
+			},
+			avatarOnChange: function (res, index, lists) {
+				res = JSON.parse(res.data);
+				if (res.code == 0) {
+					
+					this.ws.pageFun(() => {
+						this.ws.send({
+							c: 'ImBase',
+							a: 'getUploadMultipart'
+						});
+					}, this)
+					
+					uni.showModal({
+						title: '温馨提示',
+						content: res.msg,
+						showCancel: false
+					})
+					
+					this.$refs.uUpload.remove(index);
+					return false;
+				}
+			},
+			submit: function () {
+				var that = this
+				this.$refs.uForm.validate(valid => {
+					if (valid) {
+						var files = '';
+						for (let f in that.$refs.uUpload.lists) {
+							if (that.$refs.uUpload.lists[f].progress == 100) {
+								files += that.ws.imgUrl(that.$refs.uUpload.lists[f].response.data.fullurl) + ','
+							}
+						}
+						if (!files) {
+							that.$refs.uToast.show({
+								title: '请上传证据图片~',
+								type: 'error'
+							})
+							return;
+						}
+						that.submitButtonStatus = true
+						that.form.type = that.type
+						that.form.reportimage = files
+						that.form.session_id = that.id
+						that.form.method = 'post'
+						
+						that.ws.pageFun(function() {
+							let message = { c: 'ImBase', a: 'report', data: that.form }
+							that.ws.send(message);
+							that.ws.showMsgCallback = function () {
+								that.form.describe = ''
+								that.form.mobile = ''
+								that.$refs.uUpload.clear();
+								setTimeout(function(){
+									that.submitButtonStatus = false
+									uni.navigateBack({
+										delta: 1
+									})
+								}, 2000)
+							}
+						}, that)
+					}
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+page {
+	background: #FFFFFF;
+}
+.quick-reply {
+	display: block;
+	width: 92vw;
+	margin: 0 auto;
+}
+.quick-reply-button {
+	width: 60vw;
+	display: block;
+	margin: 0 auto;
+	margin-top: 200rpx;
+}
+</style>

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä