Kirin 2 bulan lalu
induk
melakukan
af028ebd40
100 mengubah file dengan 12381 tambahan dan 0 penghapusan
  1. 1 0
      .example.env
  2. 6 0
      .gitignore
  3. 42 0
      .travis.yml
  4. 32 0
      LICENSE.txt
  5. 1 0
      app/.htaccess
  6. 56 0
      app/AppService.php
  7. 92 0
      app/ExceptionHandle.php
  8. 260 0
      app/Request.php
  9. 81 0
      app/command/Test.php
  10. 792 0
      app/common.php
  11. 158 0
      app/common/AdminBaseController.php
  12. 48 0
      app/common/ApiBaseController.php
  13. 93 0
      app/controller/admin/Common.php
  14. 135 0
      app/controller/admin/Login.php
  15. 53 0
      app/controller/admin/finance/Finance.php
  16. 45 0
      app/controller/admin/system/Qrcode.php
  17. 29 0
      app/controller/admin/system/SmsRecord.php
  18. 115 0
      app/controller/admin/system/SystemAdmins.php
  19. 159 0
      app/controller/admin/system/SystemClearData.php
  20. 36 0
      app/controller/admin/system/SystemLogs.php
  21. 172 0
      app/controller/admin/system/SystemMenus.php
  22. 102 0
      app/controller/admin/system/SystemRoles.php
  23. 176 0
      app/controller/admin/system/attachment/SystemAttachment.php
  24. 113 0
      app/controller/admin/system/attachment/SystemAttachmentCategory.php
  25. 288 0
      app/controller/admin/system/config/SystemConfig.php
  26. 113 0
      app/controller/admin/system/config/SystemConfigTab.php
  27. 93 0
      app/controller/admin/system/config/SystemUserLevel.php
  28. 405 0
      app/controller/admin/user/User.php
  29. 43 0
      app/controller/admin/user/UserGroup.php
  30. 318 0
      app/controller/api/Login.php
  31. 64 0
      app/controller/api/Pay.php
  32. 102 0
      app/controller/api/Pub.php
  33. 15 0
      app/controller/api/user/User.php
  34. 83 0
      app/controller/api/user/UserBill.php
  35. 225 0
      app/controller/api/wechat/Routine.php
  36. 177 0
      app/controller/api/wechat/Wechat.php
  37. 37 0
      app/event.php
  38. 54 0
      app/http/middleware/AllowOriginMiddleware.php
  39. 64 0
      app/http/middleware/EncryptDecryptMiddleware.php
  40. 32 0
      app/http/middleware/StationOpenMiddleware.php
  41. 47 0
      app/http/middleware/admin/AdminAuthTokenMiddleware.php
  42. 41 0
      app/http/middleware/admin/AdminCheckRoleMiddleware.php
  43. 42 0
      app/http/middleware/admin/AdminLogMiddleware.php
  44. 56 0
      app/http/middleware/api/AuthTokenMiddleware.php
  45. 68 0
      app/http/middleware/api/BlockerMiddleware.php
  46. 110 0
      app/http/middleware/api/RateLimiterMiddleware.php
  47. 47 0
      app/http/middleware/api/TradeMiddleware.php
  48. 55 0
      app/jobs/system/AdminLogJob.php
  49. 58 0
      app/jobs/system/ExportExcelJob.php
  50. 30 0
      app/listener/system/config/CreateSuccess.php
  51. 30 0
      app/listener/system/config/DeleteSuccess.php
  52. 29 0
      app/listener/system/config/StatusSuccess.php
  53. 29 0
      app/listener/user/CancelUser.php
  54. 117 0
      app/listener/wechat/OffcialAccountListener.php
  55. 28 0
      app/listener/wechat/OpenPlatformListener.php
  56. 94 0
      app/listener/wechat/RoutineListener.php
  57. 242 0
      app/listener/wechat/WorkListener.php
  58. 10 0
      app/middleware.php
  59. 90 0
      app/model/system/CityArea.php
  60. 121 0
      app/model/system/Qrcode.php
  61. 88 0
      app/model/system/SmsRecord.php
  62. 98 0
      app/model/system/SystemLog.php
  63. 158 0
      app/model/system/admin/SystemAdmin.php
  64. 247 0
      app/model/system/admin/SystemMenus.php
  65. 146 0
      app/model/system/admin/SystemRole.php
  66. 120 0
      app/model/system/attachment/SystemAttachment.php
  67. 110 0
      app/model/system/attachment/SystemAttachmentCategory.php
  68. 109 0
      app/model/system/config/SystemConfig.php
  69. 88 0
      app/model/system/config/SystemConfigTab.php
  70. 67 0
      app/model/system/config/SystemStorage.php
  71. 71 0
      app/model/system/config/SystemStoreConfig.php
  72. 83 0
      app/model/system/config/SystemUserLevel.php
  73. 287 0
      app/model/user/User.php
  74. 68 0
      app/model/user/UserAddress.php
  75. 68 0
      app/model/user/UserBank.php
  76. 244 0
      app/model/user/UserBill.php
  77. 177 0
      app/model/user/UserBrokerage.php
  78. 37 0
      app/model/user/UserGroup.php
  79. 107 0
      app/model/user/UserLevel.php
  80. 169 0
      app/model/user/UserMoney.php
  81. 96 0
      app/model/user/UserSpread.php
  82. 45 0
      app/model/wechat/WechatMessage.php
  83. 124 0
      app/model/wechat/WechatUser.php
  84. 9 0
      app/provider.php
  85. 9 0
      app/service.php
  86. 188 0
      app/services/system/CityAreaServices.php
  87. 232 0
      app/services/system/QrcodeServices.php
  88. 68 0
      app/services/system/SmsRecordServices.php
  89. 111 0
      app/services/system/SystemClearServices.php
  90. 100 0
      app/services/system/SystemLogServices.php
  91. 387 0
      app/services/system/admin/SystemAdminServices.php
  92. 237 0
      app/services/system/admin/SystemMenusServices.php
  93. 192 0
      app/services/system/admin/SystemRoleServices.php
  94. 144 0
      app/services/system/attachment/SystemAttachmentCategoryServices.php
  95. 488 0
      app/services/system/attachment/SystemAttachmentServices.php
  96. 359 0
      app/services/system/config/SystemConfigServices.php
  97. 105 0
      app/services/system/config/SystemConfigTabServices.php
  98. 417 0
      app/services/system/config/SystemStorageServices.php
  99. 139 0
      app/services/system/config/SystemStoreConfigServices.php
  100. 135 0
      app/services/system/config/SystemUserLevelServices.php

+ 1 - 0
.example.env

@@ -0,0 +1 @@
+APP_DEBUG = true

[APP]
DEFAULT_TIMEZONE = Asia/Shanghai

[DATABASE]
TYPE = mysql
HOSTNAME = 127.0.0.1
DATABASE = test
USERNAME = username
PASSWORD = password
HOSTPORT = 3306
CHARSET = utf8
DEBUG = true

[LANG]
default_lang = zh-cn

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+/.idea
+/.vscode
+*.log
+.env
+/runtime
+/public/uploads

+ 42 - 0
.travis.yml

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

+ 32 - 0
LICENSE.txt

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

+ 1 - 0
app/.htaccess

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

+ 56 - 0
app/AppService.php

@@ -0,0 +1,56 @@
+<?php
+declare (strict_types=1);
+
+namespace app;
+
+use app\listener\wechat\OffcialAccountListener;
+use app\listener\wechat\OpenPlatformListener;
+use app\listener\wechat\RoutineListener;
+use app\services\system\config\SystemConfigServices;
+use qiniu\services\wechat\config\HttpCommonConfig;
+use qiniu\services\wechat\MiniProgram;
+use qiniu\services\wechat\OfficialAccount;
+use qiniu\services\wechat\OpenPlatform;
+use qiniu\services\SystemConfigService;
+use GuzzleHttp\DefaultHandler;
+use qiniu\utils\Json;
+use think\Service;
+use Yurun\Util\Swoole\Guzzle\SwooleHandler;
+
+/**
+ * 应用服务类
+ */
+class AppService extends Service
+{
+
+    public $bind = [
+        'json' => Json::class,
+        'sysConfig' => SystemConfigService::class,
+];
+    public function register()
+    {
+        // 服务注册
+        //http配置服务
+        $this->app->bind(HttpCommonConfig::class, function () {
+            return (new HttpCommonConfig())->setServe(SystemConfigServices::class);
+        });
+        //公众号
+        $this->app->bind(OfficialAccount::class, function () {
+            return (new OfficialAccount)->setPushMessageHandler(OffcialAccountListener::class);
+        });
+        //小程序
+        $this->app->bind(MiniProgram::class, function () {
+            return (new MiniProgram)->setPushMessageHandler(RoutineListener::class);
+        });
+        //开放平台
+        $this->app->bind(OpenPlatform::class, function () {
+            return (new OpenPlatform)->setPushMessageHandler(OpenPlatformListener::class);
+        });
+    }
+
+    public function boot()
+    {
+        defined('DS') || define('DS', DIRECTORY_SEPARATOR);
+        DefaultHandler::setDefaultHandler(SwooleHandler::class);
+    }
+}

+ 92 - 0
app/ExceptionHandle.php

@@ -0,0 +1,92 @@
+<?php
+
+namespace app;
+
+use crmeb\services\wechat\WechatException;
+use qiniu\exceptions\AdminException;
+use qiniu\exceptions\UploadException;
+use qiniu\exceptions\PayException;
+use qiniu\exceptions\ApiException;
+use qiniu\exceptions\TemplateException;
+use qiniu\exceptions\AuthException;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\exception\Handle;
+use think\exception\HttpException;
+use think\exception\HttpResponseException;
+use think\exception\ValidateException;
+use think\facade\Env;
+use think\Response;
+use Throwable;
+
+/**
+ * 应用异常处理类
+ */
+class ExceptionHandle extends Handle
+{
+    /**
+     * 不需要记录信息(日志)的异常类列表
+     * @var array
+     */
+    protected $ignoreReport = [
+        HttpException::class,
+        HttpResponseException::class,
+        ModelNotFoundException::class,
+        DataNotFoundException::class,
+        ValidateException::class,
+        AdminException::class,
+        UploadException::class,
+        PayException::class,
+        ApiException::class,
+        TemplateException::class,
+        AuthException::class
+    ];
+
+    /**
+     * 记录异常信息(包括日志或者其它方式记录)
+     *
+     * @access public
+     * @param Throwable $exception
+     * @return void
+     */
+    public function report(Throwable $exception): void
+    {
+        // 使用内置的方式记录异常日志
+        parent::report($exception);
+    }
+
+    /**
+     * Render an exception into an HTTP response.
+     *
+     * @access public
+     * @param \think\Request $request
+     * @param Throwable $e
+     * @return Response
+     */
+    public function render($request, Throwable $e): Response
+    {
+        // 添加自定义异常处理机制
+        $massageData = Env::get('app_debug', false) ? [
+            'file' => $e->getFile(),
+            'line' => $e->getLine(),
+            'trace' => $e->getTrace(),
+            'previous' => $e->getPrevious(),
+        ] : [];
+        if ($e instanceof DbException) {
+            return app('json')->fail($e->getMessage(), $massageData);
+        } elseif ($e instanceof AuthException ||
+            $e instanceof ValidateException ||
+            $e instanceof ApiException ||
+            $e instanceof PayException ||
+            $e instanceof TemplateException ||
+            $e instanceof UploadException ||
+            $e instanceof AdminException ||
+            $e instanceof WechatException
+        ) {
+            return app('json')->make($e->getCode() ?: 400, $e->getMessage(), $massageData);
+        } else {
+            return app('json')->code(500)->make(400, $e->getMessage(), $massageData);
+        }
+    }
+}

+ 260 - 0
app/Request.php

@@ -0,0 +1,260 @@
+<?php
+
+namespace app;
+
+use think\exception\ValidateException;
+
+/**
+ * Class Request
+ * @package app
+ */
+class Request extends \think\Request
+{
+
+    private $adminInfo = null;
+    private $user = null;
+    private $tokenData = null;
+
+    /**
+     * 不过滤变量名
+     * @var array
+     */
+    protected $except = [
+        'menu_path', 'api_url', 'unique_auth',
+        'description', 'custom_form', 'product_detail_diy', 'value', 'member', 'product_category_diy'
+    ];
+
+    public function setAdmin($adminInfo)
+    {
+        $this->adminInfo = $adminInfo;
+    }
+
+
+    public function setUser($user)
+    {
+        $this->user = $user;
+    }
+
+    public function setTokenData($data)
+    {
+        $this->tokenData = $data;
+    }
+
+
+    /**
+     * 获取请求的数据
+     * @param array $params
+     * @param bool $suffix
+     * @param bool $filter
+     * @param callable|null $deal
+     * @return array
+     */
+    public function more(array $params, bool $suffix = false, callable $deal = null, bool $filter = true): array
+    {
+        $p = [];
+        $i = 0;
+        foreach ($params as $param) {
+            if (!is_array($param)) {
+                $p[$suffix == true ? $i++ : $param] = $this->param($param);
+            } else {
+                if (!isset($param[1])) $param[1] = null;
+                if (!isset($param[2])) $param[2] = '';
+                if (!isset($param[3])) $param[3] = '';
+                if (!isset($param[4])) $param[4] = '';
+                if (is_array($param[0])) {
+                    $name = is_array($param[1]) ? $param[0][0] . '/a' : $param[0][0] . '/' . $param[0][1];
+                    $keyName = $param[0][0];
+                } else {
+                    $name = is_array($param[1]) ? $param[0] . '/a' : $param[0];
+                    $keyName = $param[0];
+                }
+                $p[$suffix == true ? $i++ : ($param[3] ?: $keyName)] = $this->param($name, $param[1], $param[2]);
+            }
+        }
+        if (!is_null($deal)) {
+            $deal($p);
+        }
+        if ($filter && $p) {
+            $p = $this->filterArrayValues($p);
+        }
+
+        return $p;
+    }
+
+    /**
+     * @param $array
+     * @return array
+     */
+    public function filterArrayValues($array)
+    {
+        $result = [];
+        foreach ($array as $key => $value) {
+            if (is_array($value)) {
+                // 如果值是数组,递归调用 filterArrayValues
+                $result[$key] = in_array($key, $this->except) ? $value : $this->filterArrayValues($value);
+            } else {
+                if (in_array($key, $this->except) || is_int($value) || is_null($value)) {
+                    $result[$key] = $value;
+                } else {
+                    // 如果值是字符串,过滤特殊字符
+                    $result[$key] = filter_str($value);
+                }
+
+            }
+        }
+        return $result;
+    }
+
+
+    /**
+     * 获取get参数
+     * @param array $params
+     * @param bool $suffix
+     * @param callable|null $deal
+     * @param bool $filter
+     * @return array
+     */
+    public function getMore(array $params, bool $suffix = false, callable $deal = null, bool $filter = true): array
+    {
+        return $this->more($params, $suffix, $deal, $filter);
+    }
+
+    /**
+     * 获取post参数
+     * @param array $params
+     * @param bool $suffix
+     * @param callable|null $deal
+     * @param bool $filter
+     * @return array
+     */
+    public function postMore(array $params, bool $suffix = false, callable $deal = null, bool $filter = true): array
+    {
+        return $this->more($params, $suffix, $deal, $filter);
+    }
+
+    /**
+     * 获取用户访问端
+     * @return array|string|null
+     */
+    public function getFromType()
+    {
+        return $this->header('Form-type', '');
+    }
+
+    /**
+     * 当前访问端
+     * @param string $terminal
+     * @return bool
+     */
+    public function isTerminal(string $terminal)
+    {
+        return strtolower($this->getFromType()) === $terminal;
+    }
+
+    /**
+     * 是否是H5端
+     * @return bool
+     */
+    public function isH5()
+    {
+        return $this->isTerminal('h5');
+    }
+
+    /**
+     * 是否是微信端
+     * @return bool
+     */
+    public function isWechat()
+    {
+        return $this->isTerminal('wechat');
+    }
+
+    /**
+     * 是否是小程序端
+     * @return bool
+     */
+    public function isRoutine()
+    {
+        return $this->isTerminal('routine');
+    }
+
+    /**
+     * 是否是app端
+     * @return bool
+     */
+    public function isApp()
+    {
+        return $this->isTerminal('app');
+    }
+
+    /**
+     * 是否是app端
+     * @return bool
+     */
+    public function isPc()
+    {
+        return $this->isTerminal('pc');
+    }
+
+    /**
+     * 获取ip
+     * @return string
+     */
+    public function ip(): string
+    {
+        if ($this->server('HTTP_CLIENT_IP', '')) {
+            $ip = $this->server('HTTP_CLIENT_IP', '');
+        } elseif ($this->server('HTTP_X_REAL_IP', '')) {
+            $ip = $this->server('HTTP_X_REAL_IP', '');
+        } elseif ($this->server('HTTP_X_FORWARDED_FOR', '')) {
+            $ip = $this->server('HTTP_X_FORWARDED_FOR', '');
+            $ips = explode(',', $ip);
+            $ip = $ips[0];
+        } elseif ($this->server('REMOTE_ADDR', '')) {
+            $ip = $this->server('REMOTE_ADDR', '');
+        } else {
+            $ip = '0.0.0.0';
+        }
+        return $ip;
+    }
+
+    public function isAdminLogin(): bool
+    {
+        return !is_null($this->adminInfo);
+    }
+
+    public function adminId()
+    {
+        return $this->adminInfo['id'] ?? 0;
+    }
+
+    public function adminInfo()
+    {
+        return $this->adminInfo ?: [];
+    }
+
+    public function user($key = null)
+    {
+        if ($key) {
+            return $this->user[$key] ?? '';
+        }
+        return $this->user;
+    }
+
+    public function uid()
+    {
+        return $this->user['uid'] ?? 0;
+    }
+
+    public function tokenData()
+    {
+        return $this->tokenData;
+    }
+
+    public function isLogin()
+    {
+        return !is_null($this->user);
+    }
+
+
+}

+ 81 - 0
app/command/Test.php

@@ -0,0 +1,81 @@
+<?php
+/**
+ * @Created by PhpStorm
+ * @author: Kirin
+ * @day: 2024/9/28
+ * @time: 0:14
+ */
+
+namespace app\command;
+
+use app\services\system\config\SystemConfigServices;
+use app\services\TestServices;
+use app\services\user\UserBillServices;
+use app\validate\admin\TestValidate;
+use qiniu\exceptions\AdminException;
+use qiniu\services\sms\Sms;
+use Swoole\Process;
+use think\console\Command;
+use think\console\Input;
+use think\console\Output;
+
+class Test extends Command
+{
+
+    protected $service;
+
+    protected function configure()
+    {
+        // 指令配置
+        $this->setName('test')
+            ->setDescription('调试用');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        $validate = new TestValidate();
+        $date = [
+            'cates' => [1, 2],
+            'inteval' => 1,
+            'number' => 1.9,
+        ];
+        $res = $validate->check($date);
+        /** @var TestServices $service */
+        $service = app()->make(TestServices::class);
+        var_dump($service->get(1)->toArray());
+    }
+
+    public function get_tree_children($data, $id = 0, $field = 'id', $pid = 'pid', $init = true)
+    {
+        static $children;
+        if ($init) {
+            $children = [];
+            foreach ($data as $v) {
+                $children[$v[$pid]][] = $v;
+            }
+            $this->log(count($children));
+        }
+        var_dump($children[16]);
+        $arr = ($children[$id] ?? []);
+//        if (count($arr) > 0) {
+//            foreach ($arr as &$v) {
+//                $this->log($v[$field]);
+//                $v['children'] = $this->get_tree_children($data, $v[$field], $field, $pid, false);
+//            }
+//        }
+        return $arr;
+    }
+
+    /**
+     * 日志输出
+     * @param string $msg
+     */
+    private function log($msg)
+    {
+        $str = "";
+        $str .= $msg;
+        $str .= "   时间:" . date('Y - m - d H:i:s') . PHP_EOL;
+        @file_put_contents('test.log', "[" . date('Y - m - d H:i:s') . "]:" . $msg . PHP_EOL, FILE_APPEND);
+        echo $str;
+    }
+}

+ 792 - 0
app/common.php

@@ -0,0 +1,792 @@
+<?php
+// 应用公共文件
+
+use app\model\user\User;
+use Fastknife\Service\BlockPuzzleCaptchaService;
+use Fastknife\Service\ClickWordCaptchaService;
+use qiniu\exceptions\AuthException;
+use qiniu\services\CacheService;
+use qiniu\services\UploadService;
+use think\exception\ValidateException;
+use think\facade\Log;
+
+if (!function_exists('filter_str')) {
+    /**
+     * 过滤字符串敏感字符
+     * @param $str
+     * @return array|mixed|string|string[]|null
+     */
+    function filter_str($str)
+    {
+        $rules = [
+            '/\.\./', // 禁用包含 ../ 的参数
+            '/\<\?/', // 禁止 php 脚本出现
+            '/\bor\b.*=.*/i', // 匹配 'or 1=1',防止 SQL 注入(注意边界词 \b 和不区分大小写 i 修饰符)
+            '/(select[\s\S]*?)(from|limit)/i', // 防止 SQL 注入
+            '/(union[\s\S]*?select)/i', // 防止 SQL 注入
+            '/(having|updatexml|extractvalue)/i', // 防止 SQL 注入
+            '/sleep\((\s*)(\d*)(\s*)\)/i', // 防止 SQL 盲注
+            '/benchmark\((.*)\,(.*)\)/i', // 防止 SQL 盲注
+            '/base64_decode\(/i', // 防止 SQL 变种注入
+            '/(?:from\W+information_schema\W)/i', // 注意这里的 (?:...) 是不合法的,应该是 (?:...) 表示非捕获组,但通常我们不需要这个
+            '/(?:current_|user|database|schema|connection_id)\s*\(/i', // 防止 SQL 注入(注意去掉了不必要的 (?:...))
+            '/(?:etc\/\W*passwd)/i', // 防止窥探 Linux 用户信息
+            '/into(\s+)(?:dump|out)file\s*/i', // 禁用 MySQL 导出函数
+            '/group\s+by.+\(/i', // 防止 SQL 注入
+            '/(?:define|eval|file_get_contents|include|require|require_once|shell_exec|phpinfo|system|passthru|preg_\w+|execute|echo|print|print_r|var_dump|(fp)open|alert|showmodaldialog)\(/i', // 禁用 webshell 相关某些函数
+            '/(gopher|doc|php|glob|file|phar|zlib|ftp|ldap|dict|ogg|data)\:\/\//i', // 防止一些协议攻击(注意协议后的三个斜杠)
+            '/\$_(GET|POST|COOKIE|FILES|SESSION|ENV|GLOBALS|SERVER)\[/i', // 禁用一些内置变量,注意 PHP 变量名通常是大写的
+            '/<(iframe|script|body|img|layer|div|meta|style|base|object|input)/i', // 防止 XSS 标签植入
+            '/(onmouseover|onerror|onload|onclick)\=/i', // 防止 XSS 事件植入
+            '/\|\|.*?(?:ls|pwd|whoami|ll|ifconfig|ipconfig|&&|chmod|cd|mkdir|rmdir|cp|mv)/i', // 防止执行 shell(注意去掉了不合适的 ifconfog)
+            '/\sand\s+.*=.*/i' // 匹配 and 1=1
+        ];
+        if (filter_var($str, FILTER_VALIDATE_URL)) {
+            $url = parse_url($str);
+            if (!isset($url['scheme'])) return $str;
+            $host = $url['scheme'] . '://' . $url['host'];
+            $str = $host . preg_replace($rules, '', str_replace($host, '', $str));
+        } else {
+            $str = preg_replace($rules, '', $str);
+        }
+        return $str;
+    }
+}
+if (!function_exists('response_log_write')) {
+
+    /**
+     * 日志写入
+     * @param array $data
+     * @param string $type
+     */
+    function response_log_write(array $data, string $type = \think\Log::ERROR)
+    {
+        try {
+            $id = 0;
+            foreach (['adminId', 'kefuId', 'uid', 'supplierId'] as $value) {
+                if (method_exists(request(), $value)) {
+                    $id = request()->{$value}();
+                }
+            }
+
+            //日志内容
+            $log = [
+                $id,                                                                                    //管理员ID
+                request()->ip(),                                                                      //客户ip
+                ceil(microtime() - (request()->time(true) * 1000)),                               //耗时(毫秒)
+                request()->method(true),                                                       //请求类型
+                str_replace("/", "", request()->rootUrl()),                             //应用
+                request()->baseUrl(),                                                                 //路由
+                json_encode(request()->param(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),//请求参数
+                json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),             //报错数据
+            ];
+
+            Log::write(implode("|", $log), $type);
+        } catch (\Throwable $e) {
+
+        }
+    }
+}
+if (!function_exists('not_empty_check')) {
+    /**
+     * 非空验证
+     * @param $param
+     * @return bool
+     */
+    function not_empty_check($param)
+    {
+        if (is_array($param)) {
+            return !(count($param) <= 0);
+        } else {
+            if ($param == '') {
+                return false;
+            }
+            if ($param == null) {
+                return false;
+            }
+            return true;
+        }
+    }
+}
+if (!function_exists('do_request')) {
+    /**
+     * CURL 请求接口
+     * @param string $url 请求地址
+     * @param array $data 请求参数
+     * @param array $header 请求头
+     * @param bool $post true:post请求 false:get请求
+     * @param bool $json post请求时 请求数据打包方式是否为json
+     * @param int $format 数据打包为json格式时打包的格式值 来自json_encode
+     * @param bool $form post请求时 请求数据是否为表单 同时为false时为http提交 优先级高于json
+     * @return bool|false|string
+     */
+    function do_request($url, $query, $data, $header = null, $post = true, $json = false, $format = 0, $form = false)
+    {
+        $curl = curl_init();
+        $url .= ('?' . http_build_query($query));
+        curl_setopt($curl, CURLOPT_URL, $url);
+        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
+        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
+        if ($post) {
+            curl_setopt($curl, CURLOPT_POST, 1);
+            if (!$json && !$form) {
+                curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($data));
+            } else if ($json && !$form) {
+                curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data, $format));
+            } else {
+                curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
+            }
+        }
+        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+        if ($header) {
+            curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
+            curl_setopt($curl, CURLOPT_HEADER, 0);
+        }
+        $result = curl_exec($curl);
+        if (curl_errno($curl)) {
+            return json_encode(['status' => curl_errno($curl), 'msg' => '请求失败']);
+        }
+        curl_close($curl);
+        return $result;
+    }
+}
+if (!function_exists('path_to_url')) {
+    /**
+     * 路径转url路径
+     * @param $path
+     * @return string
+     */
+    function path_to_url($path)
+    {
+        return trim(str_replace(DS, '/', $path), '.');
+    }
+}
+if (!function_exists('get_group_user')) {
+    //所有下级
+    function get_group_user($id, $init = true, $members = null)
+    {
+        if ($init) {
+            $us = User::column('spread_uid', 'uid');
+            $members = [];
+            foreach ($us as $k => $v) {
+                if ($v > 0)
+                    $members[$v][] = $k;
+            }
+            $id = [$id];
+        }
+        $arr = array();
+        foreach ($id as $v) {
+            $child = $members[$v] ?? [];
+            $arr = array_merge($arr, $child);
+        }
+        if (count($arr)) {
+            return array_merge($arr, get_group_user($arr, false, $members));
+        } else {
+            return $arr;
+        }
+    }
+}
+if (!function_exists('sys_config')) {
+    /**
+     * 获取系统单个配置
+     * @param string $name
+     * @param mixed $default
+     * @param bool $cache
+     * @return string|array
+     */
+    function sys_config(string $name, $default = '', bool $cache = false)
+    {
+        if (empty($name))
+            return $default;
+
+        $config = app('sysConfig')->get($name, $default, $cache);
+        if (is_string($config)) $config = trim($config);
+        if ($config === '' || $config === false) {
+            return $default;
+        } else {
+            return $config;
+        }
+    }
+}
+
+if (!function_exists('store_config')) {
+    /**
+     * 获取系统单个配置
+     * @param string $name
+     * @param int $store_id
+     * @param mixed $default
+     * @param bool $cache
+     * @return string
+     */
+    function store_config(string $name, int $store_id, $default = '', bool $cache = false)
+    {
+        if (empty($name))
+            return $default;
+
+        $config = app('sysConfig')->setStore($store_id)->get($name, $default, $cache);
+        if (is_string($config)) $config = trim($config);
+        if ($config === '' || $config === false) {
+            return $default;
+        } else {
+            return $config;
+        }
+    }
+}
+if (!function_exists('check_phone')) {
+    /**
+     * 手机号验证
+     * @param $phone
+     * @return false|int
+     */
+    function check_phone($phone)
+    {
+        return preg_match("/^1[3456789]\d{9}$/", $phone);
+    }
+}
+if (!function_exists('check_mail')) {
+    /**
+     * 邮箱验证
+     * @param $mail
+     * @return false|int
+     */
+    function check_mail($mail)
+    {
+        if (filter_var($mail, FILTER_VALIDATE_EMAIL)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+}
+if (!function_exists('aj_captcha_check_one')) {
+    /**
+     * 验证滑块1次验证
+     * @param string $token
+     * @param string $pointJson
+     * @return bool
+     */
+    function aj_captcha_check_one(string $captchaType, string $token, string $pointJson)
+    {
+        aj_get_serevice($captchaType)->check($token, $pointJson);
+        return true;
+    }
+}
+if (!function_exists('aj_captcha_check_two')) {
+    /**
+     * 验证滑块2次验证
+     * @param string $token
+     * @param string $pointJson
+     * @return bool
+     */
+    function aj_captcha_check_two(string $captchaType, string $captchaVerification)
+    {
+        aj_get_serevice($captchaType)->verificationByEncryptCode($captchaVerification);
+        return true;
+    }
+}
+if (!function_exists('aj_captcha_create')) {
+    /**
+     * 创建验证码
+     * @return array
+     */
+    function aj_captcha_create(string $captchaType)
+    {
+        return aj_get_serevice($captchaType)->get();
+    }
+}
+if (!function_exists('aj_get_serevice')) {
+
+    /**
+     * @param string $captchaType
+     * @return ClickWordCaptchaService|BlockPuzzleCaptchaService
+     */
+    function aj_get_serevice(string $captchaType)
+    {
+        $config = \think\facade\Config::get('ajcaptcha');
+        switch ($captchaType) {
+            case "clickWord":
+                $service = new ClickWordCaptchaService($config);
+                break;
+            case "blockPuzzle":
+                $service = new BlockPuzzleCaptchaService($config);
+                break;
+            default:
+                throw new ValidateException('captchaType参数不正确!');
+        }
+        return $service;
+    }
+}
+
+
+if (!function_exists('sort_list_tier')) {
+    /**
+     * 分级排序
+     * @param $data
+     * @param int $pid
+     * @param string $field
+     * @param string $pk
+     * @param string $html
+     * @param int $level
+     * @param bool $clear
+     * @return array
+     */
+    function sort_list_tier($data, $pid = 0, $field = 'pid', $pk = 'id', $html = '|-----', $level = 1, $clear = true)
+    {
+        static $list = [];
+        if ($clear) $list = [];
+        foreach ($data as $k => $res) {
+            if ($res[$field] == $pid) {
+                $res['html'] = str_repeat($html, $level);
+                $list[] = $res;
+                unset($data[$k]);
+                sort_list_tier($data, $res[$pk], $field, $pk, $html, $level + 1, false);
+            }
+        }
+        return $list;
+    }
+}
+
+
+if (!function_exists('get_tree_children')) {
+    /**
+     * 获取树状结构子集
+     */
+    function get_tree_children($data, $id = 0, $field = 'id', $pid = 'pid', $init = true)
+    {
+        static $children;
+        if ($init) {
+            $children = [];
+            foreach ($data as $v) {
+                $children[$v[$pid]][] = $v;
+            }
+        }
+        $arr = ($children[$id] ?? []);
+        foreach ($arr as &$v) {
+            $v['children'] = get_tree_children($data, $v[$field], $field, $pid, false);
+        }
+        return $arr;
+    }
+}
+
+
+if (!function_exists('set_file_url')) {
+    /**
+     * @param $file
+     * @return mixed|string
+     */
+    function set_file_url($file)
+    {
+        if (!is_array($file)) {
+            $file = [$file];
+        }
+        foreach ($file as &$v) {
+            if (strpos($v, '://') === false) {
+                $v = rtrim(sys_config('site_url', ''), '/') . '/' . ltrim($v, '/');
+            }
+        }
+        return $file;
+    }
+}
+
+if (!function_exists('check_link')) {
+    /**
+     * @param $file
+     * @return mixed|string
+     */
+    function check_link($url)
+    {
+        $ch = curl_init();
+        try {
+            curl_setopt($ch, CURLOPT_URL, $url);
+            curl_setopt($ch, CURLOPT_HEADER, 1);
+            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
+            $contents = curl_exec($ch);
+            if (preg_match("/404/", $contents)) return false;
+            if (preg_match("/403/", $contents)) return false;
+            return true;
+        } catch (\Exception $e) {
+            return false;
+        }
+    }
+}
+
+
+if (!function_exists('filter_emoji')) {
+
+    // 过滤掉emoji表情
+    function filter_emoji($str)
+    {
+        return preg_replace_callback(    //执行一个正则表达式搜索并且使用一个回调进行替换
+            '/./u',
+            function (array $match) {
+                return strlen($match[0]) >= 4 ? '' : $match[0];
+            },
+            $str);
+    }
+}
+
+if (!function_exists('getFileHeaders')) {
+
+    /**
+     * 获取文件大小头部信息
+     * @param string $url
+     * @param $isData
+     * @return array
+     */
+    function getFileHeaders(string $url, $isData = true)
+    {
+        stream_context_set_default(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]]);
+        $header['size'] = 0;
+        $header['type'] = 'image/jpeg';
+        if (!$isData) {
+            return $header;
+        }
+        try {
+            $headerArray = get_headers(str_replace('\\', '/', $url), true);
+            if (!isset($headerArray['Content-Length'])) {
+                $header['size'] = 0;
+            } else {
+                if (is_array($headerArray['Content-Length']) && count($headerArray['Content-Length']) == 2) {
+                    $header['size'] = $headerArray['Content-Length'][1];
+                } else {
+                    $header['size'] = $headerArray['Content-Length'] ?? 0;
+                }
+            }
+            if (!isset($headerArray['Content-Type'])) {
+                $header['type'] = 'image/jpeg';
+            } else {
+                if (is_array($headerArray['Content-Type']) && count($headerArray['Content-Type']) == 2) {
+                    $header['type'] = $headerArray['Content-Type'][1];
+                } else {
+                    $header['type'] = $headerArray['Content-Type'] ?? 'image/jpeg';
+                }
+            }
+        } catch (\Exception $e) {
+        }
+        return $header;
+    }
+}
+
+
+if (!function_exists('formatFileSize')) {
+
+    /**
+     * 格式化文件大小
+     * @param $size
+     * @return mixed|string|null
+     */
+    function formatFileSize($size)
+    {
+        if (!$size) {
+            return '0KB';
+        }
+        try {
+            $toKb = 1024;
+            $toMb = $toKb * 1024;
+            $toGb = $toMb * 1024;
+            if ($size >= $toGb) {
+                return round($size / $toGb, 2) . 'GB';
+            } elseif ($size >= $toMb) {
+                return round($size / $toMb, 2) . 'MB';
+            } elseif ($size >= $toKb) {
+                return round($size / $toKb, 2) . 'KB';
+            } else {
+                return $size . 'B';
+            }
+        } catch (Exception $e) {
+            return '0KB';
+        }
+    }
+}
+
+if (!function_exists('get_image_thumb')) {
+    /**
+     * 获取缩略图
+     * @param $filePath
+     * @param string $type all|big|mid|small
+     * @param bool $is_remote_down
+     * @return mixed|string|string[]
+     */
+    function get_image_thumb($filePath, string $type = 'all', bool $is_remote_down = false)
+    {
+        if (!$filePath || !is_string($filePath) || strpos($filePath, '?') !== false) return $filePath;
+        try {
+            $arr = explode('.', $filePath);
+            $ext_name = trim($arr[count($arr) - 1]);
+            if (!in_array($ext_name, ['png', 'jpg', 'jpeg'])) {
+                return $filePath;
+            }
+            $upload = UploadService::getOssInit($filePath, $is_remote_down);
+            $data = $upload->thumb('', $type);
+            $image = $type == 'all' ? $data : $data[$type] ?? $filePath;
+        } catch (\Throwable $e) {
+            $image = $filePath;
+            //            throw new ValidateException($e->getMessage());
+            Log::error('获取缩略图失败,原因:' . $e->getMessage() . '----' . $e->getFile() . '----' . $e->getLine() . '----' . $filePath);
+        }
+        $data = parse_url($image);
+        if (!isset($data['host']) && (substr($image, 0, 2) == './' || substr($image, 0, 1) == '/')) {//不是完整地址
+            $image = sys_config('site_url') . $image;
+        }
+        //请求是https 图片是http 需要改变图片地址
+        if (strpos(request()->domain(), 'https:') !== false && strpos($image, 'https:') === false) {
+            $image = str_replace('http:', 'https:', $image);
+        }
+        return $image;
+    }
+}
+
+
+if (!function_exists('get_thumb_water')) {
+    /**
+     * 处理数组获取缩略图、水印
+     * @param $list
+     * @param string $type
+     * @param array|string[] $field 1、['image','images'] type 取值参数:type 2、['small'=>'image','mid'=>'images'] type 取field数组的key
+     * @param bool $is_remote_down
+     * @return array|mixed|string|string[]
+     */
+    function get_thumb_water($list, string $type = 'small', array $field = ['image'], bool $is_remote_down = false)
+    {
+        if (!$list || !$field) return $list;
+        $baseType = $type;
+        $data = $list;
+        if (is_string($list)) {
+            $field = [$type => 'image'];
+            $data = ['image' => $list];
+        }
+        if (is_array($data)) {
+            foreach ($field as $type => $key) {
+                if (is_integer($type)) {//索引数组,默认type
+                    $type = $baseType;
+                }
+                //一维数组
+                if (isset($data[$key])) {
+                    if (is_array($data[$key])) {
+                        $path_data = [];
+                        foreach ($data[$key] as $k => $path) {
+                            $path_data[] = get_image_thumb($path, $type, $is_remote_down);
+                        }
+                        $data[$key] = $path_data;
+                    } else {
+                        $data[$key] = get_image_thumb($data[$key], $type, $is_remote_down);
+                    }
+                } else {
+                    foreach ($data as &$item) {
+                        if (!isset($item[$key]))
+                            continue;
+                        if (is_array($item[$key])) {
+                            $path_data = [];
+                            foreach ($item[$key] as $k => $path) {
+                                $path_data[] = get_image_thumb($path, $type, $is_remote_down);
+                            }
+                            $item[$key] = $path_data;
+                        } else {
+                            $item[$key] = get_image_thumb($item[$key], $type, $is_remote_down);
+                        }
+                    }
+                }
+            }
+        }
+        return is_string($list) ? ($data['image'] ?? '') : $data;
+    }
+}
+
+
+if (!function_exists('make_path')) {
+
+    /**
+     * 上传路径转化,默认路径
+     * @param $path
+     * @param int $type
+     * @param bool $force
+     * @return string
+     * @throws Exception
+     */
+    function make_path($path, int $type = 2, bool $force = false)
+    {
+        $path = DS . ltrim(rtrim($path));
+        switch ($type) {
+            case 1:
+                $path .= DS . date('Y');
+                break;
+            case 2:
+                $path .= DS . date('Y') . DS . date('m');
+                break;
+            case 3:
+                $path .= DS . date('Y') . DS . date('m') . DS . date('d');
+                break;
+        }
+        try {
+            if (is_dir(app()->getRootPath() . 'public' . DS . 'uploads' . $path) == true || mkdir(app()->getRootPath() . 'public' . DS . 'uploads' . $path, 0777, true) == true) {
+                return trim(str_replace(DS, '/', $path), '.');
+            } else return '';
+        } catch (Exception $e) {
+            if ($force)
+                throw new Exception($e->getMessage());
+            return '无法创建文件夹,请检查您的上传目录权限:' . app()->getRootPath() . 'public' . DS . 'uploads' . DS . 'attach' . DS;
+        }
+    }
+}
+
+if (!function_exists('verify_domain')) {
+    /**
+     * @param $url
+     * @return false|string
+     */
+    function verify_domain($url)
+    {
+        if (!$url) return false;
+        if (strpos($url, 'http') === false) return false;
+        return rtrim($url, '/');
+    }
+}
+
+
+if (!function_exists('password')) {
+    /**
+     * @param $string
+     * @param null $salt
+     * @return array
+     * @throws Exception
+     */
+    function password($string, $salt = null)
+    {
+        if (!$salt)
+            $salt = bin2hex(random_bytes(6));
+        return [md5(md5($string) . $salt), $salt];
+    }
+}
+
+
+if (!function_exists('parseName')) {
+    /**
+     * 字符串命名风格转换
+     * type 0 将 Java 风格转换为 C 的风格 1 将 C 风格转换为 Java 的风格
+     * @access public
+     * @param string $name 字符串
+     * @param integer $type 转换类型
+     * @param bool $ucfirst 首字母是否大写(驼峰规则)
+     * @return string
+     */
+    function parseName($name, $type = 0, $ucfirst = true)
+    {
+        if ($type) {
+            $name = preg_replace_callback('/_([a-zA-Z])/', function ($match) {
+                return strtoupper($match[1]);
+            }, $name ?? '');
+
+            return $ucfirst ? ucfirst($name) : lcfirst($name);
+        }
+
+        return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_"));
+    }
+}
+
+
+if (!function_exists('check_card')) {
+    function check_card($idCard)
+    {
+        // 去除空格
+        $idCard = trim($idCard);
+
+        // 18位身份证号码正则表达式
+        $pattern = '/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[0-9Xx]$/';
+
+        if (!preg_match($pattern, $idCard)) {
+            return false;
+        }
+
+        // 校验码验证
+        $weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
+        $checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
+
+        $sum = 0;
+        for ($i = 0; $i < 17; $i++) {
+            $sum += intval($idCard[$i]) * $weights[$i];
+        }
+
+        $checkCodeIndex = $sum % 11;
+        $expectedCheckCode = $checkCodes[$checkCodeIndex];
+
+        return strtoupper($idCard[17]) === $expectedCheckCode;
+    }
+
+}
+
+if (!function_exists('check_password')) {
+    function check_password($value)
+    {
+        // 正则表达式:至少6位,最多18位,包含大小写字母、数字和特殊字符
+        $pattern = '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{6,18}$/';
+        return preg_match($pattern, $value) === 1;
+    }
+}
+
+if (!function_exists('check_trade_password')) {
+    function check_trade_password($value)
+    {
+        return preg_match('/^\d{6}$/', $value) === 1;
+
+    }
+}
+
+if (!function_exists('check_sms_captcha')) {
+    function check_sms_captcha($phone, $type, $code)
+    {
+        $verifyCode = CacheService::get('code_' . $phone . '_' . $type);
+        if (!$verifyCode)
+            throw new AuthException('请先获取验证码');
+        $verifyError = (int)CacheService::get('code_error_' . $phone . '_' . $type, 0);
+        if ($verifyError >= 10) {
+            throw new AuthException('错误次数过多,请稍后再试');
+        }
+        $verifyCode = substr($verifyCode, 0, 6);
+        if ($verifyCode != $code) {
+            CacheService::set('code_error_' . $phone . '_' . $type, $verifyError + 1, 180);
+            throw new AuthException('验证码错误');
+        }
+        CacheService::delete('code_' . $phone . '_' . $type);
+        CacheService::delete('code_error_' . $phone . '_' . $type);
+    }
+}
+
+if (!function_exists('random_string')) {
+    function random_string($length)
+    {
+        mt_rand();
+        $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+        $randomString = '';
+        for ($i = 0; $i < $length; $i++) {
+            $index = rand(0, strlen($characters) - 1);
+            $randomString .= $characters[$index];
+        }
+        return $randomString;
+    }
+}
+
+
+if (!function_exists('create_account')) {
+    function create_account()
+    {
+        do {
+            $randomString = 'qn_' . random_string(16) . '_' . random_string(4);
+        } while (User::where('account', $randomString)->find());
+        return $randomString;
+    }
+}
+
+if (!function_exists('curl_file_exist')) {
+    function curl_file_exist($url)
+    {
+        $ch = curl_init($url);
+        curl_setopt($ch, CURLOPT_NOBODY, true); // 不需要返回体
+        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // 跟随重定向
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 返回结果而不是直接输出
+        curl_exec($ch);
+        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        curl_close($ch);
+        return $httpCode == 200;
+    }
+}

+ 158 - 0
app/common/AdminBaseController.php

@@ -0,0 +1,158 @@
+<?php
+/**
+ * @Created by PhpStorm
+ * @author: Kirin
+ * @day: 2024/11/20
+ * @time: 11:24
+ */
+
+namespace app\common;
+
+
+use app\Request;
+use qiniu\basic\BaseController;
+use qiniu\exceptions\AdminException;
+
+abstract class AdminBaseController extends BaseController
+{
+
+
+    /**
+     * 当前登陆管理员信息
+     * @var
+     */
+    protected $adminInfo;
+
+    /**
+     * 当前登陆管理员ID
+     * @var
+     */
+    protected $adminId;
+
+    /**
+     * 当前管理员权限
+     * @var array
+     */
+    protected $auth = [];
+
+    // 搜索条件
+    protected $searchable = [];
+    // 搜索处理
+    protected $searchDeal = null;
+
+    // 创建参数
+    protected $createParams = [];
+    // 新增处理
+    protected $saveDeal = null;
+    // 更新处理
+    protected $updateDeal = null;
+
+    protected $validate = null;
+
+    protected $service = null;
+
+    /**
+     * 初始化
+     */
+
+    public function __construct(Request $request)
+    {
+        parent::__construct($request);
+        $this->initialize();
+    }
+
+    protected function initialize()
+    {
+        $this->adminId = $this->request->adminId();
+        $this->adminInfo = $this->request->adminInfo();
+        $this->auth = $this->adminInfo['rule'] ?? [];
+    }
+
+
+    public function index()
+    {
+        if (!$this->service) {
+            throw new AdminException('接口不存在');
+        }
+        $where = $this->request->getMore($this->searchable, false, $this->searchDeal);
+        list($page, $limit) = $this->service->getPageValue();
+        $list = $this->service->getList($where, '*', $page, $limit);
+        $count = $this->service->getCount($where);
+        return $this->success(compact('list', 'count'));
+    }
+
+
+    public function read($id)
+    {
+        if (!$this->service) {
+            throw new AdminException('接口不存在');
+        }
+        $info = $this->service->get($id);
+        if (!$info)
+            return $this->error('数据不存在');
+        return $this->success('ok', $info->toArray());
+    }
+
+
+    public function save()
+    {
+        if (!$this->service) {
+            throw new AdminException('接口不存在');
+        }
+        $data = $this->request->postMore($this->createParams, false, $this->saveDeal);
+        if ($this->validate) {
+            $this->validate($data, $this->validate);
+        }
+        $res = $this->service->create($data);
+        if ($res) return $this->success('添加成功');
+        return $this->error('添加失败');
+    }
+
+    public function validate($data, $validate)
+    {
+        $res = $validate->check($data);
+        if (!$res) throw new AdminException($validate->getError());
+    }
+
+
+    public function update($id)
+    {
+        if (!$this->service) {
+            throw new AdminException('接口不存在');
+        }
+        $data = $this->service->get($id);
+        if (!$data)
+            return $this->error('数据不存在');
+        $data = $this->request->postMore($this->createParams, false, $this->updateDeal);
+        if ($this->validate) {
+            $this->validate($data, $this->validate);
+        }
+        $res = $this->service->update($id, $data);
+        if ($res) return $this->success('修改成功');
+        return $this->error('修改失败');
+    }
+
+
+    public function delete($id)
+    {
+        if (!$this->service) {
+            throw new AdminException('接口不存在');
+        }
+        $data = $this->service->get($id);
+        if (!$data)
+            return $this->error('数据不存在');
+        $res = $this->service->delete($id);
+        if ($res) return $this->success('已删除');
+        return $this->error('删除失败');
+    }
+
+    public function export()
+    {
+        if (!$this->service) {
+            throw new AdminException('接口不存在');
+        }
+        $where = $this->request->getMore($this->searchable, false, $this->searchDeal);
+        $export_type = sys_config('export_type', 1);
+        return $this->success($this->service->export($where, $export_type));
+    }
+}

+ 48 - 0
app/common/ApiBaseController.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * @Created by PhpStorm
+ * @author: Kirin
+ * @day: 2024/11/20
+ * @time: 11:24
+ */
+
+namespace app\common;
+
+
+use app\Request;
+use qiniu\basic\BaseController;
+
+abstract class ApiBaseController extends BaseController
+{
+
+
+    /**
+     * 当前登陆用户信息
+     * @var
+     */
+    protected $userInfo;
+
+    /**
+     * 当前登陆用户ID
+     * @var
+     */
+    protected $uid;
+
+    protected $service = null;
+
+    /**
+     * 初始化
+     */
+
+    public function __construct(Request $request)
+    {
+        parent::__construct($request);
+        $this->initialize();
+    }
+
+    protected function initialize()
+    {
+        $this->uid = $this->request->uid();
+        $this->userInfo = $this->request->user();
+    }
+}

+ 93 - 0
app/controller/admin/Common.php

@@ -0,0 +1,93 @@
+<?php
+/**
+ * @Created by PhpStorm
+ * @author: Kirin
+ * @day: 2024/11/20
+ * @time: 17:59
+ */
+
+namespace app\controller\admin;
+
+
+use app\common\AdminBaseController;
+use app\services\system\admin\SystemMenusServices;
+use app\services\system\CityAreaServices;
+use Psr\SimpleCache\InvalidArgumentException;
+use qiniu\services\CacheService;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+
+class Common extends AdminBaseController
+{
+
+    public function adminInfo(SystemMenusServices $services)
+    {
+        $adminInfo = $this->request->adminInfo();
+        [$menus, $uniqueAuth] = $services->getMenusList($adminInfo['roles'], (int)$adminInfo['level']);
+        return $this->success([
+            'menus' => $menus,
+            'unique_auth' => $uniqueAuth,
+            'role' => $services->getMenus($this->adminInfo['level'] == 0 ? [] : $this->adminInfo['roles'], 1, 0),
+            'user_info' => [
+                'id' => $adminInfo['id'],
+                'account' => $adminInfo['account'],
+                'head_pic' => $adminInfo['head_pic'],
+            ]
+        ]);
+    }
+
+    /**
+     * 格式化菜单
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException|InvalidArgumentException
+     */
+    public function menusList()
+    {
+        $cahcheKey = md5('admin_common_menu_list');
+        $list = CacheService::redisHandler()->get($cahcheKey);
+        if (!$list) {
+            /** @var SystemMenusServices $menusServices */
+            $menusServices = app()->make(SystemMenusServices::class);
+            $menus = $menusServices->search(['is_show' => 1, 'auth_type' => 1, 'is_show_path' => 0])->where('type', 1)
+                ->field('id,pid,menu_name,menu_path,unique_auth,sort')->order('sort DESC')->select();
+            $counts = $menusServices->getColumn([
+                ['is_show', '=', 1],
+                ['auth_type', '=', 1],
+                ['is_show_path', '=', 0],
+            ], 'pid');
+            $data = [];
+            foreach ($menus as $key => $item) {
+                $pid = $item->getData('pid');
+                $data[$key] = json_decode($item, true);
+                $data[$key]['pid'] = $pid;
+                if (in_array($item->id, $counts)) {
+                    $data[$key]['type'] = 1;
+                } else {
+                    $data[$key]['type'] = 0;
+                }
+                $data[$key]['menu_path'] = preg_replace('/^\/admin/', '', $item['menu_path']);
+            }
+            $list = sort_list_tier($data);
+            CacheService::redisHandler()->set($cahcheKey, $list, 86400);
+        }
+        return app('json')->success($list);
+    }
+
+    /**
+     * @param CityAreaServices $services
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function city(CityAreaServices $services)
+    {
+        $pid = $this->request->get('pid', 0);
+        $type = $this->request->get('type', 0);
+        return $this->success($services->getCityTreeList((int)$pid, $type));
+    }
+
+}

+ 135 - 0
app/controller/admin/Login.php

@@ -0,0 +1,135 @@
+<?php
+/**
+ * @Created by PhpStorm
+ * @author: Kirin
+ * @day: 2024/11/20
+ * @time: 11:21
+ */
+
+namespace app\controller\admin;
+
+
+use app\common\AdminBaseController;
+use app\services\system\admin\SystemAdminServices;
+use app\validate\admin\SystemAdminValidate;
+use qiniu\services\CacheService;
+use qiniu\services\SystemConfigService;
+use qiniu\utils\ApiErrorCode;
+use qiniu\utils\Captcha;
+use think\facade\Cache;
+use think\facade\Config;
+
+class Login extends AdminBaseController
+{
+    /**
+     * 验证码
+     * @return $this|\think\Response
+     */
+    public function captcha()
+    {
+        return app()->make(Captcha::class)->create();
+    }
+
+    /**
+     * 获取验证码
+     * @return mixed
+     */
+    public function getAjCaptcha()
+    {
+        [$account,] = $this->request->postMore([
+            'account',
+        ], true);
+
+        $key = 'login_captcha_' . $account;
+
+        return $this->success(['is_captcha' => Cache::get($key) > 2]);
+    }
+
+    /**
+     * 获取后台登录页轮播图以及LOGO
+     * @return mixed
+     */
+    public function info()
+    {
+        $data = SystemConfigService::more(['admin_login_slide', 'site_logo_square', 'site_logo', 'login_logo'],true);
+        return $this->success([
+            'slide' => sys_config('admin_login_slide') ?? [],
+            'logo_square' => $data['site_logo_square'] ?? '',//透明
+            'logo_rectangle' => $data['site_logo'] ?? '',//方形
+            'login_logo' => $data['login_logo'] ?? '',//登陆
+            'upload_file_size_max' => config('upload.filesize'),//文件上传大小kb
+        ]);
+    }
+
+    /**
+     * @return mixed
+     */
+    public function ajcaptcha()
+    {
+        $captchaType = $this->request->get('captchaType');
+        return $this->success(aj_captcha_create($captchaType));
+    }
+
+
+    /**
+     * 一次验证
+     * @return mixed
+     */
+    public function ajcheck()
+    {
+        [$token, $pointJson, $captchaType] = $this->request->postMore([
+            ['token', ''],
+            ['pointJson', ''],
+            ['captchaType', ''],
+        ], true);
+        try {
+            aj_captcha_check_one($captchaType, $token, $pointJson);
+            return $this->success();
+        } catch (\Throwable $e) {
+            return $this->error();
+        }
+    }
+
+    /**
+     * 登陆
+     * @return mixed
+     */
+    public function login(SystemAdminServices $service)
+    {
+        [$account, $password, $captchaType, $captchaVerification] = $this->request->postMore([
+            'account',
+            'pwd',
+            ['captchaType', ''],
+            ['captchaVerification', ''],
+        ], true);
+
+        $key = 'login_captcha_' . $account;
+
+
+        if (Cache::has($key) && Cache::get($key) > 2) {
+            if (!$captchaType || !$captchaVerification) {
+                return $this->error('请拖动滑块验证');
+            }
+            //二次验证
+            try {
+                aj_captcha_check_two($captchaType, $captchaVerification);
+            } catch (\Throwable $e) {
+                return $this->error($e->getError());
+            }
+        }
+        validate(SystemAdminValidate::class)->scene('get')->check(['account' => $account, 'pwd' => $password]);
+        $res = $service->login($account, $password, 'admin');
+        if ($res) {
+            Cache::delete($key);
+        }
+        return $this->success($res);
+    }
+
+    public function logout()
+    {
+        $key = trim(ltrim($this->request->header(Config::get('cookie.token_name')), 'Bearer'));
+        CacheService::redisHandler()->delete(md5($key));
+        return $this->success();
+    }
+
+}

+ 53 - 0
app/controller/admin/finance/Finance.php

@@ -0,0 +1,53 @@
+<?php
+/**
+ * @Created by PhpStorm
+ * @author: Kirin
+ * @day: 2024/12/12
+ * @time: 9:59
+ */
+
+namespace app\controller\admin\finance;
+
+
+use app\common\AdminBaseController;
+use app\Request;
+use app\services\user\UserBrokerageServices;
+use app\services\user\UserMoneyServices;
+
+class Finance extends AdminBaseController
+{
+    public function __construct(Request $request)
+    {
+        parent::__construct($request);
+    }
+
+    public function moneyLog(UserMoneyServices $services)
+    {
+        $types = $services->bill_type();
+        $where = $this->request->getMore([
+            ['uid', ''],
+            ['nickname', ''],
+            ['type', ''],
+            ['start_time', ''],
+            ['end_time', ''],
+        ]);
+        $data = $services->getMoneyList($where);
+        return $this->success(compact('types', 'data'));
+    }
+
+
+    public function brokerageLog(UserBrokerageServices $services)
+    {
+        $types = $services->bill_type();
+        $where = $this->request->getMore([
+            ['uid', ''],
+            ['nickname', ''],
+            ['type', ''],
+            ['start_time', ''],
+            ['end_time', ''],
+        ]);
+        $data = $services->getBrokerageList($where);
+        return $this->success(compact('types', 'data'));
+
+    }
+}

+ 45 - 0
app/controller/admin/system/Qrcode.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace app\controller\admin\system;
+
+use app\Request;
+use think\exception\ValidateException;
+use app\common\AdminBaseController;
+use app\services\system\QrcodeServices;
+use app\validate\admin\system\QrcodeValidate;
+
+/**
+ * 微信二维码管理控制器
+ * Class Qrcode
+ * @package app\controller\admin\system
+ */
+class Qrcode extends AdminBaseController
+{
+    /**
+     * @param Request $request
+     * @param QrcodeServices $services
+     * @param QrcodeValidate $validate
+     */
+    public function __construct(Request $request, QrcodeServices $services, QrcodeValidate $validate)
+    {
+        parent::__construct($request);
+        $this->service = $services;
+        $this->validate = $validate;
+        $this->searchable = [];
+        $this->searchDeal = function (&$data){
+        };
+        $this->createParams = [
+            ['third_type', ''],
+	        ['third_id', 0],
+	        ['ticket', ''],
+	        ['expire_seconds', 0],
+	        ['status', '1'],
+	        ['url', ''],
+	        ['qrcode_url', ''],
+	        ['scan', 0],
+	        ['type', '3']
+        ];
+        $this->saveDeal = $this->updateDeal = function (&$data){
+        };
+    }
+}

+ 29 - 0
app/controller/admin/system/SmsRecord.php

@@ -0,0 +1,29 @@
+<?php
+/**
+ * @Created by PhpStorm
+ * @author: Kirin
+ * @day: 2024/12/12
+ * @time: 10:03
+ */
+
+namespace app\controller\admin\system;
+
+
+use app\common\AdminBaseController;
+use app\Request;
+use app\services\system\SmsRecordServices;
+
+class SmsRecord extends AdminBaseController
+{
+    public function __construct(Request $request, SmsRecordServices $services)
+    {
+        parent::__construct($request);
+        $this->service = $services;
+        $this->searchable = [
+            ['time', ''],
+            ['phone', ''],
+            ['uid', 0],
+            ['add_ip', '']
+        ];
+    }
+}

+ 115 - 0
app/controller/admin/system/SystemAdmins.php

@@ -0,0 +1,115 @@
+<?php
+/**
+ * @Created by PhpStorm
+ * @author: Kirin
+ * @day: 2024/11/21
+ * @time: 17:39
+ */
+
+namespace app\controller\admin\system;
+
+
+use app\common\AdminBaseController;
+use app\Request;
+use app\services\system\admin\SystemAdminServices;
+use app\services\system\admin\SystemRoleServices;
+use app\validate\admin\SystemAdminValidate;
+use think\exception\ValidateException;
+
+class SystemAdmins extends AdminBaseController
+{
+    public function __construct(Request $request, SystemAdminServices $service)
+    {
+        parent::__construct($request);
+        $this->service = $service;
+        $this->request->filter(['addslashes', 'trim']);
+        $this->validate = new SystemAdminValidate();
+        $this->createParams = [
+            ['account', ''],
+            ['conf_pwd', ''],
+            ['pwd', ''],
+            ['real_name', ''],
+            ['phone', ''],
+            ['roles', []],
+            ['status', 0]
+        ];
+        $this->searchable = [
+            ['name', '', '', 'account_like'],
+            ['roles', ''],
+            ['status', '']
+        ];
+        $this->saveDeal = $this->updateDeal = function (&$data) {
+            $data['level'] = $this->adminInfo['level'] + 1;
+        };
+
+    }
+
+    /**
+     * 显示管理员资源列表
+     *
+     * @return \think\Response
+     */
+    public function index()
+    {
+        $where = $this->request->getMore($this->searchable);
+        $where['level'] = $this->adminInfo['level'] + 1;
+        $where['admin_type'] = 1;
+        return $this->success($this->service->getAdminList($where));
+    }
+
+    /**
+     * @param $id
+     * @return mixed
+     */
+    public function read($id)
+    {
+        $info = $this->service->get($id);
+        if (!$info)
+            return $this->error('数据不存在');
+        if ($info['level'] != $this->adminInfo['level'] + 1)
+            return $this->error('数据不存在');
+        /** @var SystemRoleServices $services */
+        $services = app()->make(SystemRoleServices::class);
+        $info['roles'] = $services->getRoleArray(['level' => $this->adminInfo['level'] + 1]);
+        return $this->success('ok', $info->toArray());
+    }
+
+
+    /**
+     * 修改状态
+     * @param $id
+     * @param $status
+     * @return mixed
+     */
+    public function setStatus($id, $status)
+    {
+        $this->service->update((int)$id, ['status' => $status]);
+        return $this->success($status == 0 ? '关闭成功' : '开启成功');
+    }
+
+
+    /**
+     * 修改当前登陆admin信息
+     * @return mixed
+     * @throws \Psr\SimpleCache\InvalidArgumentException
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public function update_admin()
+    {
+        $data = $this->request->postMore([
+            ['real_name', ''],
+            ['head_pic', ''],
+            ['pwd', ''],
+            ['new_pwd', ''],
+            ['conf_pwd', ''],
+            ['phone', ''],
+            ['code', '']
+        ]);
+        if ($this->service->updateAdmin($this->adminId, $data))
+            return $this->success('修改成功');
+        else
+            return $this->error('修改失败');
+    }
+}

+ 159 - 0
app/controller/admin/system/SystemClearData.php

@@ -0,0 +1,159 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+namespace app\controller\admin\system;
+
+use app\common\AdminBaseController;
+use app\Request;
+use app\services\system\SystemClearServices;
+use qiniu\services\CacheService;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\exception\ValidateException;
+use think\facade\App;
+use app\services\system\attachment\SystemAttachmentServices;
+
+
+/**
+ * 清除默认数据理控制器
+ * Class SystemClearData
+ * @package app\controller\admin\v1\system
+ */
+class SystemClearData extends AdminBaseController
+{
+    /**
+     * 构造方法
+     * SystemClearData constructor.
+     * @param App $app
+     * @param SystemClearServices $services
+     */
+    public function __construct(Request $request, SystemClearServices $services)
+    {
+        parent::__construct($request);
+        $this->service = $services;
+        //生产模式下不允许清除数据
+        if (!env('APP_DEBUG', false)) {
+            throw new ValidateException('生产模式下,禁止操作');
+        }
+    }
+
+    /**
+     * 清除方法入口
+     * @param $type
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function clear($type)
+    {
+        switch ($type) {
+            case 'temp':
+                return $this->userTemp();
+            case 'attachment':
+                return $this->attachmentData();
+            case 'article':
+                return $this->articleData();
+            case 'system':
+                return $this->systemData();
+            case 'user':
+                return $this->userRelevantData();
+            default:
+                return $this->error('参数有误');
+        }
+    }
+
+    /**
+     * 清除用户生成的临时文件
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function userTemp()
+    {
+        /** @var SystemAttachmentServices $services */
+        $services = app()->make(SystemAttachmentServices::class);
+        $ids = implode(',', $services->getColumn(['module_type' => 2], 'att_id'));
+        $services->del($ids);
+        $services->delete(2, 'module_type');
+        return $this->success('清除数据成功!');
+    }
+
+    /**
+     * 清除用户数据
+     * @return mixed
+     */
+    public function userRelevantData()
+    {
+        $this->service->clearData([
+            'user_money', 'user_brokerage', 'user_bill', 'user', 'user_spread', 'user_group', 'user_address'
+        ], true);
+        $this->service->delDirAndFile('./public/uploads/store/comment');
+        return $this->success('清除数据成功!');
+    }
+
+
+    /**
+     * 清除所有附件
+     * @return mixed
+     */
+    public function attachmentData()
+    {
+        $this->service->clearData([
+            'system_attachment', 'system_attachment_category'
+        ], true);
+        $this->service->delDirAndFile('./public/uploads/');
+        return $this->success('清除上传文件成功!');
+    }
+
+    /**
+     * 清楚内容数据
+     * @return mixed
+     */
+    public function articleData()
+    {
+        $this->service->clearData([
+            'article_category', 'article', 'article_content'
+        ], true);
+        return $this->success('清除数据成功!');
+    }
+
+    /**
+     * 清楚系统记录
+     * @return mixed
+     */
+    public function systemData()
+    {
+        $this->service->clearData([
+            'system_log'
+        ], true);
+        return $this->success('清除数据成功!');
+    }
+
+    /**
+     * 替换域名方法
+     * @return mixed
+     */
+    public function replaceSiteUrl()
+    {
+        [$url] = $this->request->postMore([
+            ['url', '']
+        ], true);
+        if (!$url)
+            return $this->error('请输入需要更换的域名');
+        $url = verify_domain($url);
+        if (!$url)
+            return $this->error('域名不合法');
+        $this->service->replaceSiteUrl($url);
+        return $this->success('替换成功!');
+    }
+}

+ 36 - 0
app/controller/admin/system/SystemLogs.php

@@ -0,0 +1,36 @@
+<?php
+/**
+ * @Created by PhpStorm
+ * @author: Kirin
+ * @day: 2024/11/20
+ * @time: 19:58
+ */
+
+namespace app\controller\admin\system;
+
+
+use app\common\AdminBaseController;
+use app\Request;
+use app\services\system\SystemLogServices;
+
+class SystemLogs extends AdminBaseController
+{
+
+    public function __construct(Request $request, SystemLogServices $service)
+    {
+        parent::__construct($request);
+        $this->service = $service;
+    }
+
+    public function index()
+    {
+        $where = $this->request->getMore([
+            ['time', ''],
+            ['admin_id', ''],
+            ['path', ''],
+            ['ip', ''],
+        ]);
+        return $this->success($this->service->getLogList($where, $this->adminInfo['level']));
+    }
+
+}

+ 172 - 0
app/controller/admin/system/SystemMenus.php

@@ -0,0 +1,172 @@
+<?php
+/**
+ * @Created by PhpStorm
+ * @author: Kirin
+ * @day: 2024/11/21
+ * @time: 13:35
+ */
+
+namespace app\controller\admin\system;
+
+
+use app\common\AdminBaseController;
+use app\Request;
+use app\services\system\admin\SystemMenusServices;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\exception\ValidateException;
+use think\helper\Str;
+
+class SystemMenus extends AdminBaseController
+{
+
+    public function __construct(Request $request, SystemMenusServices $service)
+    {
+        parent::__construct($request);
+        $this->service = $service;
+        $this->request->filter(['addslashes', 'trim']);
+        $this->createParams = [
+            ['menu_name', ''],
+            ['icon', ''],
+            ['params', ''],
+            ['menu_path', ''],
+            ['api_url', ''],
+            ['methods', ''],
+            ['unique_auth', ''],
+            ['pid', 0],
+            ['sort', 0],
+            ['auth_type', 1],
+            ['is_show', 1],
+            ['is_show_path', 0],
+            ['extend', []],
+        ];
+        $this->saveDeal = $this->updateDeal = function (&$data) {
+            $data['type'] = 1;
+            if (!$data['menu_name']) throw new ValidateException('请输入菜单名称');
+            if (!$data['menu_name']) throw new ValidateException('请输入菜单名称');
+            if ($data['auth_type'] == 1) {
+                if (!$data['menu_path']) throw new ValidateException('请填写菜单路径');
+            } else if ($data['auth_type'] == 2) {
+                if (!$data['api_url']) throw new ValidateException('请填写接口路径');
+                if (!in_array($data['methods'], ['GET', 'POST', 'PUT', 'DELETE'])) {
+                    throw new ValidateException('请选择接口类型');
+                }
+            }
+            $data['is_show'] = ($data['auth_type'] == 2 ? 1 : $data['is_show']);
+            if ($data['auth_type'] == 2) $data['extend'] = '';
+            $data['extend'] = json_encode($data['extend']);
+        };
+        $this->searchable = [
+            ['type', 1],
+            ['is_show', ''],
+            ['keyword', ''],
+        ];
+    }
+
+
+    /**
+     * 显示和隐藏
+     * @param $id
+     * @return mixed
+     */
+    public function show($id)
+    {
+        if (!$id) {
+            return $this->error('参数错误,请重新打开');
+        }
+        [$show] = $this->request->postMore([['is_show', 0]], true);
+        if ($this->service->update($id, ['is_show' => $show])) {
+            return $this->success('修改成功');
+        } else {
+            return $this->error('修改失败');
+        }
+    }
+
+
+    /**
+     * 获取菜单数据
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function menus()
+    {
+        [$menus, $unique] = $this->service->getMenusList($this->adminInfo['roles'], (int)$this->adminInfo['level']);
+        return app('json')->success(['menus' => $menus, 'unique' => $unique]);
+    }
+
+
+    /**
+     * 获取接口列表
+     * @return array
+     */
+    public function ruleList($type = 1)
+    {
+
+        $this->app = app();
+        $rule = $type == 1 ? 'adminapi/' : 'storeapi/';
+        $this->app->route->setTestMode(true);
+        $this->app->route->clear();
+        $path = $this->app->getRootPath() . 'route' . DIRECTORY_SEPARATOR;
+        $files = is_dir($path) ? scandir($path) : [];
+        foreach ($files as $file) {
+            if (strpos($file, '.php')) {
+                include $path . $file;
+            }
+        }
+
+        $ruleList = $this->app->route->getRuleList();
+        $ruleNewList = [];
+        foreach ($ruleList as $item) {
+            if (Str::contains($item['rule'], $rule)) {
+                $ruleNewList[] = $item;
+            }
+        }
+        foreach ($ruleNewList as $key => &$value) {
+            $only = $value['option']['only'] ?? [];
+            $route = is_string($value['route']) ? explode('/', $value['route']) : [];
+            $value['route'] = is_string($value['route']) ? $value['route'] : '';
+            $action = $route[count($route) - 1] ?? null;
+            if ($only && $action && !in_array($action, $only)) {
+                unset($ruleNewList[$key]);
+            }
+            $except = $value['option']['except'] ?? [];
+            if ($except && $action && in_array($action, $except)) {
+                unset($ruleNewList[$key]);
+            }
+            $middleware = $value['option']['middleware'] ?? [];
+            $new_middleware = [];
+            foreach ($middleware as $v) {
+                if (is_array($v)) $new_middleware[] = $v[0];
+            }
+            $value['option']['middleware'] = $new_middleware;
+        }
+        $ruleList = $ruleNewList;
+        //获取所有的路由
+        $menuApiList = $this->service->getColumn(['auth_type' => 2, 'type' => $type], "concat(`api_url`,'_',lower(`methods`)) as rule");
+        if ($menuApiList) $menuApiList = array_column($menuApiList, 'rule');
+        $list = [];
+        $action_arr = ['index', 'read', 'create', 'save', 'update', 'edit', 'delete'];
+        $middleware = $type == 1 ? 'app\http\middleware\admin\AdminCheckRoleMiddleware' : 'app\http\middleware\store\StoreCheckRoleMiddleware';
+        foreach ($ruleList as $item) {
+            $item['rule'] = str_replace($type == 1 ? 'adminapi/' : 'storeapi/', '', $item['rule']);
+            if (!in_array($item['rule'] . '_' . $item['method'], $menuApiList) && isset($item['option']['middleware']) && in_array($middleware, $item['option']['middleware'])) {
+                $real_name = $item['option']['real_name'] ?? '';
+                if (is_array($real_name)) {
+                    foreach ($action_arr as $action) {
+                        if (Str::contains($item['route'], $action)) {
+                            $real_name = $real_name[$action] ?? '';
+                        }
+                    }
+                }
+                $item['real_name'] = $real_name;
+                unset($item['option']);
+                $item['method'] = strtoupper($item['method']);
+                $list[] = $item;
+            }
+        }
+        return app('json')->success($list);
+    }
+}

+ 102 - 0
app/controller/admin/system/SystemRoles.php

@@ -0,0 +1,102 @@
+<?php
+/**
+ * @Created by PhpStorm
+ * @author: Kirin
+ * @day: 2024/11/21
+ * @time: 17:39
+ */
+
+namespace app\controller\admin\system;
+
+
+use app\common\AdminBaseController;
+use app\Request;
+use app\services\system\admin\SystemMenusServices;
+use app\services\system\admin\SystemRoleServices;
+use qiniu\services\CacheService;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\exception\ValidateException;
+
+class SystemRoles extends AdminBaseController
+{
+    public function __construct(Request $request, SystemRoleServices $service)
+    {
+        parent::__construct($request);
+        $this->service = $service;
+        $this->request->filter(['addslashes', 'trim']);
+        $this->createParams = [
+            ['role_name', ''],
+            ['status', 0],
+            ['checked_menus', []]
+        ];
+        $this->saveDeal = $this->updateDeal = function (&$data) {
+            if (!$data['role_name']) throw new ValidateException('请输入身份名称');
+            if (!is_array($data['checked_menus']) || !count($data['checked_menus']))
+                throw new ValidateException('请选择最少一个权限');
+            $data['rules'] = implode(',', $data['checked_menus']);
+        };
+        $this->searchable = [
+            ['status', ''],
+            ['role_name', ''],
+        ];
+    }
+
+    public function index()
+    {
+        $where = $this->request->getMore($this->searchable);
+        $where['type'] = 0;
+        $where['level'] = $this->adminInfo['level'] + 1;
+        return $this->success($this->service->getRoleList($where));
+    }
+
+    public function read($id)
+    {
+        $info = $this->service->get($id);
+        if (!$info)
+            return $this->error('数据不存在');
+        /** @var SystemMenusServices $services */
+        $services = app()->make(SystemMenusServices::class);
+        $info['menus'] = $services->getMenus($this->adminInfo['level'] == 0 ? [] : $this->adminInfo['roles'], 1, 0);
+        return $this->success('ok', $info->toArray());
+    }
+
+
+    public function save()
+    {
+        $data = $this->request->postMore($this->createParams);
+        $data['level'] = $this->adminInfo['level'] + 1;
+        $res = $this->service->create($data);
+        if ($res) return $this->success('添加成功');
+        return $this->error('添加失败');
+    }
+
+
+    /**
+     * 修改状态
+     * @param $id
+     * @param $status
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function setStatus($id, $status)
+    {
+        if (!$id) {
+            return $this->error('缺少参数');
+        }
+        $role = $this->service->get($id);
+        if (!$role) {
+            return $this->error('没有查到此身份');
+        }
+        $role->status = $status;
+        if ($role->save()) {
+            CacheService::clear();
+            return $this->success('修改成功');
+        } else {
+            return $this->error('修改失败');
+        }
+    }
+}

+ 176 - 0
app/controller/admin/system/attachment/SystemAttachment.php

@@ -0,0 +1,176 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+namespace app\controller\admin\system\attachment;
+
+use app\common\AdminBaseController;
+use app\Request;
+use app\services\system\attachment\SystemAttachmentServices;
+use app\services\system\admin\SystemAdminServices;
+use qiniu\services\CacheService;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\Response;
+
+
+/**
+ * 图片管理类
+ * Class SystemAttachment
+ * @package app\controller\admin\v1\file
+ */
+class SystemAttachment extends AdminBaseController
+{
+    protected $service;
+
+    public function __construct(Request $request, SystemAttachmentServices $service)
+    {
+        parent::__construct($request);
+        $this->service = $service;
+        $this->searchable = [
+            ['name', ''],
+            ['pid', 0],
+            ['file_type', 1]
+        ];
+    }
+
+    /**
+     * 显示列表
+     * @return mixed
+     * @throws DbException
+     */
+    public function index()
+    {
+        $where = $this->request->getMore($this->searchable);
+        $where['type'] = 1;
+        return $this->success($this->service->getImageList($where));
+    }
+
+    /**
+     * 删除指定资源
+     *
+     * @return Response
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function batchDelete()
+    {
+        $ids = $this->request->post('ids', '');
+        $this->service->del($ids);
+        return $this->success('删除成功');
+    }
+
+    /**图片上传
+     * @param int $upload_type
+     * @param int $type
+     * @return mixed
+     */
+    public function upload(int $upload_type = 0, int $type = 0)
+    {
+        [$pid, $file] = $this->request->postMore([
+            ['pid', 0],
+            ['file', 'file'],
+        ], true);
+        $res = $this->service->upload((int)$pid, $file, (int)$upload_type, (int)$type);
+        return $this->success('上传成功', ['src' => $res]);
+    }
+
+    /**
+     * 移动图片
+     * @return mixed
+     */
+    public function moveImageCate()
+    {
+        $data = $this->request->postMore([
+            ['pid', 0],
+            ['images', '']
+        ]);
+        $this->service->move($data);
+        return $this->success('移动成功');
+    }
+
+    /**
+     * 修改文件名
+     * @param $id
+     * @return mixed
+     */
+    public function update($id)
+    {
+        $realName = $this->request->post('real_name', '');
+        if (!$realName) {
+            return $this->error('文件名称不能为空');
+        }
+        $this->service->update($id, ['real_name' => $realName]);
+        return $this->success('修改成功');
+    }
+
+    /**
+     * 获取上传类型
+     * @return mixed
+     */
+    public function uploadType()
+    {
+        $data['upload_type'] = (string)sys_config('upload_type', 1);
+        return app('json')->success($data);
+    }
+
+    /**
+     * 视频分片上传
+     * @return mixed
+     */
+    public function videoUpload()
+    {
+        $data = $this->request->postMore([
+            ['chunkNumber', 0],//第几分片
+            ['currentChunkSize', 0],//分片大小
+            ['chunkSize', 0],//总大小
+            ['totalChunks', 0],//分片总数
+            ['file', 'file'],//文件
+            ['md5', ''],//MD5
+            ['filename', ''],//文件名称
+            ['pid', 0],//分类ID
+        ]);
+        $fileHandle = $this->request->file($data['file']);
+        if (!$fileHandle) return $this->error('上传信息为空');
+        $res = $this->service->videoUpload($data, $fileHandle);
+        return app('json')->success($res);
+    }
+
+    /**
+     * 保存云端视频记录
+     * @return mixed
+     */
+    public function saveVideoAttachment()
+    {
+        $data = $this->request->postMore([
+            ['path', ''],//视频地址
+            ['cover_image', ''],//封面地址
+            ['pid', 0],//分类ID
+            ['upload_type', 1],//上传类型
+        ]);
+        $res = $this->service->saveOssVideoAttachment($data, 1, 0, (int)$data['upload_type']);
+        return app('json')->success($res);
+    }
+
+    /**网络图片上传
+     * @return Response
+     * @throws \Exception
+     */
+    public function onlineUpload()
+    {
+        $data = $this->request->postMore([
+            ['pid', 0],
+            ['images', []]
+        ]);
+        $this->service->onlineUpload($data);
+        return app('json')->success('上传完成');
+    }
+}

+ 113 - 0
app/controller/admin/system/attachment/SystemAttachmentCategory.php

@@ -0,0 +1,113 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+namespace app\controller\admin\system\attachment;
+
+use app\common\AdminBaseController;
+use app\Request;
+use app\services\system\attachment\SystemAttachmentCategoryServices;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\exception\ValidateException;
+use think\facade\App;
+use think\Response;
+
+/**
+ * 图片分类管理类
+ * Class SystemAttachmentCategory
+ * @package app\controller\admin\v1\file
+ */
+class SystemAttachmentCategory extends AdminBaseController
+{
+
+    public function __construct(Request $request, SystemAttachmentCategoryServices $service)
+    {
+        parent::__construct($request);
+        $this->service = $service;
+        $this->searchable = [
+            ['name', ''],
+            ['pid', 0],
+            ['file_type', 1],
+        ];
+        $this->searchDeal = function (&$data) {
+            if ($data['name'] != '') $data['pid'] = '';
+        };
+        $this->createParams = [
+            ['pid', 0],
+            ['name', ''],
+            ['file_type', 1]
+        ];
+        $this->saveDeal = $this->updateDeal = function (&$data) {
+            if (!$data['name']) {
+                throw new ValidateException('请输入分类名称');
+            }
+        };
+    }
+
+    /**
+     * 显示资源列表
+     *
+     * @return Response
+     */
+    public function index()
+    {
+        $where = $this->request->getMore($this->searchable);
+        $where['type'] = 1;
+        return $this->success($this->service->getAll($where));
+    }
+
+    /**
+     * 保存新增
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function save()
+    {
+        $data = $this->request->postMore($this->createParams);
+        $data['type'] = 1;
+        $this->service->save($data);
+        return $this->success('添加成功');
+    }
+
+    /**
+     * 保存更新的资源
+     *
+     * @param int $id
+     * @return Response
+     * @throws DbException
+     * @throws DataNotFoundException
+     * @throws ModelNotFoundException
+     */
+    public function update($id)
+    {
+        $data = $this->request->postMore($this->createParams);
+        $info = $this->service->get($id);
+        $count = $this->service->getCount(['pid' => $id]);
+        if ($count && $info['pid'] != $data['pid']) return $this->error('该分类有下级分类,无法修改上级');
+        $this->service->update($id, $data);
+        return $this->success('分类编辑成功!');
+    }
+
+    /**
+     * 删除指定资源
+     *
+     * @param int $id
+     * @return Response
+     * @throws DbException
+     */
+    public function delete($id)
+    {
+        $this->service->del($id);
+        return $this->success('删除成功!');
+    }
+}

+ 288 - 0
app/controller/admin/system/config/SystemConfig.php

@@ -0,0 +1,288 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+namespace app\controller\admin\system\config;
+
+use app\common\AdminBaseController;
+use app\Request;
+use app\services\system\config\SystemConfigServices;
+use app\services\system\config\SystemConfigTabServices;
+use app\validate\admin\SystemConfigValidate;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\facade\App;
+use think\Response;
+
+/**
+ * 系统配置
+ * Class SystemConfig
+ * @package app\controller\admin\v1\setting
+ */
+class SystemConfig extends AdminBaseController
+{
+
+    /**
+     * SystemConfig constructor.
+     * @param Request $request
+     * @param SystemConfigServices $service
+     */
+    public function __construct(Request $request, SystemConfigServices $service)
+    {
+        parent::__construct($request);
+        $this->service = $service;
+        $this->searchable = [
+            ['tab_id', 0],
+            ['status', -1]
+        ];
+        $this->createParams = [
+            ['menu_name', ''],
+            ['type', ''],
+            ['input_type', ''],
+            ['config_tab_id', []],
+            ['parameter', ''],
+            ['upload_type', 1],
+            ['required', ''],
+            ['width', 0],
+            ['high', 0],
+            ['value', ''],
+            ['info', ''],
+            ['desc', ''],
+            ['sort', 0],
+            ['status', 0],
+            ['is_store', 0],
+        ];
+    }
+
+    /**
+     * 显示资源列表
+     *
+     * @return Response
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function index()
+    {
+        $where = $this->request->getMore($this->searchable);
+        if (!$where['tab_id']) {
+            return $this->error('参数错误');
+        }
+        if ($where['status'] == -1) {
+            unset($where['status']);
+        }
+        $where['is_store'] = 0;
+        return $this->success($this->service->getConfigList($where));
+    }
+
+    /**
+     * 保存新建的资源
+     *
+     * @return Response
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function save()
+    {
+        $data = $this->request->postMore($this->createParams);
+        if ($data['config_tab_id']) $data['config_tab_id'] = end($data['config_tab_id']);
+        if (!$data['info']) return $this->error('请输入配置名称');
+        if (!$data['menu_name']) return $this->error('请输入字段名称');
+        if (!$data['desc']) return $this->error('请输入配置简介');
+        if ($data['sort'] < 0) {
+            $data['sort'] = 0;
+        }
+        if ($data['type'] == 'text') {
+            if (!$data['width']) return $this->error('请输入文本框的宽度');
+            if ($data['width'] <= 0) return $this->error('请输入正确的文本框的宽度');
+        }
+        if ($data['type'] == 'textarea') {
+            if (!$data['width']) return $this->error('请输入多行文本框的宽度');
+            if (!$data['high']) return $this->error('请输入多行文本框的高度');
+            if ($data['width'] < 0) return $this->error('请输入正确的多行文本框的宽度');
+            if ($data['high'] < 0) return $this->error('请输入正确的多行文本框的宽度');
+        }
+        if ($data['type'] == 'radio' || $data['type'] == 'checkbox') {
+            if (!$data['parameter']) return $this->error('请输入配置参数');
+            $this->service->valiDateRadioAndCheckbox($data);
+        }
+        $data['value'] = json_encode($data['value']);
+        $config = $this->service->getOne(['menu_name' => $data['menu_name']]);
+        if ($config) {
+            $this->service->update($config['id'], $data, 'id');
+        } else {
+            $this->service->create($data);
+        }
+        event('config.create', [$data]);
+        return $this->success('添加配置成功!');
+    }
+
+    /**
+     * 保存更新的资源
+     *
+     * @param int $id
+     * @return Response
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function update($id)
+    {
+        $type = request()->post('type');
+        if ($type == 'text' || $type == 'textarea' || $type == 'radio' || ($type == 'upload' && (request()->post('upload_type') == 1 || request()->post('upload_type') == 3))) {
+            $value = request()->post('value');
+        } else {
+            $value = request()->post('value/a');
+        }
+        if (!$value) $value = request()->post(request()->post('menu_name'));
+        $data = $this->request->postMore([['is_store', 0], 'status', 'info', 'desc', 'sort', 'config_tab_id', 'required', 'parameter', ['value', $value], 'upload_type', 'input_type']);
+        $data['config_tab_id'] = end($data['config_tab_id']);
+        if (!$this->service->get($id)) {
+            return $this->error('编辑的记录不存在!');
+        }
+        $data['value'] = json_encode($data['value']);
+        $this->service->update($id, $data);
+        event('config.update');
+        return $this->success('修改成功!');
+    }
+
+    /**
+     * 删除指定资源
+     *
+     * @param int $id
+     * @return Response
+     */
+    public function delete($id)
+    {
+        if (!$this->service->delete($id))
+            return $this->error('删除失败,请稍候再试!');
+        else {
+            event('config.delete', [$id]);
+            return $this->success('删除成功!');
+        }
+    }
+
+    /**
+     * 修改状态
+     * @param $id
+     * @param $status
+     * @return mixed
+     */
+    public function setStatus($id, $status)
+    {
+        if ($status == '' || $id == 0) {
+            return $this->error('参数错误');
+        }
+        $this->service->update($id, ['status' => $status]);
+        event('config.update');
+        return $this->success($status == 0 ? '隐藏成功' : '显示成功');
+    }
+
+    /**
+     * 显示指定的资源
+     *
+     * @param int $id
+     * @return Response
+     */
+    public function readConfig($id)
+    {
+        if (!$id) {
+            return $this->error('参数错误,请重新打开');
+        }
+        $info = $this->service->getReadList((int)$id);
+        return $this->success(compact('info'));
+    }
+
+
+    /**
+     * 保存数据    true
+     * */
+    public function saveConfig(Request $request)
+    {
+        $post = $this->request->post();
+        foreach ($post as $k => $v) {
+            if (is_array($v)) {
+                $res = $this->service->getUploadTypeList($k);
+                foreach ($res as $kk => $vv) {
+                    if ($kk == 'upload') {
+                        if ($vv == 1 || $vv == 3) {
+                            $post[$k] = $v[0];
+                        }
+                    }
+                }
+            }
+        }
+        $this->validate($post, SystemConfigValidate::class);
+        if (isset($post['upload_type'])) {
+            $this->service->checkThumbParam($post);
+        }
+        if (isset($post['spread_banner'])) {
+            $num = count($post['spread_banner']);
+            if ($num > 5) {
+                return $this->error('分销海报不能多于5张');
+            }
+        }
+        if (isset($post['user_extract_min_price'])) {
+            if (!preg_match('/[0-9]$/', $post['user_extract_min_price'])) {
+                return $this->error('提现最低金额只能为数字!');
+            }
+        }
+        if (isset($post['store_extract_max_price']) && isset($post['store_extract_min_price'])) {
+            if ($post['store_extract_max_price'] < $post['store_extract_min_price']) {
+                return $this->error('门店提现最低金额不能大于最高金额');
+            }
+        }
+
+        if (isset($post['default_send_day'])) {
+            $post['default_send_day_start_time'] = time();
+        }
+
+        foreach ($post as $k => $v) {
+            $config_one = $this->service->getOne(['menu_name' => $k]);
+            if ($config_one) {
+                $config_one['value'] = $v;
+                $this->service->valiDateValue($config_one);
+                $this->service->update($k, ['value' => json_encode($v)], 'menu_name');
+            }
+        }
+        event('config.update');
+        return $this->success('修改成功');
+    }
+
+    /**
+     * 获取系统设置头部分类
+     * @param SystemConfigTabServices $services
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function header_basics(SystemConfigTabServices $services)
+    {
+        [$pid] = $this->request->getMore([
+            [['pid', 'd'], 0]
+        ], true);
+        $config_tab = $services->getConfigTab($pid);
+        return $this->success(compact('config_tab'));
+    }
+
+    /**
+     * 获取单个配置的值
+     * @param $name
+     * @return mixed
+     */
+    public function get_system($name)
+    {
+        $value = sys_config($name);
+        return $this->success(compact('value'));
+    }
+
+}

+ 113 - 0
app/controller/admin/system/config/SystemConfigTab.php

@@ -0,0 +1,113 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+namespace app\controller\admin\system\config;
+
+use app\common\AdminBaseController;
+use app\Request;
+use app\services\system\config\SystemConfigServices;
+use app\services\system\config\SystemConfigTabServices;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\exception\ValidateException;
+use think\Response;
+
+
+/**
+ * 配置分类
+ * Class SystemConfigTab
+ * @package app\controller\admin\v1\setting
+ */
+class SystemConfigTab extends AdminBaseController
+{
+    /**
+     * 构造方法
+     * SystemConfigTab constructor.
+     * @param Request $request
+     * @param SystemConfigTabServices $service
+     */
+    public function __construct(Request $request, SystemConfigTabServices $service)
+    {
+        parent::__construct($request);
+        $this->service = $service;
+        $this->searchable = [
+            ['status', ''],
+            ['title', ''],
+            ['is_store', '']
+        ];
+        $this->createParams = [
+            'status',
+            ['title', ''],
+            'icon',
+            ['sort', 0],
+            ['pid', 0],
+            ['is_store', 0],
+        ];
+        $this->saveDeal = $this->updateDeal = function (&$data) {
+            if (!$data['title']) throw new ValidateException('请输入分类名称');
+        };
+    }
+
+    /**
+     * 显示资源列表
+     *
+     * @return Response
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function index()
+    {
+        $where = $this->request->getMore($this->searchable);
+        return $this->success($this->service->getConfgTabList($where));
+    }
+
+    /**
+     * 删除指定资源
+     *
+     * @param int $id
+     * @return Response
+     * @throws DbException
+     */
+    public function delete($id)
+    {
+        /** @var SystemConfigServices $services */
+        $services = app()->make(SystemConfigServices::class);
+        if ($services->be(['tab_id' => $id])) {
+            return $this->error('存在下级配置,无法删除!');
+        }
+        if (!$this->service->delete($id))
+            return $this->error('删除失败,请稍候再试!');
+        else
+            return $this->success('删除成功!');
+    }
+
+    /**
+     * 修改状态
+     * @param $id
+     * @param $status
+     * @return mixed
+     */
+    public function setStatus($id, $status)
+    {
+        if ($status == '' || $id == 0) {
+            return $this->error('参数错误');
+        }
+        $this->service->update($id, ['status' => $status]);
+        return $this->success($status == 0 ? '隐藏成功' : '显示成功');
+    }
+
+
+    public function getSelectForm()
+    {
+        return $this->success($this->service->getSelectForm());
+    }
+}

+ 93 - 0
app/controller/admin/system/config/SystemUserLevel.php

@@ -0,0 +1,93 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+namespace app\controller\admin\system\config;
+
+use app\common\AdminBaseController;
+use app\Request;
+use app\services\system\config\SystemUserLevelServices;
+use app\services\system\config\SystemUserLevelTaskServices;
+use think\exception\ValidateException;
+use think\facade\App;
+
+/**
+ * 会员设置
+ * Class UserLevel
+ * @package app\controller\admin\v1\user
+ */
+class SystemUserLevel extends AdminBaseController
+{
+
+    /**
+     * user constructor.
+     * @param Request $request
+     * @param SystemUserLevelServices $services
+     */
+    public function __construct(Request $request, SystemUserLevelServices $services)
+    {
+        parent::__construct($request);
+        $this->service = $services;
+        $this->searchable = [
+            ['title', ''],
+            ['is_show', '']
+        ];
+        $this->createParams = [
+            ['name', ''],
+            ['is_forever', 0],
+            ['valid_date', 0],
+            ['grade', 0],
+            ['icon', ''],
+            ['image', ''],
+            ['is_show', 1],
+            ['explain', ''],
+            ['task', []],
+        ];
+        $this->saveDeal = $this->updateDeal = function (&$data) {
+            if (!$data['name']) throw new ValidateException('请输入等级名称');
+            if (!$data['grade']) throw new ValidateException('请输入等级');
+            if (!$data['icon']) throw new ValidateException('请上传等级图标');
+            if (!$data['image']) throw new ValidateException('请上传等级背景图标');
+            if ($data['valid_date'] == 0) $data['is_forever'] = 1;
+        };
+    }
+
+
+    public function index()
+    {
+        $where = $this->request->getMore($this->searchable);
+        return $this->success($this->service->getLevelList($where));
+    }
+
+    public function setStatus($id, $status)
+    {
+        $this->service->update((int)$id, ['is_show' => $status]);
+        return $this->success($status == 0 ? '关闭成功' : '开启成功');
+    }
+
+    public function setValue($id)
+    {
+        $data = $this->request->postMore([
+            ['field', ''],
+            ['value', '']
+        ]);
+        if ($data['field'] == '' || $data['value'] == '') return $this->error('缺少参数');
+        $res = $this->service->update((int)$id, [$data['field'] => $data['value']]);
+        if ($res)
+            return $this->success('保存成功');
+        return $this->error('保存失败');
+    }
+
+    public function getTaskBase(SystemUserLevelTaskServices $services)
+    {
+        return $this->success($services->taskBase());
+    }
+
+
+}

+ 405 - 0
app/controller/admin/user/User.php

@@ -0,0 +1,405 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+namespace app\controller\admin\user;
+
+
+use app\common\AdminBaseController;
+use app\Request;
+use app\services\user\UserBillServices;
+use app\services\user\UserGroupServices;
+use app\services\user\UserLevelServices;
+use app\services\user\UserServices;
+use app\services\user\UserSpreadServices;
+use app\services\user\UserWechatuserServices;
+use app\validate\admin\user\UserValidate;
+use qiniu\exceptions\AdminException;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+
+class User extends AdminBaseController
+{
+    /**
+     * user constructor.
+     * @param Request $request
+     * @param UserServices $services
+     */
+    public function __construct(Request $request, UserServices $services)
+    {
+        parent::__construct($request);
+        $this->service = $services;
+        $this->searchable = [
+            ['page', 1],
+            ['limit', 20],
+            ['nickname', ''],
+            ['status', ''],
+            ['is_promoter', ''],
+            ['order', ''],
+            ['data', ''],
+            ['user_type', ''],
+            ['country', ''],
+            ['province', ''],
+            ['city', ''],
+            ['user_time_type', ''],
+            ['user_time', ''],
+            ['sex', ''],
+            [['level', 0], 0],
+            [['group_id', 'd'], 0],
+            ['now_money', 'normal'],
+            ['field_key', ''],
+        ];
+    }
+
+    /**
+     * 显示资源列表头部
+     *
+     * @return \think\Response
+     */
+    public function typeHeader()
+    {
+        $list = $this->service->typeHead();
+        return $this->success(compact('list'));
+    }
+
+    /**
+     * 显示资源列表
+     *
+     * @return \think\Response
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function index()
+    {
+        $where = $this->request->getMore($this->searchable);
+        $where['user_time_type'] = $where['user_time_type'] == 'all' ? '' : $where['user_time_type'];
+        return $this->success($this->service->userIndex($where));
+    }
+
+    /**
+     * 后台添加用户
+     *
+     * @return \think\Response
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function save()
+    {
+        $data = $this->request->postMore([
+            ['is_promoter', 0],
+            ['real_name', ''],
+            ['card_id', ''],
+            ['birthday', ''],
+            ['mark', ''],
+            ['status', 1],
+            ['level', 0],
+            ['phone', 0],
+            ['group_id', 0],
+            ['pwd', ''],
+            ['true_pwd', ''],
+            ['trade_pwd', ''],
+            ['true_trade_pwd', ''],
+            ['sex', 0],
+            ['provincials', ''],
+            ['spread_uid', 0],
+            ['province', 0],
+            ['city', 0],
+            ['area', 0],
+            ['street', 0],
+        ]);
+        $this->validate(['pwd' => $data['pwd'], 'phone' => $data['phone']], app()->make(UserValidate::class));
+        if ($this->service->be(['phone' => $data['phone']])) {
+            return $this->error('手机号已经存在不能添加相同的手机号用户');
+        }
+        $data['nickname'] = substr_replace($data['phone'], '****', 3, 4);
+        if ($data['card_id']) {
+            if (!check_card($data['card_id'])) return $this->error('请输入正确的身份证号码');
+        }
+        if ($data['birthday']) {
+            if (strtotime($data['birthday']) > time()) return $this->error('生日请选择今天之前日期');
+        }
+        if (!$data['true_pwd']) {
+            return $this->error('请输入确认密码');
+        }
+        if ($data['pwd'] != $data['true_pwd']) {
+            return $this->error('两次输入的密码不一致');
+        }
+        if (!check_password($data['pwd'])) {
+            return $this->error('您设置的密码太过简单:至少6位,最多18位,包含大小写字母、数字和特殊字符');
+        }
+        [$data['pwd'], $data['salt']] = password($data['pwd']);
+        unset($data['true_pwd']);
+        if ($data['trade_pwd']) {
+            if (!$data['true_trade_pwd']) {
+                return $this->error('请确认交易密码');
+            }
+            if (!check_trade_password($data['trade_pwd'])) return $this->error('交易密码为6位数字');
+            if ($data['trade_pwd'] != $data['true_trade_pwd']) {
+                return $this->error('两次输入的交易密码不一致');
+            }
+            [$data['trade_pwd'], $data['trade_salt']] = password($data['trade_pwd']);
+            unset($data['true_trade_pwd']);
+        }
+        $data['avatar'] = sys_config('h5_avatar');
+        $data['admin_id'] = $this->adminId;
+        $data['user_type'] = 'h5';
+        $data['birthday'] = empty($data['birthday']) ? 0 : strtotime($data['birthday']);
+        $data['add_time'] = time();
+        $data['account'] = create_account();
+        $spread_uid = $data['spread_uid'] ?? 0;
+        unset($data['spread_uid']);
+        $this->service->transaction(function () use ($data, $spread_uid) {
+            $res = true;
+            $userInfo = $this->service->create($data);
+
+            if ($spread_uid > 0) {
+                /** @var UserSpreadServices $spread */
+                $spread = app()->make(UserSpreadServices::class);
+                $spread->adminSetSpread($userInfo->uid, $spread_uid);
+            }
+
+            if ($data['level']) {
+                $res = $this->service->saveGiveLevel((int)$userInfo->uid, (int)$data['level']);
+            }
+
+            if (!$res) {
+                throw new AdminException('保存添加用户失败');
+            }
+        });
+        return $this->success('添加成功');
+    }
+
+    /**
+     * 执行赠送会员等级
+     * @param $uid
+     * @return mixed
+     * @throws DbException
+     */
+    public function give_level($uid)
+    {
+        if (!$uid) return $this->error('缺少参数');
+        [$level_id] = $this->request->postMore([
+            ['level_id', 0],
+        ], true);
+        return $this->success($this->service->saveGiveLevel((int)$uid, (int)$level_id) ? '赠送成功' : '赠送失败');
+    }
+
+    /**
+     * 清除会员等级
+     * @param $uid
+     * @return mixed
+     */
+    public function del_level($uid)
+    {
+        if (!$uid) return $this->error('缺少参数');
+        return $this->success($this->service->cleanUpLevel((int)$uid) ? '清除成功' : '清除失败');
+    }
+
+    /**
+     * 保存会员分组
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function set_group()
+    {
+        [$group_id, $uids, $all, $where] = $this->request->postMore([
+            ['group_id', 0],
+            ['uids', ''],
+            ['all', 0],
+            ['where', ""],
+        ], true);
+        if (!$uids && $all == 0) return $this->error('缺少参数');
+        if (!$group_id) return $this->error('请选择分组');
+        if ($all == 0) {
+            $uids = explode(',', $uids);
+        }
+        if ($all == 1) {
+            $where = $where ? json_decode($where, true) : [];
+            /** @var UserWechatuserServices $userWechatUser */
+            $userWechatUser = app()->make(UserWechatuserServices::class);
+            $fields = 'u.uid';
+            [$list, $count] = $userWechatUser->getWhereUserList($where, $fields);
+            $uids = array_unique(array_column($list, 'uid'));
+        }
+        /** @var UserGroupServices $userGroup */
+        $userGroup = app()->make(UserGroupServices::class);
+        if (!$userGroup->get($group_id)) {
+            return $this->error('该分组不存在');
+        }
+        $this->service->setUserGroup($uids, $group_id);
+        return $this->success('已设置用户分组!');
+    }
+
+    /**
+     * 执行编辑其他
+     * @param $uid
+     * @return mixed
+     */
+    public function updateAccount($uid)
+    {
+        if (!$uid) return $this->error('数据不存在');
+        list($money_type, $pm, $num, $mark) = $this->request->postMore([
+            ['money_type', ''],
+            ['pm', 1],
+            ['num', 0],
+            ['mark', '']
+        ], true);
+        if (!$money_type) return $this->error('请选择要操作的字段');
+        if ($num <= 0) return $this->error('请输入要操作的金额');
+        return $this->success($this->service->updateAccount($uid, $money_type, $pm, $num, $mark, $this->adminId) ? '修改成功' : '修改失败');
+    }
+
+    /**
+     * 修改user表状态
+     *
+     * @return array
+     */
+    public function set_status($status, $uid)
+    {
+        if ($status == '' || $uid == 0) return $this->error('参数错误');
+        $this->service->update($uid, ['status' => $status], 'uid');
+        return $this->success($status == 0 ? '禁用成功' : '解禁成功');
+    }
+
+    /**
+     * @param $id
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function update($id)
+    {
+        $data = $this->request->postMore([
+            ['is_promoter', -1],
+            ['real_name', ''],
+            ['card_id', ''],
+            ['birthday', ''],
+            ['mark', ''],
+            ['status', 0],
+            ['level', 0],
+            ['phone', 0],
+            ['group_id', 0],
+            ['pwd', ''],
+            ['true_pwd'],
+            ['trade_pwd', ''],
+            ['true_trade_pwd', ''],
+            ['sex', 0],
+            ['provincials', ''],
+            ['province', 0],
+            ['city', 0],
+            ['area', 0],
+            ['street', 0],
+            ['spread_uid', -1],
+        ]);
+        if ($data['phone']) {
+            if (!check_phone($data['phone'])) return $this->error('手机号码格式不正确');
+        }
+        if ($data['card_id']) {
+            if (!check_card($data['card_id'])) return $this->error('请输入正确的身份证');
+        }
+        if ($data['birthday']) {
+            if (strtotime($data['birthday']) > time()) return $this->error('生日请选择今天之前日期');
+        }
+        if ($data['pwd']) {
+            if (!$data['true_pwd']) {
+                return $this->error('请输入确认密码');
+            }
+            if ($data['pwd'] != $data['true_pwd']) {
+                return $this->error('两次输入的密码不一致');
+            }
+            if (!check_password($data['pwd'])) {
+                return $this->error('您设置的密码太过简单:至少6位,最多18位,包含大小写字母、数字和特殊字符');
+            }
+            $this->validate(['pwd' => $data['pwd']], UserValidate::class);
+            [$data['pwd'], $data['salt']] = password($data['pwd']);
+        } else {
+            unset($data['pwd']);
+        }
+        unset($data['true_pwd']);
+        if ($data['trade_pwd']) {
+            if (!$data['true_trade_pwd']) {
+                return $this->error('请确认交易密码');
+            }
+            if (!check_trade_password($data['trade_pwd'])) return $this->error('交易密码为6位数字');
+            if ($data['trade_pwd'] != $data['true_trade_pwd']) {
+                return $this->error('两次输入的交易密码不一致');
+            }
+            [$data['trade_pwd'], $data['trade_salt']] = password($data['trade_pwd']);
+        } else {
+            unset($data['trade_pwd']);
+        }
+        unset($data['true_trade_pwd']);
+        $userInfo = $this->service->get($id);
+        if (!$userInfo) {
+            return $this->error('用户不存在');
+        }
+        if ($data['spread_uid'] != -1) {
+            $spreadUid = $data['spread_uid'];
+            if ($id == $spreadUid) {
+                return $this->error('上级推广人不能为自己');
+            }
+            if (!$this->service->be(['uid' => $spreadUid])) {
+                return $this->error('上级用户不存在');
+            }
+            $spreadInfo = $this->service->get($spreadUid);
+            if ($spreadInfo->spread_uid == $id) {
+                return $this->error('上级推广人不能为自己下级');
+            }
+        }
+        if (!$id) return $this->error('数据不存在');
+        $data['admin_id'] = $this->adminId;
+        $res = $this->service->updateInfo((int)$id, $data);
+        if ($res) {
+            $userInfo = $this->service->get($id);
+            /** @var UserLevelServices $levelServices */
+            $levelServices = app()->make(UserLevelServices::class);
+            $levelServices->detection((int)$userInfo['uid']);
+        }
+        return $this->success($res ? '修改成功' : '修改失败');
+    }
+
+    /**
+     * 获取单个用户信息
+     * @param int $id 用户id
+     * @return mixed
+     */
+    public function oneUserInfo($type, int $id)
+    {
+        $data = $this->request->get();
+        if (!$type) return $this->error('缺少参数');
+        return $this->success($this->service->oneUserInfo($id, $type, $data));
+    }
+
+    /**
+     * 同步微信粉丝用户
+     * @return mixed
+     */
+    public function syncWechatUsers()
+    {
+        $this->service->syncWechatUsers();
+        return $this->success('加入消息队列成功,正在异步执行中');
+    }
+
+    /**
+     * 用户注销
+     * @return mixed
+     */
+    public function delete($id)
+    {
+        if (!$id) return app('json')->fail('用户不存在');
+        event('user.cancelUser', [$id]);
+        return $this->success('注销成功');
+    }
+}

+ 43 - 0
app/controller/admin/user/UserGroup.php

@@ -0,0 +1,43 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+namespace app\controller\admin\user;
+
+use app\common\AdminBaseController;
+use app\Request;
+use app\services\user\UserGroupServices;
+use think\exception\ValidateException;
+
+/**
+ * 会员设置
+ * Class UserLevel
+ * @package app\controller\admin\v1\user
+ */
+class UserGroup extends AdminBaseController
+{
+    /**
+     * user constructor.
+     * @param Request $request
+     * @param UserGroupServices $services
+     */
+    public function __construct(Request $request, UserGroupServices $services)
+    {
+        parent::__construct($request);
+        $this->service = $services;
+        $this->createParams = [
+            ['group_name', '', '', '', function ($value) {
+                if (!$value) throw new ValidateException('请输入分组名称');
+            }],
+        ];
+        $this->saveDeal = $this->updateDeal = function (&$data) {
+            if (!$data['group_name']) throw new ValidateException('请输入分组名称');
+        };
+    }
+}

+ 318 - 0
app/controller/api/Login.php

@@ -0,0 +1,318 @@
+<?php
+/**
+ * @Created by PhpStorm
+ * @author: Kirin
+ * @day: 2024/11/20
+ * @time: 11:17
+ */
+
+namespace app\controller\api;
+
+
+use app\common\ApiBaseController;
+use app\Request;
+use app\services\user\LoginServices;
+use app\validate\api\user\RegisterValidates;
+use Exception;
+use Psr\SimpleCache\InvalidArgumentException;
+use qiniu\services\CacheService;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\exception\ValidateException;
+use think\facade\Config;
+
+class Login extends ApiBaseController
+{
+
+    /**
+     * LoginController constructor.
+     * @param Request $request
+     * @param LoginServices $services
+     */
+    public function __construct(Request $request, LoginServices $services)
+    {
+        parent::__construct($request);
+        $this->service = $services;
+    }
+
+
+    /**
+     * H5账号登陆
+     * @param Request $request
+     * @return mixed
+     * @throws DataNotFoundException|ModelNotFoundException|DbException
+     */
+    public function login(Request $request)
+    {
+        [$account, $password, $spread_uid, $login_type] = $request->postMore([
+            'account', 'password', 'spread_uid', ['login_type', 'account']
+        ], true);
+        if (!$account || !$password) {
+            return app('json')->fail('请输入账号和密码');
+        }
+        validate(\app\validate\api\LoginValidate::class)->check(['account' => $account, 'pwd' => $password]);
+
+        if ($login_type == 'phone') {
+            if (!check_phone($account)) return app('json')->fail('请输入正确的手机号码');
+        }
+
+        return app('json')->success('登录成功', $this->service->login($account, $login_type, $password, $spread_uid));
+    }
+
+
+    /**
+     * 退出登录
+     * @param Request $request
+     * @return mixed
+     * @throws InvalidArgumentException
+     */
+    public function logout(Request $request)
+    {
+        $key = trim(ltrim($request->header(Config::get('cookie.token_name')), 'Bearer'));
+        CacheService::redisHandler()->delete(md5($key));
+        return app('json')->success('成功');
+    }
+
+
+    public function verifyCode()
+    {
+        $unique = password_hash(uniqid(true), PASSWORD_BCRYPT);
+        CacheService::set('sms.key.' . $unique, 0, 300);
+        $time = sys_config('verify_expire_time', 1);
+        return app('json')->success(['key' => $unique, 'expire_time' => $time]);
+    }
+
+    public function captcha(Request $request)
+    {
+        ob_clean();
+        $rep = captcha();
+        $key = app('session')->get('captcha.key');
+        $uni = $request->get('key');
+        if ($uni)
+            CacheService::set('sms.key.cap.' . $uni, $key, 300);
+
+        return $rep;
+    }
+
+    /**
+     * 验证验证码是否正确
+     *
+     * @param $uni
+     * @param string $code
+     * @return bool
+     * @throws InvalidArgumentException
+     */
+    protected function checkCaptcha($uni, string $code): bool
+    {
+        $cacheName = 'sms.key.cap.' . $uni;
+        if (!CacheService::has($cacheName)) {
+            return false;
+        }
+
+        $key = CacheService::get($cacheName);
+
+        $code = mb_strtolower($code, 'UTF-8');
+
+        $res = password_verify($code, $key);
+
+        if ($res) {
+            CacheService::delete($cacheName);
+        }
+
+        return $res;
+    }
+
+    /**
+     * @return mixed
+     */
+    public function ajcaptcha(Request $request)
+    {
+        $captchaType = $request->get('captchaType');
+        return app('json')->success(aj_captcha_create((string)$captchaType));
+    }
+
+    /**
+     * 一次验证
+     * @return mixed
+     */
+    public function ajcheck(Request $request)
+    {
+        [$token, $pointJson, $captchaType] = $request->postMore([
+            ['token', ''],
+            ['pointJson', ''],
+            ['captchaType', ''],
+        ], true);
+        try {
+            aj_captcha_check_one($captchaType, $token, $pointJson);
+            return app('json')->success();
+        } catch (\Throwable $e) {
+            return app('json')->fail(400336);
+        }
+    }
+
+    /**
+     * 验证码发送
+     * @param Request $request
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws InvalidArgumentException
+     * @throws ModelNotFoundException
+     */
+    public function verify(Request $request)
+    {
+        [$phone, $type, $key, $captchaType, $captchaVerification] = $request->postMore([
+            ['phone', 0],
+            ['type', ''],
+            ['key', ''],
+            ['captchaType', ''],
+            ['captchaVerification', ''],
+        ], true);
+
+        $keyName = 'sms.key.' . $key;
+        $nowKey = 'sms.' . date('YmdHi');
+
+        if (!CacheService::has($keyName))
+            return $this->error('发送验证码失败,请刷新页面重新获取');
+
+
+        $total = 1;
+        if (CacheService::has($nowKey)) {
+            $total = CacheService::get($nowKey);
+            if ($total > Config::get('sms.maxMinuteCount', 20))
+                return app('json')->success('触发分钟级流控:' . Config::get('sms.maxMinuteCount', 20));
+        }
+
+        //二次验证
+        try {
+            aj_captcha_check_two($captchaType, $captchaVerification);
+        } catch (\Throwable $e) {
+            return app('json')->fail($e->getError());
+        }
+
+        try {
+            validate(RegisterValidates::class)->scene('code')->check(['phone' => $phone]);
+        } catch (ValidateException $e) {
+            return app('json')->fail($e->getError());
+        }
+        $time = sys_config('verify_expire_time', 1);
+        $smsCode = $this->service->verify($phone, $type, $time, app()->request->ip());
+        if ($smsCode) {
+            CacheService::set('code_' . $phone . '_' . $type, $smsCode, $time * 60);
+            CacheService::set($nowKey, $total, 61);
+            return app('json')->success('发送成功');
+        } else {
+            return app('json')->fail('发送失败');
+        }
+
+    }
+
+    /**
+     * H5注册新用户
+     * @param Request $request
+     * @return mixed
+     * @throws InvalidArgumentException
+     */
+    public function register(Request $request)
+    {
+        [$phone, $captcha, $password, $nickname, $spread_uid] = $request->postMore([
+            ['phone', ''],//手机号
+            ['captcha', ''],//验证码
+            ['password', ''],//密码
+            ['nickname', ''],//密码
+            ['spread_uid', ''],//推荐人ID
+        ], true);
+        try {
+            validate(RegisterValidates::class)->scene('register')->check([
+                'phone' => $phone,
+                'captcha' => $captcha,
+                'password' => $password,
+            ]);
+        } catch (ValidateException $e) {
+            return app('json')->fail($e->getError());
+        }
+        check_sms_captcha($phone, 'register', $captcha);
+        $user_type = $request->getFromType() ? $request->getFromType() : 'h5';
+        $registerStatus = $this->service->register($phone, $password, $spread_uid, $user_type, $nickname);
+        if ($registerStatus) {
+            return app('json')->success('注册成功');
+        }
+        return app('json')->fail('注册失败');
+    }
+
+    /**
+     * 密码修改
+     * @param Request $request
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function reset(Request $request)
+    {
+        [$account, $captcha, $password] = $request->postMore([['phone', ''], ['captcha', ''], ['password', '']], true);
+        check_sms_captcha($account, 'reset', $captcha);
+        try {
+            validate(RegisterValidates::class)->scene('register')->check([
+                'phone' => $account,
+                'captcha' => $captcha,
+                'password' => $password,
+            ]);
+        } catch (ValidateException $e) {
+            return app('json')->fail($e->getError());
+        }
+        $resetStatus = $this->service->reset($account, $password);
+        if ($resetStatus) {
+            return app('json')->success('修改成功');
+        }
+        return app('json')->fail('修改失败');
+    }
+
+    /**
+     * 交易密码修改
+     * @param Request $request
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function reset_trade_pwd(Request $request)
+    {
+        [$captcha, $password] = $request->postMore([['captcha', ''], ['password', '']], true);
+        $account = $request->user()['phone'] ?? '0';
+        check_sms_captcha($account, 'reset', $captcha);
+        if (!check_trade_password($password))
+            return app('json')->fail('交易密码为6位数字');
+        $resetStatus = $this->service->trade_reset($account, $password);
+        if ($resetStatus) {
+            return app('json')->success('修改成功');
+        }
+        return app('json')->fail('修改失败');
+    }
+
+    /**
+     * 手机号登录
+     * @param Request $request
+     * @return mixed
+     * @throws Exception
+     */
+    public function mobile(Request $request)
+    {
+        [$phone, $captcha, $spread_uid] = $request->postMore([['phone', ''], ['captcha', ''], ['spread_uid', 0]], true);
+        //验证手机号
+        try {
+            validate(RegisterValidates::class)->scene('code')->check(['phone' => $phone]);
+        } catch (ValidateException $e) {
+            return app('json')->fail($e->getError());
+        }
+        check_sms_captcha($phone, 'mobile', $captcha);
+        $user_type = $request->getFromType() ? $request->getFromType() : 'h5';
+        $token = $this->service->mobile($phone, $spread_uid, $user_type);
+        if ($token) {
+            return app('json')->success('登录成功', $token);
+        } else {
+            return app('json')->fail('登录失败');
+        }
+    }
+}

+ 64 - 0
app/controller/api/Pay.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace app\controller\api;
+
+
+use qiniu\services\AliPayService;
+use qiniu\services\wechat\Payment;
+
+/**
+ * 支付相关回调
+ * Class Pay
+ * @package app\controller\api\v1
+ */
+class Pay
+{
+
+    /**
+     * 支付回调
+     * @param string $type
+     * @return string|\think\Response
+     * @throws \EasyWeChat\Kernel\Exceptions\Exception
+     */
+    public function notify(string $type)
+    {
+        switch (urldecode($type)) {
+            case 'alipay':
+                return AliPayService::handleNotify();
+                break;
+            case 'routine':
+                return Payment::instance()->setAccessEnd(Payment::MINI)->handleNotify();
+                break;
+            case 'wechat':
+                return Payment::instance()->setAccessEnd(Payment::WEB)->handleNotify();
+                break;
+            case 'app':
+                return Payment::instance()->setAccessEnd(Payment::APP)->handleNotify();
+                break;
+        }
+    }
+
+    /**
+     * 退款回调
+     * @param string $type
+     * @return \think\Response
+     * @throws \EasyWeChat\Kernel\Exceptions\Exception
+     */
+    public function refund(string $type)
+    {
+        switch (urldecode($type)) {
+            case 'alipay':
+
+                break;
+            case 'routine':
+                return Payment::instance()->setAccessEnd(Payment::MINI)->handleRefundedNotify();
+                break;
+            case 'wechat':
+                return Payment::instance()->setAccessEnd(Payment::WEB)->handleRefundedNotify();
+                break;
+            case 'app':
+                return Payment::instance()->setAccessEnd(Payment::APP)->handleRefundedNotify();
+                break;
+        }
+    }
+}

+ 102 - 0
app/controller/api/Pub.php

@@ -0,0 +1,102 @@
+<?php
+/**
+ * @Created by PhpStorm
+ * @author: Kirin
+ * @day: 2024/11/19
+ * @time: 12:48
+ */
+
+namespace app\controller\api;
+
+use app\common\ApiBaseController;
+use app\services\system\CityAreaServices;
+use app\services\system\attachment\SystemAttachmentServices;
+use Psr\SimpleCache\InvalidArgumentException;
+use qiniu\services\CacheService;
+use qiniu\services\UploadService;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+
+class Pub extends ApiBaseController
+{
+
+    /**
+     * 获取版本信息
+     * @return mixed
+     */
+    public function version()
+    {
+        $version = sys_config('app_version', '');
+        $apk = sys_config('app_apk', '');
+        return $this->success(compact('version', 'apk'));
+    }
+
+    /**
+     * 图片上传
+     * @param SystemAttachmentServices $services
+     * @return mixed
+     * @throws InvalidArgumentException
+     */
+    public function upload_image(SystemAttachmentServices $services)
+    {
+        $request = $this->request;
+        $data = $request->postMore([
+            ['filename', 'file'],
+        ]);
+        if (!$data['filename']) return $this->error('参数有误');
+        if (CacheService::has('start_uploads_' . $request->uid()) && CacheService::get('start_uploads_' . $request->uid()) >= 100) return $this->error('非法操作');
+        $upload = UploadService::init();
+        $info = $upload->to('store/comment')->validate()->move($data['filename']);
+        if ($info === false) {
+            return $this->error($upload->getError());
+        }
+        $res = $upload->getUploadInfo();
+        $services->attachmentAdd($res['name'], $res['size'], $res['type'], $res['dir'], $res['thumb_path'], 1, (int)sys_config('upload_type', 1), $res['time'], 3);
+        if (CacheService::has('start_uploads_' . $request->uid()))
+            $start_uploads = (int)CacheService::get('start_uploads_' . $request->uid());
+        else
+            $start_uploads = 0;
+        $start_uploads++;
+        CacheService::set('start_uploads_' . $request->uid(), $start_uploads, 86400);
+        $res['dir'] = path_to_url($res['dir']);
+        if (strpos($res['dir'], 'http') === false) $res['dir'] = sys_config('site_url') . $res['dir'];
+        return $this->success('图片上传成功!', ['name' => $res['name'], 'url' => $res['dir']]);
+    }
+
+    /**
+     * 获取城市
+     * @param CityAreaServices $services
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function city(CityAreaServices $services)
+    {
+        $pid = $this->request->get('pid', 0);
+        $type = $this->request->get('type', 0);
+        return $this->success($services->getCityTreeList((int)$pid, (int)$type));
+    }
+
+
+    /**
+     * 解析(获取导入微信地址)
+     * @param CityAreaServices $services
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function cityList(CityAreaServices $services)
+    {
+        $address = $this->request->param('address', '');
+        if (!$address)
+            return app('json')->fail('地址不存在');
+
+        $city = $services->searchCity(compact('address'));
+        if (!$city) return app('json')->fail('地址暂未录入,请联系管理员');
+        $where = [['id', 'in', array_merge([$city['id']], explode('/', trim($city->path, '/')))]];
+        return app('json')->success($services->getCityList($where, 'id as value,id,name as label,parent_id as pid', ['children']));
+    }
+}

+ 15 - 0
app/controller/api/user/User.php

@@ -0,0 +1,15 @@
+<?php
+/**
+ * @Created by PhpStorm
+ * @author: Kirin
+ * @day: 2024/12/16
+ * @time: 10:43
+ */
+
+namespace app\controller\api\user;
+
+
+class User
+{
+
+}

+ 83 - 0
app/controller/api/user/UserBill.php

@@ -0,0 +1,83 @@
+<?php
+/**
+ * @Created by PhpStorm
+ * @author: Kirin
+ * @day: 2024/12/16
+ * @time: 10:43
+ */
+
+namespace app\controller\api\user;
+
+
+use app\common\ApiBaseController;
+use app\services\system\attachment\SystemAttachmentServices;
+use app\services\system\QrcodeServices;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+
+class UserBill extends ApiBaseController
+{
+    /**
+     * 获取海报详细信息
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getSpreadInfo()
+    {
+        $spreadBanner = sys_config('spread_banner', []);
+        $bannerCount = count($spreadBanner);
+        $routineSpreadBanner = [];
+        if ($bannerCount) {
+            foreach ($spreadBanner as $item) {
+                $routineSpreadBanner[] = ['pic' => $item];
+            }
+        }
+        $uid = (int)$this->request->uid();
+        /** @var QrcodeServices $qrcodeService */
+        $qrcodeService = app()->make(QrcodeServices::class);
+        if ($this->request->isRoutine()) {
+
+            $user = $this->request->user();
+            $uid = (int)$this->request->uid();
+            /** @var SystemAttachmentServices $systemAttachment */
+            $systemAttachment = app()->make(SystemAttachmentServices::class);
+            //小程序
+            $name = $user['uid'] . '_' . $user['is_promoter'] . '_user_routine.jpg';
+            $imageInfo = $systemAttachment->getInfo(['name' => $name]);
+            //检测远程文件是否存在
+            if (isset($imageInfo['att_dir']) && strstr($imageInfo['att_dir'], 'http') !== false && curl_file_exist($imageInfo['att_dir']) === false) {
+                $imageInfo = null;
+                $systemAttachment->delete(['name' => $name]);
+            }
+            $siteUrl = sys_config('site_url');
+            if (!$imageInfo) {
+                //生成小程序地址
+                $qrcode = $qrcodeService->getRoutineQrcode($uid, $name);
+            } else {
+                $qrcode = $imageInfo['att_dir'];
+                if ($imageInfo['image_type'] == 1) $qrcode = $siteUrl . $qrcode;
+            }
+        } else {
+            if (sys_config('share_qrcode', 0) && $this->request->isWechat()) {
+                if (sys_config('spread_share_forever', 0)) {
+                    $qrcode = $qrcodeService->getForeverQrcode('spread-' . $uid, $uid)->url;
+                } else {
+                    $qrcode = $qrcodeService->getTemporaryQrcode('spread-' . $uid, $uid)->url;
+                }
+            } else {
+                $qrcode = '';
+            }
+        }
+        return app('json')->success([
+            'images' => $routineSpreadBanner,
+            'qrcode' => $qrcode,
+            'avatar' => $this->request->user('avatar'),
+            'nickname' => $this->request->user('nickname'),
+            'site_name' => sys_config('site_name')
+        ]);
+    }
+
+}

+ 225 - 0
app/controller/api/wechat/Routine.php

@@ -0,0 +1,225 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\controller\api\wechat;
+
+
+use app\Request;
+use app\services\wechat\RoutineServices;
+
+/**
+ * 小程序相关
+ * Class AuthController
+ * @package app\controller\api\wechat
+ */
+class Routine
+{
+    protected $services = NUll;
+
+    /**
+     * AuthController constructor.
+     * @param RoutineServices $services
+     */
+    public function __construct(RoutineServices $services)
+    {
+        $this->services = $services;
+    }
+
+    /**
+     * 返回用户信息的缓存key,返回是否强制绑定手机号
+     * @param $code
+     * @param $spread_code
+     * @param $spread_spid
+     * @return \think\Response
+     */
+    public function authType($code, $spread_code = '', $spread_spid = '')
+    {
+        if (!$code) return app('json')->fail('参数有误');
+        $data = $this->services->authType($code, $spread_spid, $spread_code);
+        return app('json')->success($data);
+    }
+
+    /**
+     * 根据缓存获取token
+     * @param $key
+     * @return \think\Response
+     */
+    public function authLogin($key)
+    {
+        if (!$key) return app('json')->fail('参数有误');
+        $data = $this->services->authLogin($key);
+        return app('json')->success($data);
+    }
+
+    /**
+     * @param $key
+     * @param $phone
+     * @param $captcha
+     * @param $spread_code
+     * @param $spread_spid
+     * @param $code
+     * @return \think\Response
+     * @throws \Psr\SimpleCache\InvalidArgumentException
+     */
+    public function phoneLogin($key = '', $phone = '', $captcha = '', $spread_code = '', $spread_spid = '', $code = '')
+    {
+        //验证验证码
+        $verifyCode = CacheService::get('code_' . $phone);
+        if (!$verifyCode)
+            return app('json')->fail('请先获取验证码');
+        $verifyCode = substr($verifyCode, 0, 6);
+        if ($verifyCode != $captcha) {
+            return app('json')->fail('验证码错误');
+        }
+        CacheService::delete('code_' . $phone);
+        $data = $this->services->phoneLogin($key, $phone, $spread_spid, $spread_code, $code);
+        return app('json')->success($data);
+    }
+
+    /**
+     * 小程序绑定手机号
+     * @param $code
+     * @param $iv
+     * @param $encryptedData
+     * @return \think\Response
+     */
+    public function bindingPhone($code = '', $iv = '', $encryptedData = '')
+    {
+        if (!$code || !$iv || !$encryptedData) return app('json')->fail('参数有误');
+        $this->services->bindingPhone($code, $iv, $encryptedData);
+        return app('json')->success(410016);
+    }
+
+
+    /**
+     * 小程序授权登录
+     * @param Request $request
+     * @return mixed
+     * @throws \Psr\SimpleCache\InvalidArgumentException
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function auth(Request $request)
+    {
+        [$code, $spread_spid, $spread_code, $iv, $encryptedData] = $request->postMore([
+            ['code', ''],
+            ['spread_spid', 0],
+            ['spread_code', ''],
+            ['iv', ''],
+            ['encryptedData', ''],
+        ], true);
+        $token = $this->services->newAuth($code, $spread_spid, $spread_code, $iv, $encryptedData);
+        if ($token) {
+            if (isset($token['key']) && $token['key']) {
+                return app('json')->successful('授权成功,请绑定手机号', $token);
+            } else {
+                return app('json')->successful('登录成功!', ['token' => $token['token'], 'userInfo' => $token['userInfo'], 'expires_time' => $token['params']['exp'], 'store_user_avatar' => $token['store_user_avatar'] ?? 0]);
+            }
+        } else
+            return app('json')->fail('获取用户访问token失败!');
+    }
+
+    /**
+     * 静默授权
+     * @param $code
+     * @param $spread
+     * @return mixed
+     */
+    public function silenceAuth($code, $spread_code = '', $spread_spid = '')
+    {
+        $token = $this->services->silenceAuth($code, (int)$spread_code, (int)$spread_spid);
+        if ($token && isset($token['key'])) {
+            return app('json')->success('授权成功,请绑定手机号', $token);
+        } else if ($token) {
+            return app('json')->success('登录成功', ['token' => $token['token'], 'expires_time' => $token['params']['exp'], 'store_user_avatar' => $token['store_user_avatar'] ?? 0]);
+        } else
+            return app('json')->fail('登录失败');
+    }
+
+    /**
+     * 静默授权 不登录
+     * @param $code
+     * @param $spread
+     * @return mixed
+     */
+    public function silenceAuthNoLogin($code, $spread_code = '', $spread_spid = '')
+    {
+        $token = $this->services->silenceAuth($code, (int)$spread_code, (int)$spread_spid, true);
+        if ($token && isset($token['auth_login'])) {
+            return app('json')->success('授权成功');
+        } else if ($token) {
+            return app('json')->success('登录成功', ['token' => $token['token'], 'userInfo' => $token['userInfo'], 'expires_time' => $token['params']['exp'], 'store_user_avatar' => $token['store_user_avatar'] ?? 0]);
+        } else
+            return app('json')->fail('登录失败');
+    }
+
+    /**
+     * 静默授权
+     * @param $code
+     * @param $spread
+     * @return mixed
+     */
+    public function silenceAuthBindingPhone($code = '', $spread_code = '', $spread_spid = '', $phone = '', $captcha = '')
+    {
+        //验证验证码
+        $verifyCode = CacheService::get('code_' . $phone);
+        if (!$verifyCode)
+            return app('json')->fail('请先获取验证码');
+        $verifyCode = substr($verifyCode, 0, 6);
+        if ($verifyCode != $captcha) {
+            CacheService::delete('code_' . $phone);
+            return app('json')->fail('验证码错误');
+        }
+        $token = $this->services->silenceAuthBindingPhone($code, $spread_code, $spread_spid, $phone);
+        if ($token) {
+            CacheService::delete('code_' . $phone);
+            return app('json')->success('登录成功', ['token' => $token['token'], 'expires_time' => $token['params']['exp'], 'store_user_avatar' => $token['store_user_avatar'] ?? 0]);
+        } else
+            return app('json')->fail('登录失败');
+    }
+
+    /**
+     * 授权获取小程序用户手机号 直接绑定
+     * @param $code
+     * @param $iv
+     * @param $encryptedData
+     * @return mixed
+     */
+    public function authBindingPhone($code = '', $iv = '', $encryptedData = '', $spread_code = '', $spread_spid = '', $key = '')
+    {
+        if (!$code || !$iv || !$encryptedData)
+            return app('json')->fail('参数有误');
+        $token = $this->services->authBindingPhone($code, $iv, $encryptedData, $spread_code, $spread_spid, $key);
+        if ($token) {
+            return app('json')->success('登录成功', $token);
+        } else
+            return app('json')->fail('登录失败');
+    }
+
+    /**
+     *  更新用户信息
+     * @param $userInfo
+     * @return mixed
+     */
+    public function updateInfo(Request $request, $userInfo)
+    {
+        if (!$userInfo) {
+            return app('json')->fail('参数有误');
+        }
+        $uid = (int)$request->uid();
+        $re = $this->services->updateUserInfo($uid, $userInfo);
+        if ($re) {
+            return app('json')->success('更新成功');
+        } else
+            return app('json')->fail('更新失败');
+    }
+}

+ 177 - 0
app/controller/api/wechat/Wechat.php

@@ -0,0 +1,177 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\controller\api\wechat;
+
+
+use app\common\ApiBaseController;
+use app\Request;
+use app\services\wechat\RoutineServices;
+use app\services\wechat\WechatServices;
+use GuzzleHttp\Exception\GuzzleException;
+use qiniu\services\CacheService;
+use qiniu\services\wechat\OfficialAccount;
+use qiniu\services\wechat\Work;
+use EasyWeChat\Kernel\Exceptions\BadRequestException;
+use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
+use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
+use ReflectionException;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\Response;
+
+/**
+ * 微信公众号
+ * Class WechatController
+ * @package app\controller\api\wechat
+ */
+class Wechat extends ApiBaseController
+{
+
+
+    /**
+     * WechatController constructor.
+     * @param Request $request
+     * @param WechatServices $service
+     */
+    public function __construct(Request $request, WechatServices $service)
+    {
+        parent::__construct($request);
+        $this->service = $service;
+    }
+
+    /**
+     * 微信公众号服务
+     * @return Response
+     * @throws BadRequestException
+     * @throws InvalidArgumentException
+     * @throws InvalidConfigException
+     * @throws ReflectionException
+     */
+    public function serve()
+    {
+        return $this->service->serve();
+    }
+
+    /**
+     * 公众号权限配置信息获取
+     * @param Request $request
+     * @return mixed
+     * @throws GuzzleException
+     * @throws \Psr\SimpleCache\InvalidArgumentException
+     */
+    public function config(Request $request)
+    {
+        $url = $request->get('url', '') ?: sys_config('site_url');
+        return app('json')->success($this->service->config($url));
+    }
+
+    /**
+     * 公众号授权登陆
+     * @param Request $request
+     * @return mixed
+     * @throws DbException
+     * @throws InvalidConfigException
+     * @throws DataNotFoundException
+     * @throws ModelNotFoundException
+     */
+    public function auth(Request $request)
+    {
+        [$spread_spid, $login_type] = $request->getMore([
+            [['spread_spid', 'd'], 0],
+            ['login_type', 'wechat'],
+        ], true);
+        $token = $this->service->auth($spread_spid, $login_type);
+        if ($token && isset($token['key'])) {
+            return app('json')->success('授权成功,请绑定手机号', $token);
+        } else if ($token) {
+            return app('json')->success('登录成功', ['token' => $token['token'], 'userInfo' => $token['userInfo'], 'expires_time' => $token['params']['exp']]);
+        } else
+            return app('json')->fail('登录失败');
+    }
+
+
+    /**
+     * 微信公众号静默授权
+     * @param string $spread_spid
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function silenceAuth($spread_spid = '')
+    {
+        $token = $this->service->silenceAuth($spread_spid);
+        if ($token && isset($token['key'])) {
+            return app('json')->success('授权成功,请绑定手机号', $token);
+        } else if ($token) {
+            return app('json')->success('登录成功', ['token' => $token['token'], 'expires_time' => $token['params']['exp']]);
+        } else
+            return app('json')->fail('登录失败');
+    }
+
+    /**
+     * 微信公众号静默授权
+     * @param $spread_spid
+     * @param $snsapi
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws ModelNotFoundException
+     */
+    public function silenceAuthNoLogin($spread_spid = '', $snsapi = '')
+    {
+        $token = $this->service->silenceAuth($spread_spid, true, $snsapi);
+        if ($token && isset($token['auth_login'])) {
+            return app('json')->success('授权成功', $token);
+        } else if ($token) {
+            return app('json')->success('登录成功', ['token' => $token['token'], 'userInfo' => $token['userInfo'], 'expires_time' => $token['params']['exp']]);
+        } else
+            return app('json')->fail('登录失败');
+    }
+
+
+    /**
+     * 静默授权 手机号直接注册登录
+     * @param string $key
+     * @param string $phone
+     * @param string $captcha
+     * @return mixed
+     * @throws \Psr\SimpleCache\InvalidArgumentException
+     * @throws DataNotFoundException
+     * @throws ModelNotFoundException|DbException
+     */
+    public function authBindingPhone($key = '', $phone = '', $captcha = '')
+    {
+        //验证验证码
+        check_sms_captcha($phone, 'wechat_auth', $captcha);
+        $token = $this->service->authBindingPhone($key, $phone);
+        if ($token) {
+            return app('json')->success('登录成功', $token);
+        } else
+            return app('json')->fail('登录失败');
+    }
+
+    /**
+     * 关注二维码
+     * @return mixed
+     */
+    public function follow()
+    {
+        $data = $this->service->follow();
+        if ($data) {
+            return app('json')->success('ok', $data);
+        } else {
+            return app('json')->fail('获取失败');
+        }
+
+    }
+}

+ 37 - 0
app/event.php

@@ -0,0 +1,37 @@
+<?php
+// 事件定义文件
+return [
+    'bind' => [
+    ],
+
+    'listen' => [
+        'AppInit' => [],
+        'HttpRun' => [],
+        'HttpEnd' => [],
+        'LogLevel' => [],
+        'LogWrite' => [],
+        //swoole 初始化事件
+        'swoole.init' => [
+            \qiniu\listeners\InitSwooleLockListen::class, //初始化
+        ],
+        //swoole 启动事件
+//        'swoole.workerStart' => [
+//            \crmeb\listeners\SwooleCronListen::class, //定时任务
+//        ],
+        'swoole.workerExit' => [],
+        'swoole.workerError' => [],
+        'swoole.workerStop' => [],
+//        'swoole.shutDown' => [\qiniu\listeners\SwooleShutdownListen::class],//swoole 停止事件
+//        'swoole.websocket.user' => [\app\webscoket\handler\UserHandler::class],//socket 用户调用事件
+//        'swoole.websocket.kefu' => [\app\webscoket\handler\KefuHandler::class],//socket 客服事件
+//        'swoole.websocket.admin' => [\app\webscoket\handler\AdminHandler::class],//socket 后台事件
+        'config.create' => [\app\listener\system\config\CreateSuccess::class],//创建配置事件
+        'config.delete' => [\app\listener\system\config\DeleteSuccess::class],//删除配置事件
+        'config.update' => [\app\listener\system\config\StatusSuccess::class],//删除配置事件
+        'user.cancelUser' => [\app\listener\user\CancelUser::class],
+
+    ],
+
+    'subscribe' => [
+    ],
+];

+ 54 - 0
app/http/middleware/AllowOriginMiddleware.php

@@ -0,0 +1,54 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\http\middleware;
+
+
+use app\Request;
+use qiniu\interfaces\MiddlewareInterface;
+use think\facade\Config;
+use think\Response;
+
+/**
+ * 跨域中间件
+ * Class AllowOriginMiddleware
+ * @package app\http\middleware
+ */
+class AllowOriginMiddleware implements MiddlewareInterface
+{
+
+    /**
+     * 允许跨域的域名
+     * @var string
+     */
+    protected $cookieDomain;
+
+    /**
+     * @param Request $request
+     * @param \Closure $next
+     * @return Response
+     */
+    public function handle(Request $request, \Closure $next)
+    {
+        $this->cookieDomain = Config::get('cookie.domain', '');
+        $header = Config::get('cookie.header');
+        $origin = $request->header('origin');
+        if ($origin && ('' == $this->cookieDomain || strpos($origin, $this->cookieDomain)))
+            $header['Access-Control-Allow-Origin'] = $origin;
+        if ($request->method(true) == 'OPTIONS') {
+            $response = Response::create('ok')->code(200)->header($header);
+        } else {
+            $response = $next($request)->header($header);
+        }
+        $request->filter(['strip_tags', 'addslashes', 'trim']);
+        return $response;
+    }
+}

+ 64 - 0
app/http/middleware/EncryptDecryptMiddleware.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace app\http\middleware;
+
+use Closure;
+use app\Request;
+use think\Response;
+
+class EncryptDecryptMiddleware
+{
+    protected $secretKey = 'DUOs9QyBTDGdq3kPQcOnS6HEyp24He2h'; // 替换为你的密钥
+    protected $cipher = 'AES-128-CBC';
+
+    public function handle(Request $request, Closure $next): Response
+    {
+        // 解密请求数据
+        if ($request->param('iv')) {
+            $decryptedData = $this->decryptData($request->param('iv'));
+            foreach ($decryptedData as $key => $value) {
+                $request->param([$key => $value]);
+            }
+            // 继续执行下一个中间件或控制器
+            $response = $next($request);
+
+            // 加密响应数据
+            $originalContent = $response->getContent();
+            $encryptedContent = $this->encryptData(json_decode($originalContent, true));
+            $response->setContent(json_encode($encryptedContent));
+            return $response;
+        } else {
+            return $next($request);
+        }
+    }
+
+    private function encryptData(array $data): array
+    {
+        $ivlen = openssl_cipher_iv_length($this->cipher);
+        $iv = openssl_random_pseudo_bytes($ivlen);
+        $ciphertext_raw = openssl_encrypt(json_encode($data), $this->cipher, $this->secretKey, OPENSSL_RAW_DATA, $iv);
+        $hmac = hash_hmac('sha256', $ciphertext_raw, $this->secretKey, true);
+        $ciphertext = base64_encode($iv . $hmac . $ciphertext_raw);
+
+        return ['data' => $ciphertext];
+    }
+
+    private function decryptData(array $data): array
+    {
+        if (isset($data['data'])) {
+            $c = base64_decode($data['data']);
+            $ivlen = openssl_cipher_iv_length($this->cipher);
+            $iv = substr($c, 0, $ivlen);
+            $hmac = substr($c, $ivlen, $sha2len = 32);
+            $ciphertext_raw = substr($c, $ivlen + $sha2len);
+            $original_plaintext = openssl_decrypt($ciphertext_raw, $this->cipher, $this->secretKey, OPENSSL_RAW_DATA, $iv);
+            $calcmac = hash_hmac('sha256', $ciphertext_raw, $this->secretKey, true);
+
+            if (hash_equals($hmac, $calcmac)) {
+                return json_decode($original_plaintext, true);
+            }
+        }
+
+        return [];
+    }
+}

+ 32 - 0
app/http/middleware/StationOpenMiddleware.php

@@ -0,0 +1,32 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\http\middleware;
+
+
+use app\Request;
+use qiniu\interfaces\MiddlewareInterface;
+
+/**
+ * 站点升级
+ * Class StationOpenMiddleware
+ * @package app\api\middleware
+ */
+class StationOpenMiddleware implements MiddlewareInterface
+{
+    public function handle(Request $request, \Closure $next)
+    {
+        if (!sys_config('station_open', true)) {
+            return app('json')->make('410010', '站点升级中,请稍候访问');
+        }
+        return $next($request);
+    }
+}

+ 47 - 0
app/http/middleware/admin/AdminAuthTokenMiddleware.php

@@ -0,0 +1,47 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\http\middleware\admin;
+
+
+use app\Request;
+use app\services\system\admin\SystemAdminServices;
+use Closure;
+use Psr\SimpleCache\InvalidArgumentException;
+use qiniu\interfaces\MiddlewareInterface;
+use think\facade\Config;
+
+/**
+ * 后台登陆验证中间件
+ * Class AdminAuthTokenMiddleware
+ * @package app\http\middleware\admin
+ */
+class AdminAuthTokenMiddleware implements MiddlewareInterface
+{
+    /**
+     * @param Request $request
+     * @param Closure $next
+     * @return mixed
+     * @throws InvalidArgumentException
+     */
+    public function handle(Request $request, Closure $next)
+    {
+        $token = trim(ltrim($request->header(Config::get('cookie.token_name', 'Authori-zation')), 'Bearer'));
+
+        /** @var SystemAdminServices $service */
+        $service = app()->make(SystemAdminServices::class);
+        $adminInfo = $service->parseToken($token);
+
+        $request->setAdmin($adminInfo);
+
+        return $next($request);
+    }
+}

+ 41 - 0
app/http/middleware/admin/AdminCheckRoleMiddleware.php

@@ -0,0 +1,41 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\http\middleware\admin;
+
+use app\Request;
+use app\services\system\admin\SystemRoleServices;
+use qiniu\exceptions\AuthException;
+use qiniu\interfaces\MiddlewareInterface;
+use qiniu\utils\ApiErrorCode;
+
+/**
+ * 权限规则验证
+ * Class AdminCkeckRoleMiddleware
+ * @package app\http\middleware
+ */
+class AdminCheckRoleMiddleware implements MiddlewareInterface
+{
+
+    public function handle(Request $request, \Closure $next)
+    {
+        if (!$request->adminId() || !$request->adminInfo())
+            throw new AuthException(ApiErrorCode::ERR_ADMINID_VOID);
+
+        if ($request->adminInfo()['level']) {
+            /** @var SystemRoleServices $systemRoleService */
+            $systemRoleService = app()->make(SystemRoleServices::class);
+            $systemRoleService->verifiAuth($request);
+        }
+
+        return $next($request);
+    }
+}

+ 42 - 0
app/http/middleware/admin/AdminLogMiddleware.php

@@ -0,0 +1,42 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\http\middleware\admin;
+
+
+use app\Request;
+use app\jobs\system\AdminLogJob;
+use qiniu\interfaces\MiddlewareInterface;
+
+/**
+ * 日志中間件
+ * Class AdminLogMiddleware
+ * @package app\http\middleware\admin
+ */
+class AdminLogMiddleware implements MiddlewareInterface
+{
+    /**
+     * @param Request $request
+     * @param \Closure $next
+     * @return mixed
+     */
+    public function handle(Request $request, \Closure $next)
+    {
+        $module = $request->method();
+        $params = $request->param();
+        $rule = trim(strtolower($request->rule()->getRule()));
+        //记录后台日志
+        AdminLogJob::dispatch([$request->adminId(), $request->adminInfo()['account'], $module, $rule, $request->ip(), 'system', json_encode($params)]);
+
+        return $next($request);
+    }
+
+}

+ 56 - 0
app/http/middleware/api/AuthTokenMiddleware.php

@@ -0,0 +1,56 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\http\middleware\api;
+
+
+use app\Request;
+use app\services\user\UserAuthServices;
+use Closure;
+use Psr\SimpleCache\InvalidArgumentException;
+use qiniu\exceptions\AuthException;
+use qiniu\interfaces\MiddlewareInterface;
+
+/**
+ * Class AuthTokenMiddleware
+ * @package app\api\middleware
+ */
+class AuthTokenMiddleware implements MiddlewareInterface
+{
+    /**
+     * @param Request $request
+     * @param Closure $next
+     * @param bool $force
+     * @return mixed|object
+     * @throws InvalidArgumentException
+     */
+    public function handle(Request $request, Closure $next, bool $force = true)
+    {
+        $authInfo = null;
+        $token = trim(ltrim($request->header('Authori-zation'), 'Bearer'));
+        if (!$token) $token = trim(ltrim($request->header('Authorization'), 'Bearer'));//正式版,删除此行,某些服务器无法获取到token调整为 Authori-zation
+        try {
+            /** @var UserAuthServices $service */
+            $service = app()->make(UserAuthServices::class);
+            $authInfo = $service->parseToken($token);
+        } catch (AuthException $e) {
+            if ($force)
+                return app('json')->make($e->getCode(), $e->getMessage());
+        }
+
+        if (!is_null($authInfo)) {
+            $request->setUser($authInfo['user']);
+            $request->setTokenData($authInfo['tokenData']);
+        }
+
+        return $next($request);
+    }
+}

+ 68 - 0
app/http/middleware/api/BlockerMiddleware.php

@@ -0,0 +1,68 @@
+<?php
+/**
+ *  +----------------------------------------------------------------------
+ *  | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+ *  +----------------------------------------------------------------------
+ *  | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+ *  +----------------------------------------------------------------------
+ *  | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+ *  +----------------------------------------------------------------------
+ *  | Author: CRMEB Team <admin@crmeb.com>
+ *  +----------------------------------------------------------------------
+ */
+
+namespace app\http\middleware\api;
+
+
+use app\Request;
+use Closure;
+use qiniu\exceptions\ApiException;
+use qiniu\interfaces\MiddlewareInterface;
+use qiniu\services\CacheService;
+
+/**
+ * reids锁
+ * Class BlockerMiddleware
+ * @author 等风来
+ * @email 136327134@qq.com
+ * @date 2022/11/21
+ * @package app\http\middleware\api
+ */
+class BlockerMiddleware implements MiddlewareInterface
+{
+
+    /**
+     * @param Request $request
+     * @param Closure $next
+     * @return mixed
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/11/21
+     */
+    public function handle(Request $request, Closure $next)
+    {
+        $uid = $request->uid();
+        $key = md5($request->rule()->getRule() . $uid);
+        if (!CacheService::setMutex($key)) {
+            throw new ApiException('请求太过频繁,请稍后再试');
+        }
+
+        $response = $next($request);
+
+        $this->after($response, $key);
+
+        return $response;
+    }
+
+    /**
+     * @param $response
+     * @param $key
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/11/22
+     */
+    public function after($response, $key)
+    {
+        CacheService::delMutex($key);
+    }
+}

+ 110 - 0
app/http/middleware/api/RateLimiterMiddleware.php

@@ -0,0 +1,110 @@
+<?php
+/**
+ *  +----------------------------------------------------------------------
+ *  | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+ *  +----------------------------------------------------------------------
+ *  | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+ *  +----------------------------------------------------------------------
+ *  | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+ *  +----------------------------------------------------------------------
+ *  | Author: CRMEB Team <admin@crmeb.com>
+ *  +----------------------------------------------------------------------
+ */
+
+namespace app\http\middleware\api;
+
+
+use app\Request;
+use Closure;
+use Redis;
+use think\exception\ValidateException;
+use think\facade\Cache;
+
+/**
+ * 限流中间件
+ * Class RateLimiterMiddleware
+ * @author 等风来
+ * @email 136327134@qq.com
+ * @date 2023/1/4
+ * @package app\http\middleware\api
+ */
+class RateLimiterMiddleware
+{
+
+    /**
+     * @param Request $request
+     * @param Closure $next
+     * @return mixed
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/1/4
+     */
+    public function handle(Request $request, Closure $next)
+    {
+
+        $option = $request->rule()->getOption();
+
+        //是否开启限流
+        if (isset($option['rateLimiter']) && $option['rateLimiter']) {
+            $rule = trim(strtolower($request->rule()->getRule()));
+            $uid = 0;
+            if ($request->uid() && !empty($option['isUser'])) {
+                $uid = $request->uid();
+            }
+            $key = md5($rule . $uid);
+            $this->createLimit($key, $option['limitNum'], $option['expire']);
+        }
+        return $next($request);
+    }
+
+    /**
+     * @param string $key
+     * @param int $initNum
+     * @param int $expire
+     * @return bool
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/1/4
+     */
+    protected function createLimit(string $key, int $initNum, int $expire)
+    {
+        $nowTime = time();
+
+        /** @var Redis $redis */
+        $redis = Cache::store('redis')->handler();
+
+        $script = <<<LUA
+    local key = KEYS[1]
+    local initNum = tonumber(ARGV[1])
+    local expire = tonumber(ARGV[2])
+    local nowTime = tonumber(ARGV[3])
+
+    local limitVal = redis.call('get', key)
+
+    if limitVal then
+        limitVal = cjson.decode(limitVal)
+        local newNum = math.min(initNum, (limitVal.num - 1) + ((initNum / expire) * (nowTime - limitVal.time)))
+        if newNum <= 0 then
+            return 0
+        else
+            local redisVal = {num = newNum, time = nowTime}
+            redis.call('set', key, cjson.encode(redisVal))
+            redis.call('expire', key, expire)
+            return 1
+        end
+    else
+        local redisVal = {num = initNum, time = nowTime}
+        redis.call('set', key, cjson.encode(redisVal))
+        redis.call('expire', key, expire)
+        return 1
+    end
+LUA;
+
+        $result = $redis->eval($script, [$key, $initNum, $expire, $nowTime], 1);
+        if (!$result) {
+            throw new ValidateException('访问频次过多!');
+        }
+
+        return true;
+    }
+}

+ 47 - 0
app/http/middleware/api/TradeMiddleware.php

@@ -0,0 +1,47 @@
+<?php
+/**
+ *  +----------------------------------------------------------------------
+ *  | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+ *  +----------------------------------------------------------------------
+ *  | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+ *  +----------------------------------------------------------------------
+ *  | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+ *  +----------------------------------------------------------------------
+ *  | Author: CRMEB Team <admin@crmeb.com>
+ *  +----------------------------------------------------------------------
+ */
+
+namespace app\http\middleware\api;
+
+
+use app\Request;
+use Closure;
+use Exception;
+use qiniu\interfaces\MiddlewareInterface;
+
+/**
+ * 交易类
+ * Class BlockerMiddleware
+ * @package app\http\middleware\api
+ */
+class TradeMiddleware implements MiddlewareInterface
+{
+
+    /**
+     * @param Request $request
+     * @param Closure $next
+     * @return mixed
+     * @throws Exception
+     */
+    public function handle(Request $request, Closure $next)
+    {
+        $user = $request->user();
+
+        $trade_password = $request->param('trade_password', '');
+        if (!$trade_password) return app('json')->fail('请输入交易密码');
+        if (!$user['trade_pwd']) return app('json')->fail('请先设置交易密码');
+        [$password,] = password($trade_password, $user['trade_salt']);
+        if ($password != $user['trade_pwd']) return app('json')->fail('交易密码错误');
+        return $next($request);
+    }
+}

+ 55 - 0
app/jobs/system/AdminLogJob.php

@@ -0,0 +1,55 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\jobs\system;
+
+use app\services\system\SystemLogServices;
+use qiniu\basic\BaseJobs;
+use qiniu\traits\QueueTrait;
+
+/**
+ * 后台日志
+ * Class AdminLogJob
+ * @package app\jobs\system
+ */
+class AdminLogJob extends BaseJobs
+{
+    use QueueTrait;
+
+    /**
+     * @return mixed
+     */
+    public static function queueName()
+    {
+        return 'qiniu_admin_log';
+    }
+
+    /**
+     * @param $adminId
+     * @param $adminName
+     * @param $module
+     * @param $rule
+     * @param $ip
+     * @param $type
+     * @return bool
+     */
+    public function doJob($adminId, $adminName, $module, $rule, $ip, $type, $params)
+    {
+        try {
+            /** @var SystemLogServices $services */
+            $services = app()->make(SystemLogServices::class);
+            $services->recordAdminLog((int)$adminId, $adminName, $module, $rule, $ip, $type, $params);
+        } catch (\Exception $e) {
+
+        }
+        return true;
+    }
+}

+ 58 - 0
app/jobs/system/ExportExcelJob.php

@@ -0,0 +1,58 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\jobs\system;
+
+use qiniu\basic\BaseJobs;
+use qiniu\traits\QueueTrait;
+use qiniu\services\SpreadsheetExcelService;
+use think\facade\Log;
+
+/**
+ * 导出数据队列
+ * Class ExportExcelJob
+ * @package app\jobs
+ */
+class ExportExcelJob extends BaseJobs
+{
+    use QueueTrait;
+
+    /**
+     * 分批导出excel
+     * @param $order
+     * @return bool
+     */
+    public function doJob(array $export = [], string $filename = '', array $header = [], array $title_arr = [], string $suffix = 'xlsx', bool $is_save = false)
+    {
+        if (!$export) {
+            return true;
+        }
+        try {
+            if ($header && $title_arr) {
+                $title = isset($title_arr[0]) && !empty($title_arr[0]) ? $title_arr[0] : '导出数据';
+                $name = isset($title_arr[1]) && !empty($title_arr[1]) ? $title_arr[1] : '导出数据';
+                $info = isset($title_arr[2]) && !empty($title_arr[2]) ? $title_arr[2] : date('Y-m-d H:i:s', time());
+                SpreadsheetExcelService::instance()
+                    ->setExcelHeader($header)
+                    ->setExcelTile($title, $name, $info)
+                    ->setExcelContent($export)
+                    ->excelSave($filename, $suffix, $is_save);
+            } else {
+                SpreadsheetExcelService::instance()
+                    ->setExcelContent($export)
+                    ->excelSave($filename, $suffix, $is_save);
+            }
+        } catch (\Throwable $e) {
+            Log::error('导出excel' . $title . '失败,原因:' . $e->getMessage());
+        }
+        return true;
+    }
+}

+ 30 - 0
app/listener/system/config/CreateSuccess.php

@@ -0,0 +1,30 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\listener\system\config;
+
+
+use qiniu\interfaces\ListenerInterface;
+
+/**
+ * 配置创建成功
+ * Class CreateSuccess
+ * @package app\listener\config
+ */
+class CreateSuccess implements ListenerInterface
+{
+
+    public function handle($event): void
+    {
+        [$data] = $event;
+        \qiniu\services\SystemConfigService::clear();
+    }
+}

+ 30 - 0
app/listener/system/config/DeleteSuccess.php

@@ -0,0 +1,30 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\listener\system\config;
+
+
+use qiniu\interfaces\ListenerInterface;
+
+/**
+ * 删除配置成功
+ * Class DeleteSuccess
+ * @package app\listener\config
+ */
+class DeleteSuccess implements ListenerInterface
+{
+
+    public function handle($event): void
+    {
+        [$id] = $event;
+        \qiniu\services\SystemConfigService::clear();
+    }
+}

+ 29 - 0
app/listener/system/config/StatusSuccess.php

@@ -0,0 +1,29 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\listener\system\config;
+
+
+use qiniu\interfaces\ListenerInterface;
+
+/**
+ * 修改状态
+ * Class StatusSuccess
+ * @package app\listener\config
+ */
+class StatusSuccess implements ListenerInterface
+{
+
+    public function handle($event): void
+    {
+        \qiniu\services\SystemConfigService::clear();
+    }
+}

+ 29 - 0
app/listener/user/CancelUser.php

@@ -0,0 +1,29 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\listener\user;
+
+use app\services\user\UserServices;
+use qiniu\interfaces\ListenerInterface;
+
+/**
+ * 注销用户事件
+ */
+class CancelUser implements ListenerInterface
+{
+    public function handle($event): void
+    {
+        [$uid] = $event;
+        /** @var UserServices $UserServices */
+        $UserServices = app()->make(UserServices::class);
+        $UserServices->cancelUser((int)$uid);
+    }
+}

+ 117 - 0
app/listener/wechat/OffcialAccountListener.php

@@ -0,0 +1,117 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\listener\wechat;
+
+use app\services\message\wechat\MessageServices;
+use app\services\system\QrcodeServices;
+use app\services\wechat\WechatMessageServices;
+use app\services\wechat\WechatReplyServices;
+use EasyWeChat\Kernel\Contracts\EventHandlerInterface;
+use EasyWeChat\Kernel\Messages\Image;
+use EasyWeChat\Kernel\Messages\News;
+use EasyWeChat\Kernel\Messages\Text;
+use EasyWeChat\Kernel\Messages\Transfer;
+use EasyWeChat\Kernel\Messages\Voice;
+use think\facade\Event;
+use think\facade\Log;
+
+/**
+ * 公众号消息处理事件
+ * Class OffcialAccountListener
+ * @package app\listener\wechat
+ */
+class OffcialAccountListener implements EventHandlerInterface
+{
+
+    /**
+     * 事件回调
+     * @param null $payload
+     * @return array|bool|Image|News|Text|Transfer|Voice|string|void
+     */
+    public function handle($payload = null)
+    {
+        try {
+            /** @var MessageServices $messageService */
+            $messageService = app()->make(MessageServices::class);
+            /** @var WechatReplyServices $wechatReplyService */
+            $wechatReplyService = app()->make(WechatReplyServices::class);
+            /** @var WechatMessageServices $wechatMessage */
+            $wechatMessage = app()->make(WechatMessageServices::class);
+            $spread_uid = 0;
+            if (isset($payload['Ticket'])) {
+                /** @var QrcodeServices $qrcodeService */
+                $qrcodeService = app()->make(QrcodeServices::class);
+                $qrInfo = $qrcodeService->getQrcode($payload['Ticket'], 'ticket');
+                if ($qrInfo && isset($qrInfo['third_id'])) $spread_uid = $qrInfo['third_id'];
+            }
+            $wechatMessage->wechatMessageBefore($payload, $spread_uid);
+            switch ($payload['MsgType']) {
+                case 'event':
+                    switch (strtolower($payload['Event'])) {
+                        case 'subscribe':
+                            $response = $messageService->wechatEventSubscribe($payload, $spread_uid);
+                            break;
+                        case 'unsubscribe':
+                            $messageService->wechatEventUnsubscribe($payload);
+                            break;
+                        case 'scan':
+                            $response = $messageService->wechatEventScan($payload);
+                            break;
+                        case 'location':
+                            $response = $messageService->wechatEventLocation($payload);
+                            break;
+                        case 'click':
+                            $response = $wechatReplyService->reply($payload['EventKey'], $payload['FromUserName']);
+                            break;
+                        case 'view':
+                            $response = $messageService->wechatEventView($payload);
+                            break;
+                        case 'user_get_card'://卡券领取
+                            $response = $messageService->wechatEventUserGetCard($payload);
+                            break;
+                        case 'submit_membercard_user_info'://卡券激活
+                            $response = $messageService->wechatEventSubmitMembercardUserInfo($payload);
+                            break;
+                        case 'user_del_card'://卡券删除
+                            $response = $messageService->wechatEventUserDelCard($payload);
+                            break;
+                    }
+                    break;
+                case 'text':
+                    $response = $wechatReplyService->reply($payload['Content'], $payload['FromUserName']);
+                    break;
+                case 'image':
+                    $response = $messageService->wechatMessageImage($payload);
+                    break;
+                case 'voice':
+                    $response = $messageService->wechatMessageVoice($payload);
+                    break;
+                case 'video':
+                    $response = $messageService->wechatMessageVideo($payload);
+                    break;
+                case 'location':
+                    $response = $messageService->wechatMessageLocation($payload);
+                    break;
+                case 'link':
+                    $response = $messageService->wechatMessageLink($payload);
+                    break;
+                // ... 其它消息
+                default:
+                    $response = $messageService->wechatMessageOther($payload);
+                    break;
+            }
+        } catch (\Throwable $e) {
+            \think\facade\Log::error(['title' => '微信消息服务端消息执行错误', 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
+        }
+        return $response ?? false;
+    }
+}

+ 28 - 0
app/listener/wechat/OpenPlatformListener.php

@@ -0,0 +1,28 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\listener\wechat;
+
+use EasyWeChat\Kernel\Contracts\EventHandlerInterface;
+
+/**
+ * 公众平台消息
+ * Class OpenPlatformListener
+ * @package app\listener\wechat
+ */
+class OpenPlatformListener implements EventHandlerInterface
+{
+
+    public function handle($payload = null)
+    {
+        // TODO: Implement handle() method.
+    }
+}

+ 94 - 0
app/listener/wechat/RoutineListener.php

@@ -0,0 +1,94 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\listener\wechat;
+
+use qiniu\services\wechat\MiniProgram;
+use EasyWeChat\Kernel\Contracts\EventHandlerInterface;
+use EasyWeChat\Kernel\Messages\Image;
+use EasyWeChat\Kernel\Messages\News;
+use EasyWeChat\Kernel\Messages\Text;
+use EasyWeChat\Kernel\Messages\Transfer;
+use EasyWeChat\Kernel\Messages\Voice;
+use think\facade\Event;
+use think\facade\Log;
+
+/**
+ * 小程序消息处理事件
+ * Class RoutineListener
+ * @package app\listener\wechat
+ */
+class RoutineListener implements EventHandlerInterface
+{
+
+    /**
+     * 事件回调
+     * @param null $payload
+     * @return array|bool|Image|News|Text|Transfer|Voice|string|void
+     */
+    public function handle($payload = null)
+    {
+        try {
+            switch ($payload['MsgType']) {
+                case 'event':
+                    switch (strtolower($payload['Event'])) {
+                        case 'funds_order_pay':
+                            $prefix = substr($payload['order_info']['trade_no'],0,2);
+                            //处理一下参数
+                            switch ($prefix) {
+								case 'wx':
+                                case 'cp':
+                                    $data['attach'] = 'Product';
+                                    break;
+                                case 'hy':
+                                    $data['attach'] = 'Member';
+                                    break;
+                                case 'cz':
+                                    $data['attach'] = 'UserRecharge';
+                                    break;
+                            }
+                            $data['out_trade_no'] = $payload['order_info']['trade_no'];
+                            $data['transaction_id'] = $payload['order_info']['transaction_id'];
+                            $data['opneid'] = $payload['FromUserName'];
+                            if (Event::until('pay.notify', [$data])) {
+                                $response = 'success';
+                            } else {
+                                $response = 'faild';
+                            }
+                            Log::error(['data'=>$data,'res'=>$response,'message'=>$payload]);
+                            break;
+						case 'trade_manage_remind_access_api':  // 小程序完成账期授权时  小程序产生第一笔交易时 已产生交易但从未发货的小程序,每天一次
+                            break;
+                        case 'trade_manage_remind_shipping':   // 曾经发过货的小程序,订单超过48小时未发货时
+                            break;
+                        case 'trade_manage_order_settlement':     // 订单完成发货时  订单结算时
+                            if (isset($payload['estimated_settlement_time'])) { //订单完成发货时
+                                MiniProgram::notifyConfirmByTradeNo($payload['merchant_trade_no'], time());
+                            }
+                            if (isset($payload['confirm_receive_method'])) {  // 订单结算时
+                                /** @var StoreOrderTakeServices $StoreOrderTakeServices */
+                                $storeOrderTakeServices = app()->make(StoreOrderTakeServices::class);
+                                $storeOrderTakeServices->miniOrderTakeOrder($payload['merchant_trade_no']);
+                            }
+                            break;
+                    }
+                    break;
+                // ... 其它消息
+                default:
+
+                    break;
+            }
+        } catch (\Throwable $e) {
+            \think\facade\Log::error(['title' => '微信消息服务端消息执行错误', 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
+        }
+        return $response ?? false;
+    }
+}

+ 242 - 0
app/listener/wechat/WorkListener.php

@@ -0,0 +1,242 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\listener\wechat;
+
+use EasyWeChat\Kernel\Contracts\EventHandlerInterface;
+
+/**
+ * 企业微信服务消息处理
+ * Class WorkListener
+ * @package app\listener\wechat
+ */
+class WorkListener implements EventHandlerInterface
+{
+
+    public function handle($payload = null)
+    {
+        $response = null;
+        switch ($payload['MsgType']) {
+            case 'event':
+                switch ($payload['Event']) {
+                    case 'change_contact'://通讯录事件
+                        $this->changeContactEvent($payload);
+                        break;
+                    case 'change_external_chat'://客户群事件
+                        $this->changeExternalChatEvent($payload);
+                        break;
+                    case 'change_external_contact'://客户事件
+                        $this->externalContactEvent($payload);
+                        break;
+                    case 'change_external_tag'://客户标签事件
+                        $this->changeExternalTagEvent($payload);
+                        break;
+                    case 'batch_job_result'://异步任务完成通知
+                        $this->batchJobResultEvent($payload);
+                        break;
+                }
+                break;
+            case 'text'://文本消息
+                break;
+            case 'image'://图片消息
+                break;
+            case 'voice'://语音消息
+                break;
+            case 'video'://视频消息
+                break;
+            case 'news'://图文消息
+                break;
+            case 'update_button'://模板卡片更新消息
+                break;
+            case 'update_template_card'://更新点击用户的整张卡片
+                break;
+        }
+        return $response;
+    }
+
+
+    public function batchJobResultEvent(array $payload)
+    {
+        switch ($payload['JobType']) {
+            case 'sync_user'://增量更新成员
+                break;
+            case 'replace_user'://全量覆盖成员
+                break;
+            case 'invite_user'://邀请成员关注
+                break;
+            case 'replace_party'://全量覆盖部门
+                break;
+        }
+    }
+
+    /**
+     * 企业微信通讯录事件
+     * @param array $payload
+     * @return null
+     */
+    public function changeContactEvent(array $payload)
+    {
+        $response = null;
+        try {
+            switch ($payload['ChangeType']) {
+                case 'create_user'://新增成员事件
+                    /** @var WorkMemberServices $make */
+                    $make = app()->make(WorkMemberServices::class);
+                    $make->createMember($payload);
+                    break;
+                case 'update_user'://更新成员事件
+                    /** @var WorkMemberServices $make */
+                    $make = app()->make(WorkMemberServices::class);
+                    $make->updateMember($payload);
+                    break;
+                case 'delete_user'://删除成员事件
+                    /** @var WorkMemberServices $make */
+                    $make = app()->make(WorkMemberServices::class);
+                    $make->deleteMember($payload['ToUserName'], $payload['UserID']);
+                    break;
+                case 'create_party'://新增部门事件
+                    /** @var WorkDepartmentServices $make */
+                    $make = app()->make(WorkDepartmentServices::class);
+                    $make->createDepartment($payload);
+                    break;
+                case 'update_party'://更新部门事件
+                    /** @var WorkDepartmentServices $make */
+                    $make = app()->make(WorkDepartmentServices::class);
+                    $make->updateDepartment($payload['ToUserName'], (int)$payload['Id'], '');
+                    break;
+                case 'delete_party'://删除部门事件
+                    /** @var WorkDepartmentServices $make */
+                    $make = app()->make(WorkDepartmentServices::class);
+                    $make->deleteDepartment($payload['ToUserName'], (int)$payload['Id']);
+                    break;
+                case 'update_tag'://标签成员变更事件
+
+                    break;
+            }
+        } catch (\Throwable $e) {
+            \think\facade\Log::error([
+                'message' => '企业微信通讯录事件发生错误:' . $e->getMessage(),
+                'payload' => $payload,
+                'file' => $e->getFile(),
+                'line' => $e->getLine()
+            ]);
+        }
+        return $response;
+    }
+
+    /**
+     * 客户事件
+     * @param array $payload
+     * @return |null
+     */
+    public function externalContactEvent(array $payload)
+    {
+        $response = null;
+        try {
+            switch ($payload['ChangeType']) {
+                case 'add_external_contact'://添加企业客户事件
+                    /** @var WorkClientServices $make */
+                    $make = app()->make(WorkClientServices::class);
+                    $make->createClient($payload);
+                    break;
+                case 'edit_external_contact'://编辑企业客户事件
+                    /** @var WorkClientServices $make */
+                    $make = app()->make(WorkClientServices::class);
+                    $make->updateClient($payload);
+                    break;
+                case 'del_external_contact':
+                    /** @var WorkClientServices $make */
+                    $make = app()->make(WorkClientServices::class);
+                    $make->deleteClient($payload);
+                    break;
+                case 'add_half_external_contact'://外部联系人免验证添加成员事件
+                    break;
+                case 'del_follow_user'://删除跟进成员事件
+                    /** @var WorkClientServices $make */
+                    $make = app()->make(WorkClientServices::class);
+                    $make->deleteFollowClient($payload);
+                    break;
+                case 'transfer_fail'://客户接替失败事件
+                    break;
+            }
+        } catch (\Throwable $e) {
+            \think\facade\Log::error([
+                'message' => '客户事件发生错误:' . $e->getMessage(),
+                'payload' => $payload,
+                'file' => $e->getFile(),
+                'line' => $e->getLine()
+            ]);
+        }
+        return $response;
+    }
+
+    /**
+     * 客户群事件
+     * @param array $payload
+     */
+    public function changeExternalChatEvent(array $payload)
+    {
+        try {
+            switch ($payload['ChangeType']) {
+                case 'create'://客户群创建事件
+                    /** @var WorkGroupChatServices $make */
+                    $make = app()->make(WorkGroupChatServices::class);
+                    $make->saveWorkGroupChat($payload['ToUserName'], $payload['ChatId']);
+                    break;
+                case 'update'://客户群变更事件
+                    /** @var WorkGroupChatServices $make */
+                    $make = app()->make(WorkGroupChatServices::class);
+                    $make->updateGroupChat($payload);
+                    break;
+                case 'dismiss'://客户群解散事件
+                    /** @var WorkGroupChatServices $make */
+                    $make = app()->make(WorkGroupChatServices::class);
+                    $make->dismissGroupChat($payload['ToUserName'], $payload['ChatId']);
+                    break;
+            }
+        } catch (\Throwable $e) {
+            \think\facade\Log::error([
+                'message' => $e->getMessage(),
+                'payload' => $payload,
+                'file' => $e->getFile(),
+                'line' => $e->getLine()
+            ]);
+        }
+    }
+
+
+    /**
+     * 客户标签事件
+     * @param array $payload
+     */
+    public function changeExternalTagEvent(array $payload)
+    {
+        switch ($payload['ChangeType']) {
+            case 'create'://企业客户标签创建事件
+                /** @var UserLabelServices $make */
+                $make = app()->make(UserLabelServices::class);
+                $make->createUserLabel($payload['ToUserName'], $payload['Id'], $payload['TagType']);
+                break;
+            case 'update'://企业客户标签变更事件
+                /** @var UserLabelServices $make */
+                $make = app()->make(UserLabelServices::class);
+                $make->updateUserLabel($payload['ToUserName'], $payload['Id'], $payload['TagType']);
+                break;
+            case 'delete'://企业客户标签删除事件
+                /** @var UserLabelServices $make */
+                $make = app()->make(UserLabelServices::class);
+                $make->deleteUserLabel($payload['ToUserName'], $payload['Id'], $payload['TagType']);
+                break;
+            case 'shuffle'://企业客户标签重排事件
+                break;
+        }
+    }
+}

+ 10 - 0
app/middleware.php

@@ -0,0 +1,10 @@
+<?php
+// 全局中间件定义文件
+return [
+    // 全局请求缓存
+    // \think\middleware\CheckRequestCache::class,
+    // 多语言加载
+    // \think\middleware\LoadLangPack::class,
+    // Session初始化
+    // \think\middleware\SessionInit::class
+];

+ 90 - 0
app/model/system/CityArea.php

@@ -0,0 +1,90 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\system;
+
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\model\relation\HasMany;
+
+/**
+ * 城市数据(包含街道)
+ * Class CityArea
+ * @package app\model\other
+ */
+class CityArea extends BaseModel
+{
+    use ModelTrait;
+
+    /**
+     * @var string
+     */
+    protected $name = 'city_area';
+
+    /**
+     * @var string
+     */
+    protected $key = 'id';
+
+    protected $order = 'id asc';
+
+    /**
+     * @return HasMany
+     */
+    public function children()
+    {
+        return $this->hasMany(self::class, 'parent_id', 'id');
+    }
+
+
+    /**
+     * @param array $where
+     * @return \think\Model|null
+     */
+    public function search(array $where = [])
+    {
+        return parent::search($where)->when(isset($where['pid']) && $where['pid'] !== '', function ($query) use ($where) {
+            $query->where('parent_id', $where['pid']);
+        })->when(isset($where['address']) && $where['address'] !== '', function ($query) use ($where) {
+            $address = explode('/', trim($where['address'], '/'));
+            if (isset($address[0]) && isset($address[1]) && $address[0] == $address[1]) {//直辖市:北京市北京市朝阳区
+                array_shift($address);
+            }
+            $p = array_shift($address);
+            if (mb_strlen($p) - 1 === mb_strpos($p, '市')) {
+                $p = mb_substr($p, 0, -1);
+            } elseif (mb_strlen($p) - 1 === mb_strpos($p, '省')) {
+                $p = mb_substr($p, 0, -1);
+            } elseif (mb_strlen($p) - 3 === mb_strpos($p, '自治区')) {
+                $p = mb_substr($p, 0, -3);
+            }
+            $pcity = $this->getModel()->where('name', $p)->value('id');
+            $path = ['', $pcity];
+            $street = $p;
+            $i = 0;
+            foreach ($address as $item) {
+                //县级市,只有三级地址;市和县相同
+                if ($item == ($address[$i-1] ?? '')) continue;
+                $pcity = $this->getModel()->whereLike('path', implode('/', $path) . '/%')->where('name', $item)->value('id');
+                if (!$pcity) {
+                    break;
+                }
+                $path[] = $pcity;
+                $street = $item;
+                $i++;
+            }
+            array_pop($path);
+            $query->whereLike('path', implode('/', $path) . '/%')->where('name', $street);
+        });
+
+    }
+}

+ 121 - 0
app/model/system/Qrcode.php

@@ -0,0 +1,121 @@
+<?php
+declare (strict_types = 1);
+
+namespace app\model\system;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+
+
+/**
+ * 微信二维码管理模型
+ * Class Qrcode
+ * @package app\model\system
+ */
+class Qrcode extends BaseModel
+{
+
+    use ModelTrait;
+    
+
+
+    // 表名
+    protected $name = 'qrcode';
+
+    //主键
+    protected $pk = 'id';
+
+    //隐藏属性
+    protected $hidden = [
+    
+    ];
+
+    // 追加属性
+    protected $append = [
+        'status_chs',
+	    'type_chs'
+    ];
+
+    public function getStatusList()
+    {
+        return ['1' => '有效', '0' => '无效'];
+    }
+
+    public function getTypeList()
+    {
+        return ['1' => '小程序', '2' => '公众号', '3' => 'H5'];
+    }
+
+
+    public function getStatusChsAttr($value, $data)
+    {
+        $value = $value ? $value : (isset($data['status']) ? $data['status'] : '');
+        $list = $this->getStatusList();
+        return isset($list[$value]) ? $list[$value] : '';
+    }
+
+
+    protected function setAddTimeAttr($value)
+    {
+        return $value?:date('Y-m-d H:i:s',time());
+    }
+
+
+    public function getTypeChsAttr($value, $data)
+    {
+        $value = $value ? $value : (isset($data['type']) ? $data['type'] : '');
+        $list = $this->getTypeList();
+        return isset($list[$value]) ? $list[$value] : '';
+    }
+
+
+    /**
+     * type 搜索器
+     * @param $query
+     * @param $value
+     */
+    public function searchTypeAttr($query, $value)
+    {
+        if ($value != '') {
+            $query->whereLike('type', $value);
+        }
+    }
+
+    /**
+     * status 搜索器
+     * @param $query
+     * @param $value
+     */
+    public function searchStatusAttr($query, $value)
+    {
+        if ($value != '') {
+            $query->whereLike('status', $value);
+        }
+    }
+
+    /**
+     * third_type 搜索器
+     * @param $query
+     * @param $value
+     */
+    public function searchThirdTypeAttr($query, $value)
+    {
+        if ($value != '') {
+            $query->whereLike('third_type', $value);
+        }
+    }
+
+    /**
+     * third_id 搜索器
+     * @param $query
+     * @param $value
+     */
+    public function searchThirdIdAttr($query, $value)
+    {
+        if ($value != '') {
+            $query->whereLike('third_id', $value);
+        }
+    }
+
+
+}

+ 88 - 0
app/model/system/SmsRecord.php

@@ -0,0 +1,88 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\system;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\Model;
+
+/**
+ * 短信记录Model
+ * Class SmsRecord
+ * @package app\model\message\sms
+ */
+class SmsRecord extends BaseModel
+{
+    use ModelTrait;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'sms_record';
+
+
+    protected function setAddTimeAttr()
+    {
+        return time();
+    }
+
+    /**
+     * 时间获取器
+     * @param $value
+     * @return false|string
+     */
+    protected function getAddTimeAttr($value)
+    {
+        return $value ? date('Y-m-d H:i:s', $value) : '';
+    }
+
+    /**
+     * 电话号码搜索器
+     * @param Model $query
+     * @param $value
+     * @param $data
+     */
+    public function searchPhoneAttr($query, $value)
+    {
+        $query->where('phone', $value);
+    }
+
+    /**
+     * uid搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchUidAttr($query, $value)
+    {
+        if ($value) {
+            $query->where('uid', $value);
+        }
+    }
+
+    /**
+     * ip
+     * @param Model $query
+     * @param $value
+     * @param $data
+     */
+    public function searchAddIpAttr($query, $value)
+    {
+        $query->where('add_ip', $value);
+    }
+}

+ 98 - 0
app/model/system/SystemLog.php

@@ -0,0 +1,98 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\system;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\Model;
+
+/**
+ * 日志模型
+ * Class SystemLog
+ * @package app\model\system\log
+ */
+class SystemLog extends BaseModel
+{
+    use ModelTrait;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'system_log';
+
+    protected $auto_expire = '3 month';
+
+    protected function setAddTimeAttr()
+    {
+        return time();
+    }
+
+    /**
+     * @param $query
+     * @param $value
+     */
+    public function searchStoreIdAttr($query, $value)
+    {
+        if ($value !== '') $query->where('store_id', $value);
+    }
+
+    /**
+     * 访问方式搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchPagesAttr($query, $value)
+    {
+        $query->whereLike('page', '%' . $value . '%');
+    }
+
+    /**
+     * 访问路径搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchPathAttr($query, $value)
+    {
+        $query->whereLike('path', '%' . $value . '%');
+    }
+
+    /**
+     * ip搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchIpAttr($query, $value)
+    {
+        $query->where('ip', 'LIKE', "%$value%");
+    }
+
+    /**
+     * 管理员id搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchAdminIdAttr($query, $value)
+    {
+        $query->whereIn('admin_id', $value);
+    }
+
+}

+ 158 - 0
app/model/system/admin/SystemAdmin.php

@@ -0,0 +1,158 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\system\admin;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\JwtAuthModelTrait;
+use qiniu\traits\ModelTrait;
+use think\Model;
+use think\model\concern\SoftDelete;
+
+/**
+ * 管理员模型
+ * Class SystemAdmin
+ * @package app\model\system\admin
+ */
+class SystemAdmin extends BaseModel
+{
+    use JwtAuthModelTrait;
+    use ModelTrait;
+    use SoftDelete;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'system_admin';
+
+    protected $order = 'id asc';
+
+    protected function setAddTimeAttr()
+    {
+        return time();
+    }
+
+    /**
+     * 权限数据
+     * @param $value
+     * @return false|string[]
+     */
+    public static function getRolesAttr($value)
+    {
+        return explode(',', $value);
+    }
+
+    /**
+     * 管理员级别搜索器
+     * @param Model $query
+     * @param $value
+     * @param $data
+     */
+    public function searchLevelAttr($query, $value)
+    {
+        if (is_array($value)) {
+            $query->where('level', $value[0], $value[1]);
+        } else {
+            $query->where('level', $value);
+        }
+    }
+
+    /**
+     * 管理员账号和姓名搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchAccountLikeAttr($query, $value)
+    {
+        if ($value) {
+            $query->whereLike('account|real_name', '%' . $value . '%');
+        }
+    }
+
+    /**
+     * 管理员账号搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchAccountAttr($query, $value)
+    {
+        if ($value) {
+            $query->where('account', $value);
+        }
+    }
+
+    /**
+     * 管理员电话搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchPhoneAttr($query, $value)
+    {
+        if ($value != '') {
+            $query->where('phone', $value);
+        }
+    }
+
+    /**
+     * 管理员权限搜索器
+     * @param Model $query
+     * @param $roles
+     */
+    public function searchRolesAttr($query, $roles)
+    {
+        if ($roles) {
+            $query->where("CONCAT(',',roles,',')  LIKE '%,$roles,%'");
+        }
+    }
+    /**
+     * 状态搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchStatusAttr($query, $value)
+    {
+        if ($value != '' && $value != null) {
+            $query->where('status', $value);
+        }
+    }
+
+    /**
+     * 关联ID搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchRelationIdAttr($query, $value)
+    {
+        if ($value != '' && $value != null) {
+            $query->where('relation_id', $value);
+        }
+    }
+
+    /**
+     * 状态搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchAdminTypeAttr($query, $value)
+    {
+        if ($value != '' && $value != null) {
+            $query->where('admin_type', $value);
+        }
+    }
+
+}

+ 247 - 0
app/model/system/admin/SystemMenus.php

@@ -0,0 +1,247 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\system\admin;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\Model;
+use think\model\concern\SoftDelete;
+
+/**
+ * 菜单规则模型
+ * Class SystemMenus
+ * @package app\model\system
+ */
+class SystemMenus extends BaseModel
+{
+    use ModelTrait;
+    use SoftDelete;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'system_menus';
+
+    protected $hidden = ['delete_time'];
+
+    protected $order = 'id asc';
+
+
+    public function button()
+    {
+        return $this->hasMany(self::class, 'pid', 'id')->where('auth_type', 3)->where('is_show', 1);
+    }
+
+    /**
+     * 参数修改器
+     * @param $value
+     * @return false|string
+     */
+    public function setParamsAttr($value)
+    {
+        $value = $value ? explode('/', $value) : [];
+        $params = array_chunk($value, 2);
+        $data = [];
+        foreach ($params as $param) {
+            if (isset($param[0]) && isset($param[1])) $data[$param[0]] = $param[1];
+        }
+        return json_encode($data);
+    }
+
+    /**
+     * 参数获取器
+     * @param $_value
+     * @return mixed
+     */
+    public function getParamsAttr($_value)
+    {
+        return is_string($_value) ? json_decode($_value, true) : $_value;
+    }
+
+    /**
+     * pid获取器
+     * @param $value
+     * @return mixed|string
+     */
+    public function getPidAttr($value)
+    {
+        return !$value ? '顶级' : $this->where('pid', $value)->value('menu_name');
+    }
+
+    /**
+     * 默认条件查询器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchDefaultAttr($query)
+    {
+        $query->where(['is_show' => 1]);
+    }
+
+    /**
+     * 是否显示搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchIsShowAttr($query, $value)
+    {
+        if ($value != '') {
+            $query->where('is_show', $value);
+        }
+    }
+
+    /**
+     * Pid搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchPidAttr($query, $value)
+    {
+        $query->where('pid', $value ?? 0);
+    }
+
+    /**
+     * type搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchTypeAttr($query, $value)
+    {
+        if ($value) $query->where('type', $value);
+    }
+
+    /**
+     * 规格搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchRuleAttr($query, $value)
+    {
+        $query->whereIn('id', $value)->whereOr('pid', 0);
+    }
+
+    /**
+     * 搜索菜单
+     * @param Model $query
+     * @param $value
+     */
+    public function searchKeywordAttr($query, $value)
+    {
+        if ($value != '') {
+            $query->whereLike('menu_name|menu_path|unique_auth|api_url|id|pid', "%$value%");
+        }
+    }
+
+    /**
+     * 方法搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchActionAttr($query, $value)
+    {
+        $query->where('action', $value);
+    }
+
+    /**
+     * 控制器搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchControllerAttr($query, $value)
+    {
+        $query->where('controller', lcfirst($value));
+    }
+
+    /**
+     * 访问地址搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchUrlAttr($query, $value)
+    {
+        $query->where('api_url', $value);
+    }
+
+    /**
+     * 参数搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchParamsAttr($query, $value)
+    {
+        $query->where(function ($query) use ($value) {
+            $query->where('params', $value)->whereOr('params', "'[]'");
+        });
+    }
+
+    /**
+     * 权限标识搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchUniqueAttr($query, $value)
+    {
+        if ($value) {
+            $query->whereIn('id', $value);
+        }
+    }
+
+    /**
+     * 菜单规格搜索
+     * @param Model $query
+     * @param $value
+     */
+    public function searchRouteAttr($query, $value)
+    {
+        $query->where('auth_type', 1)->where('is_show', 1);
+        if ($value) {
+            $query->whereIn('id', $value);
+        }
+    }
+
+    /**
+     * Id搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchIdAttr($query, $value)
+    {
+        $query->whereIn('id', $value);
+    }
+
+    /**
+     * is_show_path
+     * @param Model $query
+     * @param $value
+     */
+    public function searchIsShowPathAttr($query, $value)
+    {
+        $query->where('is_show_path', $value);
+    }
+
+    /**
+     * auth_type
+     * @param Model $query
+     * @param $value
+     */
+    public function searchAuthTypeAttr($query, $value)
+    {
+        $query->where('auth_type', $value);
+    }
+}

+ 146 - 0
app/model/system/admin/SystemRole.php

@@ -0,0 +1,146 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\system\admin;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\Model;
+
+/**
+ * 管理员权限规则
+ * Class SystemRole
+ * @package app\model\system\admin
+ */
+class SystemRole extends BaseModel
+{
+    use ModelTrait;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'system_role';
+
+    protected $order = 'id asc';
+
+
+    public function __construct(array $data = [])
+    {
+        parent::__construct($data);
+    }
+
+    /**
+     * 规则修改器
+     * @param Model $value
+     * @return string
+     */
+    public static function setRulesAttr($value)
+    {
+        return is_array($value) ? implode(',', $value) : $value;
+    }
+
+    /**
+     * 商户搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchTypeAttr($query, $value)
+    {
+        if (is_array($value)) {
+            if ($value) $query->whereIn('type', $value);
+        } else {
+            if ($value !== '') $query->where('type', $value);
+        }
+    }
+
+    /**
+     * 关联门店ID搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchRelationIdAttr($query, $value)
+    {
+        if (is_array($value)) {
+            if ($value) $query->whereIn('relation_id', $value);
+        } else {
+            if ($value !== '') $query->where('relation_id', $value);
+        }
+    }
+
+    /**
+     * 门店
+     * @param Model $query
+     * @param $value
+     */
+    public function searchStoreIdAttr($query, $value)
+    {
+        if (is_array($value)) {
+            if ($value) $query->whereIn('relation_id', $value)->where('type', 1);
+        } else {
+            if ($value !== '') $query->where('relation_id', $value)->where('type', 1);
+        }
+    }
+
+    /**
+     * 权限规格状态搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchStatusAttr($query, $value)
+    {
+        if ($value != '') {
+            $query->where('status', $value);
+        }
+    }
+
+    /**
+     * 权限等级搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchLevelAttr($query, $value)
+    {
+        $query->where('level', $value);
+    }
+
+    /**
+     * id搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchIdAttr($query, $value)
+    {
+        if (is_array($value)) {
+            $query->whereIn('id', $value);
+        } else {
+            $query->where('id', $value);
+        }
+    }
+
+    /**
+     * 身份管理搜索
+     * @param Model $query
+     * @param $value
+     */
+    public function searchRoleNameAttr($query, $value)
+    {
+        if ($value) {
+            $query->whereLike('role_name', '%' . $value . '%');
+        }
+    }
+}

+ 120 - 0
app/model/system/attachment/SystemAttachment.php

@@ -0,0 +1,120 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\system\attachment;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\Model;
+
+/**
+ * 附件管理模型
+ * Class SystemAttachment
+ * @package app\model\system\attachment
+ */
+class SystemAttachment extends BaseModel
+{
+    use ModelTrait;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'att_id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'system_attachment';
+
+    /**
+     * 图片类型搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchModuleTypeAttr($query, $value)
+    {
+        $query->where('module_type', $value ?: 1);
+    }
+
+    /**
+     * pid搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchPidAttr($query, $value)
+    {
+        if ($value) $query->where('pid', $value);
+    }
+
+    /**
+     * name模糊搜索
+     * @param Model $query
+     * @param $value
+     */
+    public function searchLikeNameAttr($query, $value)
+    {
+        if ($value) $query->where('name', 'LIKE', "$value%");
+    }
+
+    /**
+     * type搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchTypeAttr($query, $value)
+    {
+        if (is_array($value)) {
+            if ($value) $query->whereIn('type', $value);
+        } else {
+            if ($value !== '') $query->where('type', $value);
+        }
+    }
+
+    /**
+     * 关联门店ID、供应商ID搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchRelationIdAttr($query, $value)
+    {
+        if (is_array($value)) {
+            if ($value) $query->whereIn('relation_id', $value);
+        } else {
+            if ($value !== '') $query->where('relation_id', $value);
+        }
+    }
+
+    /**
+     * FileType搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchFileTypeAttr($query, $value)
+    {
+        if (is_array($value)) {
+            if ($value) $query->whereIn('file_type', $value);
+        } else {
+            if ($value !== '') $query->where('file_type', $value);
+        }
+    }
+
+    /**
+     * store_id搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchStoreIdAttr($query, $value)
+    {
+        if ($value !== '') $query->where('relation_id', $value)->where('type', 1);
+    }
+}

+ 110 - 0
app/model/system/attachment/SystemAttachmentCategory.php

@@ -0,0 +1,110 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\system\attachment;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\Model;
+
+/**
+ * 附件管理分类模型
+ * Class SystemAttachmentCategory
+ * @package app\model\system\attachment
+ */
+class SystemAttachmentCategory extends BaseModel
+{
+    use ModelTrait;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'system_attachment_category';
+
+    protected $order = 'id asc';
+
+
+    /**
+     * 附件分类昵称搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchNameAttr($query, $value)
+    {
+        if ($value !== '') $query->where('name', 'like', '%' . $value . '%');
+    }
+
+    /**
+     * pid搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchPidAttr($query, $value)
+    {
+        if ($value !== '') $query->where('pid', $value);
+    }
+
+    /**
+     * type搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchTypeAttr($query, $value)
+    {
+        if ($value) $query->where('type', $value);
+    }
+
+	/**
+     * 关联门店ID、供应商ID搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchRelationIdAttr($query, $value)
+    {
+		if (is_array($value)) {
+			if ($value) $query->whereIn('relation_id', $value);
+		} else {
+			if ($value !== '') $query->where('relation_id', $value);
+		}
+    }
+
+	/**
+     * FileType搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchFileTypeAttr($query, $value)
+    {
+		if (is_array($value)) {
+			if ($value) $query->whereIn('file_type', $value);
+		} else {
+			if ($value !== '') $query->where('file_type', $value);
+		}
+    }
+
+    /**
+     * store_id搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchStoreIdAttr($query, $value)
+    {
+        if ($value !== '') $query->where('relation_id', $value)->where('type', 1);
+    }
+
+}

+ 109 - 0
app/model/system/config/SystemConfig.php

@@ -0,0 +1,109 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\system\config;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\Model;
+use think\model\relation\HasOne;
+
+/**
+ * 系统配置模型
+ * Class SystemConfig
+ * @package app\model\system\config
+ */
+class SystemConfig extends BaseModel
+{
+    use ModelTrait;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'system_config';
+
+    protected $order = 'id asc';
+
+    /**
+     * 一对一关联门店配置表
+     * @return HasOne
+     */
+    public function storeConfig()
+    {
+        return $this->hasOne(SystemStoreConfig::class, 'key_name', 'menu_name')->field(['key_name', 'store_id', 'value'])->bind([
+            'store_value' => 'value',
+        ]);
+    }
+
+    /**
+     * 菜单名搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchMenuNameAttr($query, $value)
+    {
+        if (is_array($value)) {
+            $query->whereIn('menu_name', $value);
+        } else {
+            $query->where('menu_name', $value);
+        }
+    }
+
+    /**
+     * tab id 搜索
+     * @param Model $query
+     * @param $value
+     */
+    public function searchTabIdAttr($query, $value)
+    {
+        $query->where('config_tab_id', $value);
+    }
+
+    /**
+     * 状态搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchStatusAttr($query, $value)
+    {
+        $query->where('status', $value ?: 1);
+    }
+
+    /**
+     * value搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchValueAttr($query, $value)
+    {
+        $query->where('value', $value);
+    }
+
+
+    /**
+     * is_store 搜索
+     * @param $query
+     * @param $value
+     */
+    public function searchIsStoreAttr($query, $value)
+    {
+        if ($value) {
+            $query->where('is_store', $value);
+        }
+    }
+}

+ 88 - 0
app/model/system/config/SystemConfigTab.php

@@ -0,0 +1,88 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\system\config;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\Model;
+
+/**
+ * 配置分类模型
+ * Class SystemConfigTab
+ * @package app\model\system\config
+ */
+class SystemConfigTab extends BaseModel
+{
+    use ModelTrait;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'system_config_tab';
+
+    protected $order = 'id asc';
+
+    /**
+     * 状态搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchStatusAttr($query, $value)
+    {
+        if ($value != '') {
+            $query->where('status', $value);
+        }
+    }
+
+    /**
+     * pid搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchPidAttr($query, $value)
+    {
+        if (is_array($value)) {
+            $query->whereIn('pid', $value);
+        } else {
+            $value && $query->where('pid', $value);
+        }
+    }
+
+    /**
+     * 分类名称搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchTitleAttr($query, $value)
+    {
+        $query->whereLike('title', '%' . $value . '%');
+    }
+
+    /**
+     * @param $query
+     * @param $value
+     */
+    public function searchIsStoreAttr($query, $value)
+    {
+        if ($value !== '') {
+            $query->where('is_store', $value);
+        }
+    }
+
+}

+ 67 - 0
app/model/system/config/SystemStorage.php

@@ -0,0 +1,67 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\system\config;
+
+
+use qiniu\basic\BaseModel;
+
+/**
+ * 云存储
+ * Class SystemStorage
+ * @package app\model\system\config
+ */
+class SystemStorage extends BaseModel
+{
+
+    /**
+     * @var string
+     */
+    protected $name = 'system_storage';
+
+    /**
+     * @var string
+     */
+    protected $pk = 'id';
+
+
+    protected $order = 'id asc';
+
+    /**
+     * @param $query
+     * @param $value
+     */
+    public function searchNameAttr($query, $value)
+    {
+        $query->where('name', $value);
+    }
+
+    /**
+     * @param $query
+     * @param $value
+     */
+    public function searchTypeAttr($query, $value)
+    {
+        if ($value !== '') {
+            $query->where('type', $value);
+        }
+    }
+
+	/**
+	 * 状态搜索器
+	 * @param $query
+	 * @param $value
+	 */
+	public function searchStatusAttr($query, $value)
+	{
+		if ($value !== '') $query->where('status', $value);
+	}
+}

+ 71 - 0
app/model/system/config/SystemStoreConfig.php

@@ -0,0 +1,71 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\system\config;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\Model;
+
+/**
+ * 门店配置
+ * Class StoreConfig
+ * @package app\model\store
+ */
+class SystemStoreConfig extends BaseModel
+{
+    use ModelTrait;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'system_store_config';
+
+    protected $order = 'id asc';
+
+    /**
+     * 门店
+     * @param Model $query
+     * @param $value
+     */
+    public function searchStoreIdAttr($query, $value)
+    {
+        if (is_array($value)) {
+            if ($value) $query->whereIn('store_id', $value);
+        } else {
+            if ($value !== '') $query->where('store_id', $value);
+        }
+    }
+
+    /**
+     * 关键字
+     * @param $query
+     * @param $value
+     */
+    public function searchKeyNameAttr($query, $value)
+    {
+        if ($value) {
+            if (is_array($value))
+                $query->whereIn('key_name', $value);
+            else
+                $query->where('key_name', $value);
+        }
+    }
+
+
+}

+ 83 - 0
app/model/system/config/SystemUserLevel.php

@@ -0,0 +1,83 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\system\config;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\Model;
+use think\model\concern\SoftDelete;
+
+/**
+ * 系统等级设置模型
+ * Class SystemUserLevel
+ * @package app\model\user\level
+ */
+class SystemUserLevel extends BaseModel
+{
+    use ModelTrait;
+    use SoftDelete;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'system_user_level';
+
+    protected $order = 'grade asc,id asc';
+
+    /**
+     * 时间获取器
+     * @param $value
+     * @return false|string
+     */
+    public function getAddTimeAttr($value)
+    {
+        return date('Y-m-d H:i:s', (int)$value);
+    }
+
+    public function setTaskAttr($value)
+    {
+        return json_encode($value);
+    }
+
+    public function getTaskAttr($value)
+    {
+        return json_decode($value, true);
+    }
+
+
+    /**
+     * 是否展示
+     * @param \think\Model $query
+     * @param $value
+     */
+    public function searchIsShowAttr($query, $value)
+    {
+        $query->where('is_show', $value);
+    }
+
+    /**
+     * @param Model $query
+     * @param $value
+     */
+    public function searchTitleAttr($query, $value)
+    {
+        $query->where('title', 'LIKE', "%$value%");
+    }
+
+}

+ 287 - 0
app/model/user/User.php

@@ -0,0 +1,287 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\user;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\Model;
+use think\model\concern\SoftDelete;
+
+/**
+ * Class User
+ * @package app\model\user
+ */
+class User extends BaseModel
+{
+    use ModelTrait;
+    use SoftDelete;
+
+    /**
+     * @var string
+     */
+    protected $pk = 'uid';
+
+    protected $name = 'user';
+
+    protected $hidden = [
+        'add_ip', 'account', 'clean_time', 'last_ip', 'pwd', 'salt', 'trade_pwd', 'trade_salt', 'admin_id'
+    ];
+
+    /**
+     * 自动转类型
+     * @var string[]
+     */
+    protected $type = [
+        'birthday' => 'int'
+    ];
+
+    protected $updateTime = false;
+
+
+    protected function setAddTimeAttr($value)
+    {
+        return time();
+    }
+
+    protected function setAddIpAttr($value)
+    {
+        return app('request')->ip();
+    }
+
+    protected function setLastTimeAttr($value)
+    {
+        return time();
+    }
+
+    protected function setLastIpAttr($value)
+    {
+        return app('request')->ip();
+    }
+
+    /**
+     * 自定义信息
+     * @param $value
+     * @return mixed
+     */
+    protected function setExtendInfoAttr($value)
+    {
+        if ($value) {
+            return is_array($value) ? json_encode($value) : $value;
+        }
+        return '';
+    }
+
+    /**
+     * 自定义信息
+     * @param $value
+     * @return mixed
+     */
+    protected function getExtendInfoAttr($value)
+    {
+        if ($value) {
+            return is_string($value) ? json_decode($value, true) : $value;
+        }
+        return [];
+    }
+
+    /**
+     * 关联用户分组
+     * @return \think\model\relation\HasOne
+     */
+    public function userGroup()
+    {
+        return $this->hasOne(UserGroup::class, 'id', 'group_id');
+    }
+
+    /**
+     * 关联自己
+     * @return \think\model\relation\HasOne
+     */
+    public function spreadUser()
+    {
+        return $this->hasOne(self::class, 'uid', 'spread_uid');
+    }
+
+    /**
+     * 关联自己
+     * @return \think\model\relation\HasMany
+     */
+    public function spreadCount()
+    {
+        return $this->hasMany(UserSpread::class, 'spread_uid', 'uid');
+    }
+
+    /**
+     * 关联用户地址
+     * @return \think\model\relation\HasMany
+     */
+    public function address()
+    {
+        return $this->hasMany(UserAddress::class, 'uid', 'uid');
+    }
+
+
+    /**
+     * 关联积分数据
+     * @return \think\model\relation\HasMany
+     */
+    public function bill()
+    {
+        return $this->hasMany(UserBill::class, 'uid', 'uid');
+    }
+
+    /**
+     * 关联佣金数据
+     * @return \think\model\relation\HasMany
+     */
+    public function brokerage()
+    {
+        return $this->hasMany(UserBrokerage::class, 'uid', 'uid');
+    }
+
+    /**
+     * 关联余额数据
+     * @return \think\model\relation\HasMany
+     */
+    public function money()
+    {
+        return $this->hasMany(UserMoney::class, 'uid', 'uid');
+    }
+
+    /**
+     * 用户uid
+     * @param Model $query
+     * @param $value
+     */
+    public function searchUidAttr($query, $value)
+    {
+        if (is_array($value))
+            $query->whereIn('uid', $value);
+        else
+            $query->where('uid', $value);
+    }
+
+    /**
+     * 账号搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchAccountAttr($query, $value)
+    {
+        $query->where('account', $value);
+    }
+
+    /**
+     * 模糊条件搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchLikeAttr($query, $value)
+    {
+        $query->where('account|nickname|phone|real_name|uid', 'LIKE', "%$value%");
+    }
+
+    /**
+     * 手机号搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchPhoneAttr($query, $value)
+    {
+        if (is_array($value)) {
+            $query->whereIn('phone', $value);
+        } else {
+            $query->where('phone', $value);
+        }
+    }
+
+    /**
+     * 分组搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchGroupIdAttr($query, $value)
+    {
+        $query->where('group_id', $value);
+    }
+
+    /**
+     * 是否推广人搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchIsPromoterAttr($query, $value)
+    {
+        $query->where('is_promoter', $value);
+    }
+
+    /**
+     * 状态搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchStatusAttr($query, $value)
+    {
+        $query->where('status', $value);
+    }
+
+    /**
+     * 会员等级搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchLevelAttr($query, $value)
+    {
+        $query->where('level', $value);
+    }
+
+    /**
+     * 推广人uid搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchSpreadUidAttr($query, $value)
+    {
+        $query->where('spread_uid', $value);
+    }
+
+    /**
+     * 用户类型搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchUserTypeAttr($query, $value)
+    {
+        if ($value != '') $query->where('user_type', $value);
+    }
+
+    /**
+     * 购买次数搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchPayCountAttr($query, $value)
+    {
+        if ($value !== '') {
+            if ($value == -1) {
+                $query->where('pay_count', '>', 0);
+            } else {
+                $query->where('pay_count', $value);
+            }
+        }
+    }
+
+    public function searchNicknameAttr($query, $value)
+    {
+        $query->where('nickname', "like", "%" . $value . "%");
+    }
+}

+ 68 - 0
app/model/user/UserAddress.php

@@ -0,0 +1,68 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\user;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\model;
+
+/**
+ * Class UserAddress
+ * @package app\model\user
+ */
+class UserAddress extends BaseModel
+{
+    use ModelTrait;
+    use model\concern\SoftDelete;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'user_address';
+
+    protected $hidden = ['add_time', 'delete_time'];
+
+
+    protected function setAddTimeAttr()
+    {
+        return time();
+    }
+
+    /**
+     * 用户uid
+     * @param $query
+     * @param $value
+     */
+    public function searchUidAttr($query, $value)
+    {
+        $query->where('uid', $value);
+    }
+
+
+    /**
+     * 是否默认地址
+     * @param Model $query
+     * @param $value
+     */
+    public function searchIsDefaultAttr($query, $value)
+    {
+        $query->where('is_default', $value);
+    }
+
+}

+ 68 - 0
app/model/user/UserBank.php

@@ -0,0 +1,68 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\user;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\model;
+
+/**
+ * Class UserAddress
+ * @package app\model\user
+ */
+class UserBank extends BaseModel
+{
+    use ModelTrait;
+    use model\concern\SoftDelete;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'user_bank';
+
+    protected $hidden = ['add_time', 'delete_time'];
+
+
+    protected function setAddTimeAttr()
+    {
+        return time();
+    }
+
+    /**
+     * 用户uid
+     * @param $query
+     * @param $value
+     */
+    public function searchUidAttr($query, $value)
+    {
+        $query->where('uid', $value);
+    }
+
+
+    /**
+     * 是否默认地址
+     * @param Model $query
+     * @param $value
+     */
+    public function searchDefaultAttr($query, $value)
+    {
+        $query->where('default', $value);
+    }
+
+}

+ 244 - 0
app/model/user/UserBill.php

@@ -0,0 +1,244 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\user;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\model;
+
+/**
+ * Class UserBill
+ * @package app\model\user
+ */
+class UserBill extends BaseModel
+{
+    use ModelTrait;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'user_bill';
+
+    protected $chsName = '用户流水';
+
+    protected $append = [
+        'category_chs',
+        'pm_chs',
+        'status_chs',
+    ];
+
+    protected $CategoryLang = [
+        'integral' => '积分',
+    ];
+
+    protected $PmLang = [
+        '0' => '支出',
+        '1' => '收入',
+    ];
+
+    protected $StatusLang = [
+        '0' => '待确定',
+        '1' => '有效',
+        '-1' => '无效',
+    ];
+
+
+    protected function setAddTimeAttr($value)
+    {
+        return $value ?: time();
+    }
+
+    /**
+     * 添加时间获取器
+     * @param $value
+     * @return false|string
+     */
+    public function getAddTimeAttr($value)
+    {
+        if (!empty($value)) {
+            if (is_string($value)) {
+                return $value;
+            } elseif (is_int($value)) {
+                return date('Y-m-d H:i:s', (int)$value);
+            }
+        }
+        return '';
+    }
+
+    /**
+     * @param $value
+     * @param $data
+     * @return false|string
+     */
+    public function getCategoryChsAttr($value, $data)
+    {
+        return $this->CategoryLang[$data['category']] ?? '未知';
+    }
+
+    /**
+     * @param $value
+     * @return string
+     */
+    public function getPmChsAttr($value, $data)
+    {
+        return $this->PmLang[$data['pm']] ?? '未知';
+    }
+
+    /**
+     * @param $value
+     * @return string
+     */
+    public function getStatusChsAttr($value, $data)
+    {
+        return $this->StatusLang[$data['status']] ?? '未知';
+    }
+
+    /**
+     * 关联用户
+     * @return model\relation\HasOne
+     */
+    public function user()
+    {
+        return $this->hasOne(User::class, 'uid', 'uid');
+    }
+
+    /**
+     * 用户uid
+     * @param Model $query
+     * @param $value
+     */
+    public function searchUidAttr($query, $value)
+    {
+        if ($value !== '') {
+            if (is_array($value))
+                $query->whereIn('uid', $value);
+            else
+                $query->where('uid', $value);
+        }
+    }
+
+    /**
+     * 关联id
+     * @param Model $query
+     * @param $value
+     */
+    public function searchLinkIdAttr($query, $value)
+    {
+        if (is_array($value))
+            $query->whereIn('link_id', $value);
+        else
+            $query->where('link_id', $value);
+    }
+
+    /**
+     * 支出|获得
+     * @param Model $query
+     * @param $value
+     */
+    public function searchPmAttr($query, $value)
+    {
+        if ($value !== '') $query->where('pm', $value);
+    }
+
+    /**
+     * 种类 now_money:余额 integral:积分 exp:经验
+     * @param Model $query
+     * @param $value
+     */
+    public function searchCategoryAttr($query, $value)
+    {
+        if (is_array($value))
+            $query->whereIn('category', $value);
+        else
+            $query->where('category', $value);
+    }
+
+    /**
+     * @param Model $query
+     * @param $value
+     */
+    public function searchNotCategoryAttr($query, $value)
+    {
+        if (is_array($value))
+            $query->whereNotIn('category', $value);
+        else
+            $query->where('category', '<>', $value);
+    }
+
+    /**
+     * 类型
+     * @param Model $query
+     * @param $value
+     */
+    public function searchTypeAttr($query, $value)
+    {
+        if (is_array($value))
+            $query->whereIn('type', $value);
+        else
+            $query->where('type', $value);
+    }
+
+    /**
+     * @param Model $query
+     * @param $value
+     */
+    public function searchNotTypeAttr($query, $value)
+    {
+        if (is_array($value))
+            $query->whereNotIn('type', $value);
+        else
+            $query->where('type', '<>', $value);
+    }
+
+    /**
+     * 状态 0:带确定 1:有效 -1:无效
+     * @param Model $query
+     * @param $value
+     */
+    public function searchStatusAttr($query, $value)
+    {
+        $query->where('status', $value);
+    }
+
+    /**
+     * 模糊搜索
+     * @param Model $query
+     * @param $value
+     */
+    public function searchLikeAttr($query, $value)
+    {
+        $query->where(function ($query) use ($value) {
+            $query->where('id|uid|title', 'like', "%$value%")->whereOr('uid', 'in', function ($query) use ($value) {
+                $query->name('user')->whereLike('uid|account|nickname|phone', '%' . $value . '%')->field('uid')->select();
+            });
+        });
+    }
+
+    /**
+     * 时间
+     * @param Model $query
+     * @param $value
+     */
+    public function searchAddTimeAttr($query, $value)
+    {
+        if (is_string($value)) $query->whereTime($query, $value);
+        if (is_array($value) && count($value) == 2) $query->whereTime('add_time', 'between', $value);
+    }
+
+}

+ 177 - 0
app/model/user/UserBrokerage.php

@@ -0,0 +1,177 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\user;
+
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\model;
+
+/**
+ * 用户佣金
+ * Class UserBrokerage
+ * @package app\model\user
+ */
+class UserBrokerage extends BaseModel
+{
+    use ModelTrait;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'user_brokerage';
+
+    protected function setAddTimeAttr($value)
+    {
+        return $value ?: time();
+    }
+
+    /**
+     * 添加时间获取器
+     * @param $value
+     * @return false|string
+     */
+    public function getAddTimeAttr($value)
+    {
+        if (!empty($value)) {
+            if (is_string($value)) {
+                return $value;
+            } elseif (is_int($value)) {
+                return date('Y-m-d H:i:s', (int)$value);
+            }
+        }
+        return '';
+    }
+
+    /**
+     * 关联用户
+     * @return model\relation\HasOne
+     */
+    public function user()
+    {
+        return $this->hasOne(User::class, 'uid', 'uid');
+    }
+
+    /**
+     * 用户uid
+     * @param Model $query
+     * @param $value
+     */
+    public function searchUidAttr($query, $value)
+    {
+        if ($value !== '') {
+            if (is_array($value))
+                $query->whereIn('uid', $value);
+            else
+                $query->where('uid', $value);
+        }
+    }
+
+    /**
+     * 关联id
+     * @param Model $query
+     * @param $value
+     */
+    public function searchLinkIdAttr($query, $value)
+    {
+        if (is_array($value))
+            $query->whereIn('link_id', $value);
+        else
+            $query->where('link_id', $value);
+    }
+
+    /**
+     * 支出|获得
+     * @param Model $query
+     * @param $value
+     */
+    public function searchPmAttr($query, $value)
+    {
+        if ($value !== '') $query->where('pm', $value);
+    }
+
+
+    /**
+     * 类型
+     * @param Model $query
+     * @param $value
+     */
+    public function searchTypeAttr($query, $value)
+    {
+        if ($value !== '') {
+            if (is_array($value))
+                $query->whereIn('type', $value);
+            else
+                $query->where('type', $value);
+        }
+
+    }
+
+    /**
+     * @param Model $query
+     * @param $value
+     */
+    public function searchNotTypeAttr($query, $value)
+    {
+        if ($value !== '') {
+            if (is_array($value))
+                $query->whereNotIn('type', $value);
+            else
+                $query->where('type', '<>', $value);
+        }
+    }
+
+    /**
+     * 状态 0:带确定 1:有效 -1:无效
+     * @param Model $query
+     * @param $value
+     */
+    public function searchStatusAttr($query, $value)
+    {
+        $query->where('status', $value);
+    }
+
+    /**
+     * 模糊搜索
+     * @param Model $query
+     * @param $value
+     */
+    public function searchLikeAttr($query, $value)
+    {
+        if ($value !== '') {
+            $query->where(function ($query) use ($value) {
+                $query->where('uid|title', 'like', "%$value%")->whereOr('uid', 'in', function ($query) use ($value) {
+                    $query->name('user')->whereLike('uid|account|nickname|phone', '%' . $value . '%')->field('uid')->select();
+                });
+            });
+        }
+    }
+
+    /**
+     * 时间
+     * @param Model $query
+     * @param $value
+     */
+    public function searchAddTimeAttr($query, $value)
+    {
+        if (is_string($value)) $query->whereTime($query, $value);
+        if (is_array($value) && count($value) == 2) $query->whereTime('add_time', 'between', $value);
+    }
+
+}

+ 37 - 0
app/model/user/UserGroup.php

@@ -0,0 +1,37 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\user;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+
+/**
+ * 用户分组
+ * Class UserGroup
+ * @package app\model\user\group
+ */
+class UserGroup extends BaseModel
+{
+    use ModelTrait;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'user_group';
+}

+ 107 - 0
app/model/user/UserLevel.php

@@ -0,0 +1,107 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\user;
+
+use app\model\system\config\SystemUserLevel;
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\Model;
+use think\model\concern\SoftDelete;
+
+/**
+ * 用户等级
+ * Class UserLevel
+ * @package app\model\user\level
+ */
+class UserLevel extends BaseModel
+{
+    use ModelTrait;
+    use SoftDelete;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'user_level';
+
+    public function setAddTimeAttr($value)
+    {
+        return time();
+    }
+
+    public function getAddTimeAttr($value)
+    {
+        return $value ? date('Y-m-d H:i:s', $value) : '--';
+    }
+
+
+    public function levelInfo()
+    {
+        return $this->hasOne(SystemUserLevel::class, 'id', 'level_id');
+    }
+
+    /**
+     * 用户uid
+     * @param Model $query
+     * @param $value
+     */
+    public function searchUidAttr($query, $value)
+    {
+        $query->where('uid', $value);
+    }
+
+    /**
+     * 是否永久
+     * @param Model $query
+     * @param $value
+     */
+    public function searchIsForeverAttr($query, $value)
+    {
+        $query->where('is_forever', $value);
+    }
+
+    /**
+     * 过期时间
+     * @param Model $query
+     * @param $value
+     */
+    public function searchValidTimeAttr($query, $value)
+    {
+        $query->where('valid_time', '>', $value);
+    }
+
+    /**
+     * 过期时间
+     * @param Model $query
+     * @param $value
+     */
+    public function searchValidAttr($query, $value)
+    {
+        if ($value) $query->where('valid_time', '>', time())->whereOr('is_forever', 1)->where('status', 1);
+    }
+
+    /**
+     * 状态
+     * @param Model $query
+     * @param $value
+     */
+    public function searchStatusAttr($query, $value)
+    {
+        $query->where('status', $value);
+    }
+}

+ 169 - 0
app/model/user/UserMoney.php

@@ -0,0 +1,169 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\user;
+
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\model;
+
+/**
+ * 用户余额
+ * Class UserMoney
+ * @package app\model\user
+ */
+class UserMoney extends BaseModel
+{
+    use ModelTrait;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'user_money';
+
+    protected function setAddTimeAttr($value)
+    {
+        return $value ?: time();
+    }
+
+    /**
+     * 添加时间获取器
+     * @param $value
+     * @return false|string
+     */
+    public function getAddTimeAttr($value)
+    {
+        if (!empty($value)) {
+            if (is_string($value)) {
+                return $value;
+            } elseif (is_int($value)) {
+                return date('Y-m-d H:i:s', (int)$value);
+            }
+        }
+        return '';
+    }
+
+    /**
+     * 关联用户
+     * @return model\relation\HasOne
+     */
+    public function user()
+    {
+        return $this->hasOne(User::class, 'uid', 'uid');
+    }
+
+    /**
+     * 用户uid
+     * @param Model $query
+     * @param $value
+     */
+    public function searchUidAttr($query, $value)
+    {
+        if ($value !== '') {
+            if (is_array($value))
+                $query->whereIn('uid', $value);
+            else
+                $query->where('uid', $value);
+        }
+    }
+
+    /**
+     * 关联id
+     * @param Model $query
+     * @param $value
+     */
+    public function searchLinkIdAttr($query, $value)
+    {
+        if (is_array($value))
+            $query->whereIn('link_id', $value);
+        else
+            $query->where('link_id', $value);
+    }
+
+    /**
+     * 支出|获得
+     * @param Model $query
+     * @param $value
+     */
+    public function searchPmAttr($query, $value)
+    {
+        if ($value !== '') $query->where('pm', $value);
+    }
+
+    /**
+     * 类型
+     * @param Model $query
+     * @param $value
+     */
+    public function searchTypeAttr($query, $value)
+    {
+        if (is_array($value))
+            $query->whereIn('type', $value);
+        else
+            $query->where('type', $value);
+    }
+
+    /**
+     * @param Model $query
+     * @param $value
+     */
+    public function searchNotTypeAttr($query, $value)
+    {
+        if (is_array($value))
+            $query->whereNotIn('type', $value);
+        else
+            $query->where('type', '<>', $value);
+    }
+
+    /**
+     * 状态 0:带确定 1:有效 -1:无效
+     * @param Model $query
+     * @param $value
+     */
+    public function searchStatusAttr($query, $value)
+    {
+        $query->where('status', $value);
+    }
+
+    /**
+     * 模糊搜索
+     * @param Model $query
+     * @param $value
+     */
+    public function searchLikeAttr($query, $value)
+    {
+        $query->where(function ($query) use ($value) {
+            $query->where('uid|title', 'like', "%$value%")->whereOr('uid', 'in', function ($query) use ($value) {
+                $query->name('user')->whereLike('uid|account|nickname|phone', '%' . $value . '%')->field('uid')->select();
+            });
+        });
+    }
+
+    /**
+     * 时间
+     * @param Model $query
+     * @param $value
+     */
+    public function searchAddTimeAttr($query, $value)
+    {
+        if (is_string($value)) $query->whereTime($query, $value);
+        if (is_array($value) && count($value) == 2) $query->whereTime('add_time', 'between', $value);
+    }
+
+}

+ 96 - 0
app/model/user/UserSpread.php

@@ -0,0 +1,96 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\user;
+
+
+use app\model\system\admin\SystemAdmin;
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\model;
+
+/**
+ * Class UserSpread
+ * @package app\model\user
+ */
+class UserSpread extends BaseModel
+{
+    use ModelTrait;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'user_spread';
+
+    public function setSpreadTimeAttr($value)
+    {
+        return $value ?: time();
+    }
+
+    /**
+     * 用户
+     * @return model\relation\HasOne
+     */
+    public function user()
+    {
+        return $this->hasOne(User::class, 'uid', 'uid')->field('uid,nickname,avatar')->bind([
+            'nickname' => 'nickname',
+            'avatar' => 'avatar'
+        ]);
+    }
+
+    /**
+     * 推荐人
+     * @return model\relation\HasOne
+     */
+    public function spreadUser()
+    {
+        return $this->hasOne(User::class, 'uid', 'spread_uid')->field('uid,nickname,avatar')->bind([
+            'nickname' => 'nickname',
+            'avatar' => 'avatar'
+        ]);
+    }
+
+    /**
+     * 用户uid
+     * @param Model $query
+     * @param $value
+     */
+    public function searchUidAttr($query, $value)
+    {
+        if (is_array($value))
+            $query->whereIn('uid', $value);
+        else
+            $query->where('uid', $value);
+
+    }
+
+    /**
+     * 推广人uid
+     * @param Model $query
+     * @param $value
+     */
+    public function searchSpreadUidAttr($query, $value)
+    {
+        if (is_array($value))
+            $query->whereIn('spread_uid', $value);
+        else
+            $query->where('spread_uid', $value);
+
+    }
+}

+ 45 - 0
app/model/wechat/WechatMessage.php

@@ -0,0 +1,45 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\wechat;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+
+/**
+ * 微信用户行为记录  model
+ * Class WechatMessage
+ * @package app\model\wechat
+ */
+class WechatMessage extends BaseModel
+{
+    use ModelTrait;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'id';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'wechat_message';
+
+    protected $insert = ['add_time'];
+
+    public static function setAddTimeAttr($value)
+    {
+        return time();
+    }
+
+}

+ 124 - 0
app/model/wechat/WechatUser.php

@@ -0,0 +1,124 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\model\wechat;
+
+use qiniu\basic\BaseModel;
+use qiniu\traits\ModelTrait;
+use think\model\concern\SoftDelete;
+use think\model\relation\HasOne;
+
+/**
+ * Class WechatUser
+ * @package app\model\wechat
+ */
+class WechatUser extends BaseModel
+{
+    use ModelTrait;
+    use SoftDelete;
+
+    /**
+     * 数据表主键
+     * @var string
+     */
+    protected $pk = 'uid';
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'wechat_user';
+
+    public static function setAddTimeAttr()
+    {
+        return time();
+    }
+
+    protected function getAddTimeAttr($value)
+    {
+        return $value ? date('Y-m-d H:i:s', (int)$value) : '--';
+    }
+
+    /**
+     * 关联user
+     * @return HasOne
+     */
+    public function user()
+    {
+        return $this->hasOne(User::class, 'uid', 'uid');
+    }
+
+    /**
+     * 绑定公众号
+     * @param $query
+     * @param $value
+     * @return mixed
+     */
+    public function searchUnionidAttr($query, $value)
+    {
+        return $query->where('unionid', $value);
+    }
+
+    /**
+     * 公众号唯一id
+     * @param $query
+     * @param $value
+     * @return mixed
+     */
+    public function searchOpenidAttr($query, $value)
+    {
+        return $query->where('openid', $value);
+    }
+
+    /**
+     * 分组
+     * @param $query
+     * @param $value
+     * @return mixed
+     */
+    public function searchGroupIdAttr($query, $value)
+    {
+        return $query->where('group_id', $value);
+    }
+
+    /**
+     * 性别
+     * @param $query
+     * @param $value
+     * @return mixed
+     */
+    public function searchSexAttr($query, $value)
+    {
+        return $query->where('sex', $value);
+    }
+
+    /**
+     * 是否关注
+     * @param $query
+     * @param $value
+     * @return mixed
+     */
+    public function searchSubscribeAttr($query, $value)
+    {
+        return $query->where('subscribe', $value);
+    }
+
+    /**
+     * 用户类型
+     * @param $query
+     * @param $value
+     * @return mixed
+     */
+    public function searchUserTypeAttr($query, $value)
+    {
+        return $query->where('user_type', $value);
+    }
+}

+ 9 - 0
app/provider.php

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

+ 9 - 0
app/service.php

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

+ 188 - 0
app/services/system/CityAreaServices.php

@@ -0,0 +1,188 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\services\system;
+
+
+use app\model\system\CityArea;
+use Exception;
+use qiniu\basic\BaseServices;
+use qiniu\services\CacheService;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\Model;
+use Throwable;
+
+/**
+ * 城市数据(街道)
+ * Class CityAreaServices
+ * @package app\services\other
+ */
+class CityAreaServices extends BaseServices
+{
+
+    /**
+     * 城市类型
+     * @var string[]
+     */
+    public $type = [
+        '1' => 'province',
+        '2' => 'city',
+        '3' => 'area',
+        '4' => 'street'
+    ];
+
+    /**
+     * CityAreaServices constructor.
+     * @param CityArea $model
+     */
+    public function __construct(CityArea $model)
+    {
+        $this->model = $model;
+    }
+
+    /**
+     * 获取某一个城市id相关上级所有ids
+     * @param int $id
+     * @return array|int[]
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getRelationCityIds(int $id)
+    {
+        $cityInfo = $this->model->get($id);
+        $ids = [];
+        if ($cityInfo) {
+            $ids = explode('/', trim($cityInfo['path'], '/'));
+        }
+        return array_merge([$id], $ids);
+    }
+
+    /**
+     * @param int $id
+     * @param int $expire
+     * @return bool|mixed|null
+     * @throws Throwable
+     */
+    public function getRelationCityIdsCache(int $id, int $expire = 1800)
+    {
+        return CacheService::redisHandler('apiCity')->remember('city_ids_' . $id, function () use ($id) {
+            $cityInfo = $this->model->get($id);
+            $ids = [];
+            if ($cityInfo) {
+                $ids = explode('/', trim($cityInfo['path'], '/'));
+            }
+            return array_merge([$id], $ids);
+        }, $expire);
+    }
+
+
+    /**
+     * 获取城市数据
+     * @param int $pid
+     * @param int $type 1:省市 2:省市区 0、3:省市区街道
+     * @return false|mixed|string|null
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getCityTreeList(int $pid = 0, int $type = 0)
+    {
+        $parent_name = '中国';
+        if ($pid) {
+            $city = $this->model->get($pid);
+            $parent_name = $city ? $city['name'] : '';
+        }
+        $cityList = $this->getCityList(['parent_id' => $pid], 'id as value,id,name as label,parent_id as pid,level', ['children']);
+        foreach ($cityList as &$item) {
+            $item['parent_name'] = $parent_name;
+            if (isset($item['children']) && $item['children']) {
+                $item['children'] = [];
+                $item['loading'] = false;
+                $item['_loading'] = false;
+            } else {
+                unset($item['children']);
+            }
+        }
+        if ($cityList) {
+            switch ($type) {
+                case 0:
+                case 3:
+                    break;
+                case 1://控制children 前端不能请求下一级数据
+                    foreach ($cityList as &$item) {
+                        if ($item['level'] == 2) {
+                            unset($item['children'], $item['loading'], $item['_loading']);
+                        }
+                    }
+                    break;
+                case 2:
+                    foreach ($cityList as &$item) {
+                        if ($item['level'] == 3) {
+                            unset($item['children'], $item['loading'], $item['_loading']);
+                        }
+                    }
+                    break;
+                case 4:
+                    foreach ($cityList as &$item) {
+                        if ($item['level'] == 1) {
+                            unset($item['children'], $item['loading'], $item['_loading']);
+                        }
+                    }
+                    break;
+            }
+        }
+
+        return $cityList;
+    }
+
+
+    /**
+     * 搜索某个地址
+     * @param array $where
+     * @return array|Model|null
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function searchCity(array $where)
+    {
+        return $this->model->search($where)->order('id DESC')->find();
+    }
+
+    /**
+     * 获取地址
+     * @param array $where
+     * @param string $field
+     * @param array $with
+     * @return array
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getCityList(array $where, string $field = '*', array $with = [])
+    {
+        return $this->model->where($where)->field($field)->with($with)->order('id asc')->select()->toArray();
+    }
+
+    /**
+     * 删除上级城市和当前城市id
+     * @param int $cityId
+     * @return bool
+     * @throws Exception
+     */
+    public function deleteCity(int $cityId)
+    {
+        return $this->model->where('id', $cityId)->whereOr('parent_id', $cityId)->delete();
+    }
+}

+ 232 - 0
app/services/system/QrcodeServices.php

@@ -0,0 +1,232 @@
+<?php
+declare (strict_types=1);
+
+namespace app\services\system;
+
+use app\services\system\attachment\SystemAttachmentServices;
+use GuzzleHttp\Psr7\Utils;
+use qiniu\basic\BaseServices;
+use app\model\system\Qrcode;
+use qiniu\exceptions\AdminException;
+use qiniu\services\UploadService;
+use qiniu\services\UtilService;
+use qiniu\services\wechat\MiniProgram;
+use qiniu\services\wechat\OfficialAccount;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\Model;
+
+/**
+ * 微信二维码管理服务
+ * Class QrcodeServices
+ * @package app\services\system
+ * @mixin Qrcode
+ */
+class QrcodeServices extends BaseServices
+{
+
+    protected $exportHeader = [
+        'id' => '微信二维码ID',
+        'third_type' => '二维码类型',
+        'third_id' => '用户id',
+        'ticket' => '二维码参数',
+        'expire_seconds' => '二维码有效时间',
+        'status_chs' => '状态',
+        'add_time' => '添加时间',
+        'url' => '微信访问url',
+        'qrcode_url' => '微信二维码url',
+        'scan' => '被扫的次数',
+        'type_chs' => '二维码所属平台'
+    ];
+
+    /**
+     * QrcodeServices constructor.
+     * @param Qrcode $model
+     */
+    public function __construct(Qrcode $model)
+    {
+        $this->model = $model;
+    }
+
+
+    public function getQrcode($id, $type = 'id')
+    {
+        return $this->getModel()->where($type, $id)->find();
+    }
+
+
+    /**
+     * 修改二维码使用状态
+     * @param $id
+     * @param string $type
+     * @return mixed
+     */
+    public function scanQrcode($id, $type = 'id')
+    {
+        return $this->getModel()->where($type, $id)->inc('scan')->update();
+    }
+
+
+    /**
+     * 获取临时二维码
+     * @param $type
+     * @param $id
+     * @return array|Model|null
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getTemporaryQrcode($type, $id)
+    {
+        $where['third_id'] = $id;
+        $where['third_type'] = $type;
+        $where['type'] = 2;
+        $res = $this->getOne($where);
+        if (!$res) {
+            $this->createTemporaryQrcode($id, $type);
+            $res = $this->getTemporaryQrcode($type, $id);
+        } else if (empty($res['expire_seconds']) || $res['expire_seconds'] < time()) {
+            $this->createTemporaryQrcode($id, $type, $res['id']);
+            $res = $this->getTemporaryQrcode($type, $id);
+        }
+        if (!$res['ticket']) throw new AdminException('临时二维码获取错误');
+        return $res;
+    }
+
+    /**
+     * 临时二维码生成
+     * @param $id
+     * @param $type
+     * @param string $qrcode_id
+     */
+    public function createTemporaryQrcode($id, $type, $qrcode_id = '')
+    {
+        $qrcode = OfficialAccount::qrcodeService();
+        $data = $qrcode->temporary($id, 30 * 24 * 3600);
+        $data['qrcode_url'] = $data['url'];
+        $data['expire_seconds'] = $data['expire_seconds'] + time();
+        $data['url'] = $qrcode->url($data['ticket']);
+        $data['status'] = 1;
+        $data['third_id'] = $id;
+        $data['third_type'] = $type;
+        $data['type'] = 2;
+        if ($qrcode_id) {
+            $this->update($qrcode_id, $data);
+        } else {
+            $data['add_time'] = time();
+            $this->save($data);
+        }
+    }
+
+    /**
+     * 获取永久二维码
+     * @param $type
+     * @param $id
+     * @return array|mixed|Model
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getForeverQrcode($type, $id)
+    {
+        $where['third_id'] = $id;
+        $where['third_type'] = $type;
+        $where['type'] = 2;
+        $res = $this->getOne($where);
+        if (!$res) {
+            $this->createForeverQrcode($id, $type);
+            $res = $this->getForeverQrcode($type, $id);
+        }
+        if (!$res['ticket']) throw new AdminException('永久二维码获取错误');
+        return $res;
+    }
+
+    /**
+     * 永久二维码生成
+     * @param $id
+     * @param $type
+     */
+    public function createForeverQrcode($id, $type)
+    {
+        $qrcode = OfficialAccount::qrcodeService();
+        $data = $qrcode->forever($id);
+        $data['qrcode_url'] = $data['url'];
+        $data['url'] = $qrcode->url($data['ticket']);
+        $data['expire_seconds'] = 0;
+        $data['status'] = 1;
+        $data['third_id'] = $id;
+        $data['third_type'] = $type;
+        $data['add_time'] = time();
+        $data['type'] = 2;
+        $this->save($data);
+    }
+
+    /**
+     * 获取小程序分享二维码
+     * @param int $uid
+     * @param string $namePath
+     * @param bool $isSaveAttach
+     * @param array $appendData
+     * @param string $savePath
+     * @return false|mixed|string
+     */
+    public function getRoutineQrcode(int $uid, string $namePath = '', bool $isSaveAttach = true, array $appendData = [], string $savePath = 'qrcode/routine')
+    {
+        $data = 'spread_uid=' . $uid;
+        $page = 'pages/index/index';
+        if (!$page || !$namePath) {
+            return false;
+        }
+        try {
+            /** @var SystemAttachmentServices $systemAttachmentService */
+            $systemAttachmentService = app()->make(SystemAttachmentServices::class);
+            if (!$isSaveAttach) {
+                $imageInfo = "";
+            } else {
+                $imageInfo = $systemAttachmentService->getOne(['name' => $savePath . '/' . $namePath]);
+                //检测远程文件是否存在
+                if (isset($imageInfo['att_dir']) && strstr($imageInfo['att_dir'], 'http') !== false && curl_file_exist($imageInfo['att_dir']) === false) {
+                    $imageInfo = '';
+                    $systemAttachmentService->delete(['name' => $savePath . '/' . $namePath]);
+                }
+            }
+            $siteUrl = sys_config('site_url');
+            if (!$imageInfo) {
+                $res = MiniProgram::appCodeUnlimit($data, $page, 280);
+                if (!$res) return false;
+                $uploadType = (int)sys_config('upload_type', 1);
+                $upload = UploadService::init($uploadType);
+                $res = (string)Utils::streamFor($res);
+                $res = $upload->to($savePath)->validate()->stream($res, $namePath);
+                if ($res === false) {
+                    return false;
+                }
+                $imageInfo = $upload->getUploadInfo();
+                $imageInfo['image_type'] = $uploadType;
+                if ($imageInfo['image_type'] == 1) $remoteImage = UtilService::remoteImage($siteUrl . $imageInfo['dir']);
+                else $remoteImage = UtilService::remoteImage($imageInfo['dir']);
+                if (!$remoteImage['status']) return false;
+                if ($isSaveAttach) {
+                    $systemAttachmentService->save([
+                        'name' => $imageInfo['name'],
+                        'att_dir' => $imageInfo['dir'],
+                        'satt_dir' => $imageInfo['thumb_path'],
+                        'att_size' => $imageInfo['size'],
+                        'att_type' => $imageInfo['type'],
+                        'image_type' => $imageInfo['image_type'],
+                        'module_type' => 2,
+                        'time' => time(),
+                        'pid' => 1,
+                        'type' => 2
+                    ]);
+                }
+                $url = $imageInfo['dir'];
+            } else $url = $imageInfo['att_dir'];
+            if ($imageInfo['image_type'] == 1) $url = $siteUrl . $url;
+            return $url;
+        } catch (\Throwable $e) {
+            return false;
+        }
+    }
+}

+ 68 - 0
app/services/system/SmsRecordServices.php

@@ -0,0 +1,68 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\services\system;
+
+
+use app\model\system\SmsRecord;
+use app\services\user\UserServices;
+use qiniu\services\CacheService;
+use qiniu\basic\BaseServices;
+use think\exception\ValidateException;
+use think\facade\Config;
+
+/**
+ * 短信发送记录
+ * Class SmsRecordServices
+ * @package app\services\message\sms
+ * @mixin SmsRecord
+ */
+class SmsRecordServices extends BaseServices
+{
+    /**
+     * 构造方法
+     * SmsRecordServices constructor.
+     * @param SmsRecord $model
+     */
+    public function __construct(SmsRecord $model)
+    {
+        $this->model = $model;
+    }
+
+    /**
+     * 获取短信发送列表
+     * @param array $where
+     * @return array
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public function getRecordList(array $where)
+    {
+        [$page, $limit] = $this->getPageValue();
+        $data = $this->getList($where, $page, $limit);
+        $count = $this->getCount($where);
+        return compact('data', 'count');
+    }
+
+    public function setRecord($phone, $template, $content, $ip, $store = 'qiniu', $record_id = 0, $uid = 0)
+    {
+        return $this->model->save([
+            'phone' => $phone,
+            'template' => $template,
+            'content' => $content,
+            'add_ip' => $ip,
+            'store' => $store,
+            'uid' => $uid,
+            'record_id' => $record_id,
+        ]);
+    }
+}

+ 111 - 0
app/services/system/SystemClearServices.php

@@ -0,0 +1,111 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\services\system;
+
+
+use qiniu\basic\BaseServices;
+use qiniu\exceptions\AdminException;
+use think\facade\Config;
+use think\facade\Db;
+
+/**
+ * 清除数据
+ * Class SystemClearServices
+ * @package app\services\system
+ */
+class SystemClearServices extends BaseServices
+{
+    /**
+     * 清除表数据
+     * @param string|array $table_name
+     * @param bool $status
+     */
+    public function clearData($table_name, bool $status)
+    {
+        $prefix = config('database.connections.' . config('database.default'))['prefix'];
+        if (is_string($table_name)) {
+            $clearData = [$table_name];
+        } else {
+            $clearData = $table_name;
+        }
+        foreach ($clearData as $name) {
+            if ($status) {
+                Db::execute('TRUNCATE TABLE ' . $prefix . $name);
+            } else {
+                Db::execute('DELETE FROM' . $prefix . $name);
+            }
+        }
+    }
+
+    /**
+     * 递归删除文件,只能删除 public/uploads下的文件
+     * @param string $dirName
+     * @param bool $subdir
+     * @return bool
+     */
+    public function delDirAndFile(string $dirName, $subdir = true)
+    {
+        if (strstr($dirName, 'public/uploads') === false) {
+            return true;
+        }
+        if ($handle = @opendir("$dirName")) {
+            while (false !== ($item = readdir($handle))) {
+                if ($item != "." && $item != "..") {
+                    if (is_dir("$dirName/$item"))
+                        $this->delDirAndFile("$dirName/$item", false);
+                    else
+                        @unlink("$dirName/$item");
+                }
+            }
+            closedir($handle);
+            if (!$subdir) @rmdir($dirName);
+        }
+        return true;
+    }
+
+    /**
+     * 替换域名
+     * @param string $url
+     * @return mixed
+     */
+    public function replaceSiteUrl(string $url)
+    {
+        $siteUrl = sys_config('site_url');
+        $siteUrlJosn = str_replace('://', ':\\\/\\\/', $siteUrl);
+        $valueJosn = str_replace('://', ':\\\/\\\/', $url);
+        $prefix = Config::get('database.connections.' . Config::get('database.default') . '.prefix');
+        $sql = [
+            "UPDATE `{$prefix}system_attachment` SET `att_dir` = replace(att_dir ,'{$siteUrl}','{$url}'),`satt_dir` = replace(satt_dir ,'{$siteUrl}','{$url}')",
+            "UPDATE `{$prefix}store_product` SET `image` = replace(image ,'{$siteUrl}','{$url}'),`slider_image` = replace(slider_image ,'{$siteUrlJosn}','{$valueJosn}')",
+            "UPDATE `{$prefix}store_product_attr_value` SET `image` = replace(image ,'{$siteUrl}','{$url}')",
+            "UPDATE `{$prefix}store_seckill` SET `image` = replace(image ,'{$siteUrl}','{$url}'),`images` = replace(images,'{$siteUrlJosn}','{$valueJosn}')",
+            "UPDATE `{$prefix}store_combination` SET `image` = replace(image ,'{$siteUrl}','{$url}'),`images` = replace(images,'{$siteUrlJosn}','{$valueJosn}')",
+            "UPDATE `{$prefix}store_bargain` SET `image` = replace(image ,'{$siteUrl}','{$url}'),`images` = replace(images,'{$siteUrlJosn}','{$valueJosn}')",
+            "UPDATE `{$prefix}system_config` SET `value` = replace(value ,'{$siteUrlJosn}','{$valueJosn}')",
+            "UPDATE `{$prefix}article_category` SET `image` = replace(`image` ,'{$siteUrl}','{$url}')",
+            "UPDATE `{$prefix}article` SET `image_input` = replace(`image_input` ,'{$siteUrl}','{$url}')",
+            "UPDATE `{$prefix}article_content` SET `content` = replace(`content` ,'{$siteUrl}','{$url}')",
+            "UPDATE `{$prefix}store_product_category` SET `pic` = replace(`pic` ,'{$siteUrl}','{$url}')",
+            "UPDATE `{$prefix}system_group_data` SET `value` = replace(value ,'{$siteUrlJosn}','{$valueJosn}')",
+            "UPDATE `{$prefix}store_product_description` SET `description`= replace(description,'{$siteUrl}','{$url}')"
+        ];
+        return $this->transaction(function () use ($sql) {
+            try {
+                foreach ($sql as $item) {
+                    Db::execute($item);
+                }
+            } catch (\Throwable $e) {
+                throw new AdminException('替换失败,失败原因:' . $e->getMessage());
+            }
+        });
+    }
+}

+ 100 - 0
app/services/system/SystemLogServices.php

@@ -0,0 +1,100 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\services\system;
+
+
+use app\model\system\SystemLog;
+use app\services\system\admin\SystemAdminServices;
+use app\services\system\admin\SystemMenusServices;
+use qiniu\basic\BaseServices;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+
+/**
+ * 系统日志
+ * Class SystemLogServices
+ * @package app\services\system\log
+ * @mixin SystemLog
+ */
+class SystemLogServices extends BaseServices
+{
+
+    /**
+     * 构造方法
+     * SystemLogServices constructor.
+     * @param SystemLog $model
+     */
+    public function __construct(SystemLog $model)
+    {
+        $this->model = $model;
+    }
+
+    /**
+     * 记录访问日志
+     * @param int $adminId
+     * @param string $adminName
+     * @param string $module
+     * @param string $rule
+     * @param string $ip
+     * @param string $type
+     * @param string $params
+     * @return bool
+     */
+    public function recordAdminLog(int $adminId, string $adminName, string $module, string $rule, string $ip, string $type, string $params)
+    {
+        /** @var SystemMenusServices $service */
+        $service = app()->make(SystemMenusServices::class);
+        $data = [
+            'method' => $module,
+            'add_time' => time(),
+            'admin_name' => $adminName,
+            'path' => $rule,
+            'page' => $service->getVisitName($rule) ?: '未知',
+            'ip' => $ip,
+            'type' => $type,
+            'params' => $params,
+        ];
+        if ($type == 'store') {
+            $data['store_id'] = $adminId;
+        } else {
+            $data['admin_id'] = $adminId;
+        }
+        if ($this->model->save($data)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 获取系统日志列表
+     * @param array $where
+     * @param int $level
+     * @return array
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getLogList(array $where, int $level)
+    {
+        [$page, $limit] = $this->getPageValue();
+        if (!$where['admin_id']) {
+            /** @var SystemAdminServices $service */
+            $service = app()->make(SystemAdminServices::class);
+            $where['admin_id'] = $service->getAdminIds($level);
+        }
+        $list = $this->model->getList($where, '*', $page, $limit);
+        $count = $this->model->getCount($where);
+        return compact('list', 'count');
+    }
+}

+ 387 - 0
app/services/system/admin/SystemAdminServices.php

@@ -0,0 +1,387 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\services\system\admin;
+
+use app\model\system\admin\SystemAdmin;
+use Firebase\JWT\ExpiredException;
+use Psr\SimpleCache\InvalidArgumentException;
+use qiniu\basic\BaseServices;
+
+//use app\webscoket\SocketPush;
+use qiniu\exceptions\AdminException;
+use qiniu\exceptions\AuthException;
+use qiniu\services\CacheService;
+use qiniu\services\SystemConfigService;
+use qiniu\utils\ApiErrorCode;
+use qiniu\utils\JwtAuth;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\facade\Cache;
+use think\Model;
+
+/**
+ * 管理员service
+ * Class SystemAdminServices
+ * @package app\services\system\admin
+ */
+class SystemAdminServices extends BaseServices
+{
+
+
+    /**
+     * SystemAdminServices constructor.
+     * @param SystemAdmin $model
+     */
+    public function __construct(SystemAdmin $model)
+    {
+        $this->model = $model;
+    }
+
+    /**
+     * 管理员登陆
+     * @param string $account
+     * @param string $password
+     * @param int $adminType
+     * @return array|Model
+     */
+    public function verifyLogin(string $account, string $password, int $adminType = 1)
+    {
+        $key = 'login_captcha_' . $account;
+        $adminInfo = $this->accountByAdmin($account, $adminType);
+        if (!$adminInfo) {
+            Cache::inc($key);
+            throw new AdminException('管理员不存在!');
+        }
+        if (!$adminInfo->status) {
+            Cache::inc($key);
+            throw new AdminException('您已被禁止登录!');
+        }
+        if (!password_verify($password, $adminInfo->pwd)) {
+            Cache::inc($key);
+            throw new AdminException('账号或密码错误,请重新输入');
+        }
+        $adminInfo->last_time = time();
+        $adminInfo->last_ip = app('request')->ip();
+        $adminInfo->login_count++;
+        $adminInfo->save();
+
+        return $adminInfo;
+    }
+
+    /**
+     * 后台登陆获取菜单获取token
+     * @param string $account
+     * @param string $password
+     * @param string $type
+     * @return array
+     * @throws DbException
+     * @throws DataNotFoundException
+     * @throws ModelNotFoundException
+     */
+    public function login(string $account, string $password, string $type)
+    {
+        $adminInfo = $this->verifyLogin($account, $password);
+        $tokenInfo = $this->createToken($adminInfo->id, $type, $adminInfo['pwd']);
+        /** @var SystemMenusServices $services */
+        $services = app()->make(SystemMenusServices::class);
+        [$menus, $uniqueAuth] = $services->getMenusList($adminInfo['roles'], (int)$adminInfo['level']);
+        $data = SystemConfigService::more(['site_logo', 'site_logo_square']);
+        return [
+            'token' => $tokenInfo['token'],
+            'expires_time' => $tokenInfo['params']['exp'],
+            'menus' => $menus,
+            'unique_auth' => $uniqueAuth,
+            'user_info' => [
+                'id' => $adminInfo['id'],
+                'account' => $adminInfo['account'],
+                'head_pic' => $adminInfo['head_pic'],
+            ],
+            'logo' => $data['site_logo'],
+            'logo_square' => $data['site_logo_square'],
+            'prefix' => config('admin.admin_prefix')
+        ];
+    }
+
+    /**
+     * 获取Admin授权信息
+     * @param string $token
+     * @return array
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws InvalidArgumentException
+     * @throws ModelNotFoundException
+     */
+    public function parseToken(string $token): array
+    {
+        /** @var CacheService $cacheService */
+        $cacheService = app()->make(CacheService::class);
+
+        if (!$token || $token === 'undefined') {
+            throw new AuthException(ApiErrorCode::ERR_LOGIN);
+        }
+        /** @var JwtAuth $jwtAuth */
+        $jwtAuth = app()->make(JwtAuth::class);
+        //设置解析token
+        [$id, $type, $auth] = $jwtAuth->parseToken($token);
+
+
+        //检测token是否过期
+        $md5Token = md5($token);
+        if (!$cacheService->hasToken($md5Token) || !($cacheToken = $cacheService->getTokenBucket($md5Token))) {
+            throw new AuthException(ApiErrorCode::ERR_LOGIN);
+        }
+        //是否超出有效次数
+        if (isset($cacheToken['invalidNum']) && $cacheToken['invalidNum'] >= 3) {
+            if (!request()->isCli()) {
+                $cacheService->clearToken($md5Token);
+            }
+            throw new AuthException(ApiErrorCode::ERR_LOGIN_INVALID);
+        }
+
+        //验证token
+        try {
+            $jwtAuth->verifyToken();
+            $cacheService->setTokenBucket($md5Token, $cacheToken, $cacheToken['exp']);
+        } catch (ExpiredException $e) {
+            $cacheToken['invalidNum'] = isset($cacheToken['invalidNum']) ? $cacheToken['invalidNum']++ : 1;
+            $cacheService->setTokenBucket($md5Token, $cacheToken, $cacheToken['exp']);
+        } catch (\Throwable $e) {
+            if (!request()->isCli()) {
+                $cacheService->clearToken($md5Token);
+            }
+            throw new AuthException(ApiErrorCode::ERR_LOGIN_INVALID);
+        }
+
+        //获取管理员信息
+        $adminInfo = $this->model->get($id);
+        if (!$adminInfo || !$adminInfo->id) {
+            if (!request()->isCli()) {
+                $cacheService->clearToken($md5Token);
+            }
+            throw new AuthException(ApiErrorCode::ERR_LOGIN_STATUS);
+        }
+        //修改密码后token立刻过期
+        if ($auth !== md5($adminInfo['pwd'])) {
+            throw new AuthException(ApiErrorCode::ERR_LOGIN_STATUS);
+        }
+
+        $adminInfo->type = $type;
+        return $adminInfo->hidden(['pwd', 'status'])->toArray();
+    }
+
+
+    /**
+     * 管理员列表
+     * @param array $where
+     * @return array
+     */
+    public function getAdminList(array $where)
+    {
+        [$page, $limit] = $this->getPageValue();
+        $list = $this->getList($where,'*', $page, $limit);
+        $count = $this->count($where);
+
+        /** @var SystemRoleServices $service */
+        $service = app()->make(SystemRoleServices::class);
+        $allRole = $service->getRoleArray(['type' => 0]);
+        foreach ($list as &$item) {
+            if ($item['roles']) {
+                $roles = [];
+                foreach ($item['roles'] as $id) {
+                    if (isset($allRole[$id])) $roles[] = $allRole[$id];
+                }
+                if ($roles) {
+                    $item['roles'] = implode(',', $roles);
+                } else {
+                    $item['roles'] = '';
+                }
+            }
+            $item['_add_time'] = date('Y-m-d H:i:s', $item['add_time']);
+            $item['_last_time'] = $item['last_time'] ? date('Y-m-d H:i:s', $item['last_time']) : '';
+        }
+        return compact('list', 'count');
+    }
+
+    /**
+     * 创建管理员
+     * @param array $data
+     * @return bool
+     * @throws DbException
+     */
+    public function create(array $data)
+    {
+        if ($data['conf_pwd'] != $data['pwd']) {
+            throw new AdminException('两次输入的密码不相同');
+        }
+        unset($data['conf_pwd']);
+
+        if ($this->model->be(['account' => $data['account'], 'admin_type' => $data['admin_type'] ?? 1])) {
+            throw new AdminException('管理员账号已存在');
+        }
+        if ($this->model->be(['phone' => $data['phone'], 'admin_type' => $data['admin_type'] ?? 1])) {
+            throw new AdminException('管理员电话已存在');
+        }
+
+        $data['pwd'] = $this->passwordHash($data['pwd']);
+        $data['add_time'] = time();
+        $data['roles'] = implode(',', $data['roles']);
+
+        return $this->transaction(function () use ($data) {
+            if ($this->model->save($data)) {
+                CacheService::clear();
+                return true;
+            } else {
+                throw new AdminException('添加失败');
+            }
+        });
+    }
+
+
+    /**
+     * 修改管理员
+     * @param int $id
+     * @param array $data
+     * @return bool
+     * @throws DbException
+     * @throws DataNotFoundException
+     * @throws ModelNotFoundException
+     */
+    public function save(int $id, array $data)
+    {
+        if (!$adminInfo = $this->model->get($id)) {
+            throw new AdminException('管理员不存在,无法修改');
+        }
+        //修改密码
+        if ($data['pwd']) {
+            if (!$data['conf_pwd']) {
+                throw new AdminException('请输入确认密码');
+            }
+            if ($data['conf_pwd'] != $data['pwd']) {
+                throw new AdminException('上次输入的密码不相同');
+            }
+            $adminInfo->pwd = $this->passwordHash($data['pwd']);
+        }
+        //修改账号
+        if (isset($data['account']) && $data['account'] != $adminInfo->account && $this->model->be(['account' => $data['account'], 'admin_type' => 1])) {
+            throw new AdminException('管理员账号已存在');
+        }
+        if (isset($data['phone']) && $data['phone'] != $adminInfo->phone && $this->model->be(['phone' => $data['phone'], 'admin_type' => 1])) {
+            throw new AdminException('管理员电话已存在');
+        }
+        if (isset($data['roles'])) {
+            $adminInfo->roles = implode(',', $data['roles']);
+        }
+        $adminInfo->real_name = $data['real_name'] ?? $adminInfo->real_name;
+        $adminInfo->phone = $data['phone'] ?? $adminInfo->phone;
+        $adminInfo->account = $data['account'] ?? $adminInfo->account;
+        $adminInfo->head_pic = $data['head_pic'] ?? $adminInfo->head_pic;
+        $adminInfo->status = $data['status'];
+        if ($adminInfo->save()) {
+            CacheService::clear();
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 修改当前管理员信息
+     * @param int $id
+     * @param array $data
+     * @return bool
+     * @throws DbException
+     * @throws InvalidArgumentException
+     * @throws DataNotFoundException
+     * @throws ModelNotFoundException
+     */
+    public function updateAdmin(int $id, array $data)
+    {
+        $adminInfo = $this->model->get($id);
+        if (!$adminInfo)
+            throw new AdminException('管理员信息未查到');
+        if ($data['head_pic'] != '') {
+            $adminInfo->head_pic = $data['head_pic'];
+        } elseif ($data['real_name'] != '') {
+            $adminInfo->real_name = $data['real_name'];
+        } elseif ($data['pwd'] != '') {
+            if (!password_verify($data['pwd'], $adminInfo['pwd']))
+                throw new AdminException('原始密码错误');
+            if (!$data['new_pwd'])
+                throw new AdminException('请输入新密码');
+            if (!$data['conf_pwd'])
+                throw new AdminException('请输入确认密码');
+            if ($data['new_pwd'] != $data['conf_pwd'])
+                throw new AdminException('两次输入的密码不一致');
+            $adminInfo->pwd = $this->passwordHash($data['new_pwd']);
+        } elseif ($data['phone'] != '') {
+            $verifyCode = CacheService::get('code_' . $data['phone']);
+            if (!$verifyCode)
+                throw new AdminException('请先获取验证码');
+            $verifyCode = substr($verifyCode, 0, 6);
+            if ($verifyCode != $data['code']) {
+                CacheService::delete('code_' . $data['phone']);
+                throw new AdminException('验证码错误');
+            }
+            $adminInfo->phone = $data['phone'];
+        }
+        if ($adminInfo->save()) {
+            CacheService::delete('code_' . $data['phone']);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+
+//    /**
+//     * 后台订单下单,评论,支付成功,后台消息提醒
+//     */
+//    public function adminNewPush()
+//    {
+//        try {
+//            /** @var StoreOrderServices $orderServices */
+//            $orderServices = app()->make(StoreOrderServices::class);
+//            $data['ordernum'] = $orderServices->count(['is_del' => 0, 'status' => 1, 'shipping_type' => 1]);
+//            /** @var StoreProductServices $productServices */
+//            $productServices = app()->make(StoreProductServices::class);
+//            $data['inventory'] = $productServices->count(['type' => 5]);
+//            /** @var StoreProductReplyServices $replyServices */
+//            $replyServices = app()->make(StoreProductReplyServices::class);
+//            $data['commentnum'] = $replyServices->count(['is_reply' => 0]);
+//            /** @var UserExtractServices $extractServices */
+//            $extractServices = app()->make(UserExtractServices::class);
+//            $data['reflectnum'] = $extractServices->getCount(['status' => 0]);//提现
+//            $data['msgcount'] = intval($data['ordernum']) + intval($data['inventory']) + intval($data['commentnum']) + intval($data['reflectnum']);
+//            SocketPush::admin()->type('ADMIN_NEW_PUSH')->data($data)->push();
+//        } catch (\Exception $e) {
+//        }
+//    }
+
+
+    public function accountByAdmin(string $account, int $adminType)
+    {
+        return $this->model->getOne(['account' => $account, 'status' => 1, 'admin_type' => $adminType]);
+    }
+
+
+    /**
+     * 获取adminid
+     * @param int $level
+     * @return array
+     */
+    public function getAdminIds(int $level)
+    {
+        return $this->model->where('level', '>=', $level)->column('id', 'id');
+    }
+
+}

+ 237 - 0
app/services/system/admin/SystemMenusServices.php

@@ -0,0 +1,237 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\services\system\admin;
+
+use app\model\system\admin\SystemMenus;
+use qiniu\basic\BaseServices;
+use qiniu\exceptions\AdminException;
+use qiniu\services\CacheService;
+use qiniu\utils\Arr;
+use think\Collection;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+
+/**
+ * 权限菜单
+ * Class SystemMenusServices
+ * @package app\services\system
+ * @mixin SystemMenus
+ */
+class SystemMenusServices extends BaseServices
+{
+
+    /**
+     * @var string[]
+     */
+    protected $type = [
+        1 => 'admin',//平台
+        2 => 'store',//门店
+    ];
+
+    /**
+     * 初始化
+     * SystemMenusServices constructor.
+     * @param SystemMenus $model
+     */
+    public function __construct(SystemMenus $model)
+    {
+        $this->model = $model;
+    }
+
+    /**
+     * 获取菜单没有被修改器修改的数据
+     * @param $menusList
+     * @param int $type
+     * @return array
+     */
+    public function getMenusData($menusList, int $type = 1)
+    {
+        $data = [];
+        foreach ($menusList as $item) {
+//            $item['expand'] = true;
+            $item['selected'] = false;
+            $item['title'] = $item['menu_name'];
+            $item['menu_path'] = preg_replace('/^\/' . ($this->type[$type] ?? 'admin') . '/', '', $item['menu_path']);
+            $item['extend'] = json_decode($item['extend'], true);
+            $data[] = $item->getData();
+        }
+        return $data;
+    }
+
+    /**
+     * 获取后台权限菜单和权限
+     * @param $rouleId
+     * @param int $level
+     * @param int $type
+     * @return array
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getMenusList($rouleId, int $level, int $type = 1)
+    {
+        $rulesStr = '';
+        if ($level) {//超级管理员查询所有菜单
+            /** @var SystemRoleServices $systemRoleServices */
+            $systemRoleServices = app()->make(SystemRoleServices::class);
+            $rules = $systemRoleServices->getRoleArray(['status' => 1, 'id' => $rouleId], 'rules');
+            $rulesStr = Arr::unique($rules);
+        }
+        $menusList = $this->getMenusRole(['type' => $type, 'route' => $level ? $rulesStr : '']);
+        $unique = $this->getMenusUnique(['type' => $type, 'unique' => $level ? $rulesStr : '']);
+        return [Arr::getMenuIviewList($this->getMenusData($menusList, $type)), $unique];
+    }
+
+
+    /**
+     * 获取后台菜单树型结构列表
+     * @param array $where
+     * @return array
+     * @throws DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws ModelNotFoundException
+     */
+    public function getList(array $where)
+    {
+        $where = array_merge($where);
+        $menusList = $this->search($where)->order('sort DESC,id ASC')->select();
+        $menusList = $this->getMenusData($menusList);
+        return get_tree_children($menusList);
+    }
+
+
+    public function create($data)
+    {
+        if (parent::create($data)) {
+            CacheService::redisHandler('system_menus')->clear();
+            return true;
+        } else {
+            throw new AdminException('添加失败');
+        }
+    }
+
+    public function update($id, array $data, ?string $key = null)
+    {
+        if (parent::update($id, $data)) {
+            CacheService::redisHandler('system_menus')->clear();
+            return true;
+        } else {
+            throw new AdminException('修改失败');
+        }
+    }
+
+    public function delete($id, ?string $key = null)
+    {
+        if ($this->be(['pid' => $id])) {
+            throw new AdminException('请先删除改菜单下的子菜单');
+        }
+        if (parent::delete($id)) {
+            CacheService::redisHandler('system_menus')->clear();
+            return true;
+        } else {
+            throw new AdminException('删除失败,请稍候再试!');
+        }
+    }
+
+    /**
+     * 获取权限菜单列表
+     * @param array $where
+     * @param array|null $field
+     * @return Collection
+     * @throws DbException
+     * @throws DataNotFoundException
+     * @throws ModelNotFoundException
+     */
+    public function getMenusRole(array $where, ?array $field = [])
+    {
+        if (!$field) {
+            $field = ['id', 'menu_name', 'icon', 'pid', 'sort', 'menu_path', 'is_show', 'is_show_path', 'extend'];
+        }
+        return $this->search($where)->field($field)->order('sort DESC,id DESC')->failException(false)->select();
+    }
+
+    /**
+     * 获取菜单中的唯一权限
+     * @param array $where
+     * @return array
+     */
+    public function getMenusUnique(array $where)
+    {
+        return $this->search($where)->where('unique_auth', '<>', '')->column('unique_auth', '');
+    }
+
+    /**
+     * 获取添加身份规格
+     * @param $roles
+     * @param int $type
+     * @param int $is_show
+     * @return array
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getMenus($roles, int $type = 1, int $is_show = 1): array
+    {
+        $field = ['menu_name', 'pid', 'id'];
+        $where = ['type' => $type];
+        if ($is_show) $where['is_show'] = 1;
+        if (!$roles) {
+            $menus = $this->getMenusRole($where, $field);
+        } else {
+            /** @var SystemRoleServices $service */
+            $service = app()->make(SystemRoleServices::class);
+            //拼接有长度限制
+//            $ids = $service->value([['id', 'in', $roles]], 'GROUP_CONCAT(rules) as ids');
+            $roles = is_string($roles) ? explode(',', $roles) : $roles;
+            $ids = $service->getRoleIds($roles);
+            $menus = $this->getMenusRole(['rule' => $ids] + $where, $field);
+        }
+        return $this->tidyMenuTier(false, $menus);
+    }
+
+    /**
+     * 组合菜单数据
+     * @param bool $adminFilter
+     * @param mixed $menusList
+     * @param int $pid
+     * @param array $navList
+     * @return array
+     */
+    public function tidyMenuTier(bool $adminFilter = false, $menusList = [], int $pid = 0, array $navList = []): array
+    {
+        foreach ($menusList as $k => $menu) {
+            $menu = $menu->getData();
+            $menu['title'] = $menu['menu_name'];
+            unset($menu['menu_name']);
+            if ($menu['pid'] == $pid) {
+                unset($menusList[$k]);
+                $menu['children'] = $this->tidyMenuTier($adminFilter, $menusList, $menu['id']);
+                if ($pid == 0 && !count($menu['children'])) continue;
+                if ($menu['children']) $menu['expand'] = true;
+                $navList[] = $menu;
+            }
+        }
+        return $navList;
+    }
+
+
+    /**
+     * 根据访问地址获得菜单名
+     * @param string $rule
+     * @return mixed
+     */
+    public function getVisitName(string $rule)
+    {
+        return $this->search(['url' => $rule])->value('menu_name');
+    }
+}

+ 192 - 0
app/services/system/admin/SystemRoleServices.php

@@ -0,0 +1,192 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\services\system\admin;
+
+
+use app\Request;
+use qiniu\basic\BaseServices;
+use app\model\system\admin\SystemRole;
+use qiniu\exceptions\AdminException;
+use qiniu\exceptions\AuthException;
+use qiniu\utils\ApiErrorCode;
+use qiniu\services\CacheService;
+use think\db\exception\DbException;
+
+
+/**
+ * Class SystemRoleServices
+ * @package app\services\system
+ * @mixin SystemRole
+ */
+class SystemRoleServices extends BaseServices
+{
+
+    /**
+     * 当前管理员权限缓存前缀
+     */
+    const ADMIN_RULES_LEVEL = 'Admin_rules_level_';
+
+    /**
+     * SystemRoleServices constructor.
+     * @param SystemRole $model
+     */
+    public function __construct(SystemRole $model)
+    {
+        $this->model = $model;
+    }
+
+    /**
+     * 获取权限
+     * @return mixed
+     */
+    public function getRoleArray(array $where = [], string $field = '', string $key = '')
+    {
+        return $this->search($where)->column($field ?: 'role_name', $key ?: 'id');
+    }
+
+    /**
+     * 身份管理列表
+     * @param array $where
+     * @return array
+     * @throws DbException
+     */
+    public function getRoleList(array $where)
+    {
+        [$page, $limit] = $this->getPageValue();
+        $list = $this->getList($where, '*', $page, $limit);
+        $count = $this->getCount($where);
+        /** @var SystemMenusServices $service */
+        $service = app()->make(SystemMenusServices::class);
+        foreach ($list as &$item) {
+            $item['rules'] = implode(',', array_merge($service->search(['id' => $item['rules']])->column('menu_name', 'id')));
+        }
+        return compact('count', 'list');
+    }
+
+    /**
+     * 后台验证权限
+     * @param Request $request
+     * @return bool
+     * @throws \Throwable
+     */
+    public function verifiAuth(Request $request)
+    {
+        $rule = str_replace('adminapi/', '', trim(strtolower($request->rule()->getRule())));
+        if (in_array($rule, ['logout', 'menuslist'])) {
+            return true;
+        }
+        $method = trim(strtolower($request->method()));
+        $auth = $this->getAllRoles(2);
+        //验证访问接口是否存在
+        if (!in_array($method . '@@' . $rule, array_map(function ($item) {
+            return trim(strtolower($item['methods'])) . '@@' . trim(strtolower(str_replace(' ', '', $item['api_url'])));
+        }, $auth))) {
+            return true;
+        }
+        $auth = $this->getRolesByAuth($request->adminInfo()['roles'], 2);
+        //验证访问接口是否有权限
+        if ($auth && empty(array_filter($auth, function ($item) use ($rule, $method) {
+                if (trim(strtolower($item['api_url'])) === $rule && $method === trim(strtolower($item['methods'])))
+                    return true;
+                else
+                    return false;
+            }))) {
+            throw new AuthException(ApiErrorCode::ERR_AUTH);
+        }
+        return true;
+    }
+
+    /**
+     * 获取所有权限
+     * @param int $auth_type
+     * @param int $type
+     * @param string $cachePrefix
+     * @return array|bool|mixed|null
+     * @throws \Throwable
+     */
+    public function getAllRoles(int $auth_type = 1, int $type = 1, string $cachePrefix = self::ADMIN_RULES_LEVEL)
+    {
+        $cacheName = md5($cachePrefix . '_' . $auth_type . '_' . $type . '_ALl');
+        return CacheService::redisHandler('system_menus')->remember($cacheName, function () use ($auth_type, $type) {
+            /** @var SystemMenusServices $menusService */
+            $menusService = app()->make(SystemMenusServices::class);
+            return $menusService->getColumn([['auth_type', '=', $auth_type], ['type', '=', $type]], 'api_url,methods');
+        });
+    }
+
+    /**
+     * 获取指定权限
+     * @param array $roles
+     * @param int $auth_type
+     * @param int $type
+     * @param string $cachePrefix
+     * @return array|bool|mixed|null
+     * @throws \Throwable
+     */
+    public function getRolesByAuth(array $roles, int $auth_type = 1, int $type = 1, string $cachePrefix = self::ADMIN_RULES_LEVEL)
+    {
+        if (empty($roles)) return [];
+        $cacheName = md5($cachePrefix . '_' . $auth_type . '_' . $type . '_' . implode('_', $roles));
+        CacheService::redisHandler('system_menus')->clear();
+        return CacheService::redisHandler('system_menus')->remember($cacheName, function () use ($roles, $auth_type, $type) {
+            /** @var SystemMenusServices $menusService */
+            $menusService = app()->make(SystemMenusServices::class);
+            return $menusService->getColumn([['id', 'IN', $this->getRoleIds($roles)], ['auth_type', '=', $auth_type], ['type', '=', $type]], 'api_url,methods');
+        });
+    }
+
+    /**
+     * 获取权限id
+     * @param array $roles
+     * @param string $field
+     * @param string $key
+     * @return array
+     */
+    public function getRoleIds(array $roles, string $field = 'rules', string $key = 'id')
+    {
+        $rules = $this->model->getColumn([['id', 'IN', $roles], ['status', '=', '1']], $field, $key);
+        return $rules ? array_unique(explode(',', implode(',', $rules))) : [];
+    }
+
+
+    public function create($data)
+    {
+        if (parent::create($data)) {
+            CacheService::clear();
+            return true;
+        } else {
+            throw new AdminException('添加失败');
+        }
+    }
+
+    public function update($id, array $data, ?string $key = null)
+    {
+        if (parent::update($id, $data)) {
+            CacheService::clear();
+            return true;
+        } else {
+            throw new AdminException('修改失败');
+        }
+    }
+
+    public function delete($id, ?string $key = null)
+    {
+        if (parent::delete($id)) {
+            CacheService::clear();
+            return true;
+        } else {
+            throw new AdminException('删除失败,请稍候再试!');
+        }
+    }
+
+
+}

+ 144 - 0
app/services/system/attachment/SystemAttachmentCategoryServices.php

@@ -0,0 +1,144 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+declare (strict_types=1);
+
+namespace app\services\system\attachment;
+
+use app\model\system\attachment\SystemAttachmentCategory;
+use qiniu\basic\BaseServices;
+use qiniu\exceptions\AdminException;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\facade\Route as Url;
+
+/**
+ *
+ * Class SystemAttachmentCategoryServices
+ * @package app\services\attachment
+ * @mixin SystemAttachmentCategory
+ */
+class SystemAttachmentCategoryServices extends BaseServices
+{
+
+    /**
+     * SystemAttachmentCategoryServices constructor.
+     * @param SystemAttachmentCategory $model
+     */
+    public function __construct(SystemAttachmentCategory $model)
+    {
+        $this->model = $model;
+    }
+
+    /**
+     * 获取分类列表
+     * @param array $where
+     * @return array
+     */
+    public function getAll(array $where)
+    {
+        $list = $this->getList($where);
+        foreach ($list as &$item) {
+            $item['title'] = $item['name'];
+            $item['children'] = [];
+            if ($where['name'] == '' && $this->getCount(['pid' => $item['id'], 'file_type' => $where['file_type']])) $item['loading'] = false;
+        }
+        return compact('list');
+    }
+
+    /**
+     * 格式化列表
+     * @param $menusList
+     * @param int $pid
+     * @param array $navList
+     * @return array
+     */
+    public function tidyMenuTier($menusList, $pid = 0, $navList = [])
+    {
+        foreach ($menusList as $k => $menu) {
+            $menu['title'] = $menu['name'];
+            if ($menu['pid'] == $pid) {
+                unset($menusList[$k]);
+                $menu['children'] = $this->tidyMenuTier($menusList, $menu['id']);
+                if ($menu['children']) $menu['expand'] = true;
+                $navList[] = $menu;
+            }
+        }
+        return $navList;
+    }
+
+    /**
+     * 获取分类列表(添加修改)
+     * @param array $where
+     * @return mixed
+     */
+    public function getCateList(array $where)
+    {
+        $list = $this->getList($where);
+        $options = [['value' => 0, 'label' => '所有分类']];
+        foreach ($list as $id => $cateName) {
+            $options[] = ['label' => $cateName['name'], 'value' => $cateName['id']];
+        }
+        return $options;
+    }
+
+    /**
+     * 保存新建的资源
+     * @param array $data
+     * @return \qiniu\basic\BaseModel|\think\Model
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function save(array $data)
+    {
+        if ($this->getOne(['name' => $data['name'], 'relation_id' => $data['relation_id'] ?? 0])) {
+            throw new AdminException('该分类已经存在');
+        }
+        $res = $this->create($data);
+        if (!$res) throw new AdminException('新增失败!');
+        return $res;
+    }
+
+    /**
+     * 保存修改的资源
+     * @param int $id
+     * @param array $data
+     * @param string|null $key
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function update($id, array $data, ?string $key = null)
+    {
+        $attachment = $this->getOne(['name' => $data['name'], 'relation_id' => $data['relation_id'] ?? 0]);
+        if ($attachment && $attachment['id'] != $id) {
+            throw new AdminException('该分类已经存在');
+        }
+        return parent::update($id, $data, $key);
+    }
+
+    /**
+     * 删除分类
+     * @param int $id
+     * @throws DbException
+     */
+    public function del(int $id)
+    {
+        $count = $this->be(['pid' => $id]);
+        if ($count) {
+            throw new AdminException('请先删除下级分类!');
+        } else {
+            $res = $this->delete($id);
+            if (!$res) throw new AdminException('请先删除下级分类!');
+        }
+    }
+}

+ 488 - 0
app/services/system/attachment/SystemAttachmentServices.php

@@ -0,0 +1,488 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+declare (strict_types=1);
+
+namespace app\services\system\attachment;
+
+use app\model\system\attachment\SystemAttachment;
+use qiniu\basic\BaseServices;
+use qiniu\exceptions\AdminException;
+use qiniu\exceptions\UploadException;
+use qiniu\services\DownloadImageService;
+use qiniu\services\UploadService;
+use think\Collection;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\exception\ValidateException;
+
+/**
+ *
+ * Class SystemAttachmentServices
+ * @package app\services\attachment
+ * @mixin SystemAttachment
+ */
+class SystemAttachmentServices extends BaseServices
+{
+
+    /**
+     * SystemAttachmentServices constructor.
+     * @param SystemAttachment $model
+     */
+    public function __construct(SystemAttachment $model)
+    {
+        $this->model = $model;
+    }
+
+    /**
+     * 获取单个资源
+     * @param array $where
+     * @param string $field
+     * @return array
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getInfo(array $where, string $field = '*')
+    {
+        return $this->getOne($where, $field);
+    }
+
+    /**
+     * 获取图片列表
+     * @param array $where
+     * @return array
+     * @throws DbException
+     */
+    public function getImageList(array $where)
+    {
+        [$page, $limit] = $this->getPageValue();
+        $list = $this->getList($where,'*', $page, $limit);
+        if ($list) {
+            $site_url = sys_config('site_url');
+            foreach ($list as &$item) {
+                if ($item['file_type'] == 1) {
+                    if ($site_url) {
+                        $item['satt_dir'] = (strpos($item['satt_dir'], $site_url) !== false || strstr($item['satt_dir'], 'http') !== false) ? $item['satt_dir'] : $site_url . $item['satt_dir'];
+                        $item['att_dir'] = (strpos($item['att_dir'], $site_url) !== false || strstr($item['att_dir'], 'http') !== false) ? $item['satt_dir'] : $site_url . $item['att_dir'];
+                    }
+                }
+                $item['att_size'] = formatFileSize($item['att_size']);
+                $item['time'] = date('Y-m-d H:i:s', $item['time']);
+            }
+            $list = get_thumb_water($list, 'mid', ['satt_dir']);
+        }
+        $where['module_type'] = 1;
+        $count = $this->getCount($where);
+        return compact('list', 'count');
+    }
+
+    /**
+     * 删除图片
+     * @param string $ids
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function del(string $ids)
+    {
+        $ids = explode(',', $ids);
+        if (empty($ids)) throw new AdminException('请选择要删除的图片');
+        foreach ($ids as $v) {
+            $attinfo = $this->get((int)$v);
+            if ($attinfo) {
+                try {
+                    $upload = UploadService::init($attinfo['image_type']);
+                    if ($attinfo['image_type'] == 1) {
+                        if (strpos($attinfo['att_dir'], '/') == 0) {
+                            $attinfo['att_dir'] = substr($attinfo['att_dir'], 1);
+                        }
+                        if ($attinfo['att_dir']) $upload->delete(public_path() . $attinfo['att_dir']);
+                    } else {
+                        if ($attinfo['name']) $upload->delete($attinfo['name']);
+                    }
+                } catch (\Throwable $e) {
+                }
+                $this->delete((int)$v);
+            }
+        }
+    }
+
+    /**
+     * 图片上传
+     * @param int $pid
+     * @param string $file
+     * @param int $upload_type
+     * @param int $type
+     * @return mixed
+     */
+    public function upload(int $pid, string $file, int $upload_type, int $type)
+    {
+        if ($upload_type == 0) {
+            $upload_type = (int)sys_config('upload_type', 1);
+        }
+        try {
+            $path = make_path('attach', 2, true);
+            $upload = UploadService::init($upload_type);
+            $res = $upload->to($path)->validate()->setAuthThumb(true)->move($file);
+            if ($res === false) {
+                throw new UploadException($upload->getError());
+            } else {
+                $fileInfo = $upload->getUploadInfo();
+                //保存附件记录
+                if ($type == 0) $this->saveAttachment($fileInfo, $pid, $type, 0, 1, $upload_type);
+                return $res->filePath;
+            }
+        } catch (\Exception $e) {
+            throw new UploadException($e->getMessage());
+        }
+    }
+
+    public function move(array $data)
+    {
+        $res = $this->search()->whereIn('att_id', $data['images'])->update(['pid' => $data['pid']]);
+        if (!$res) throw new AdminException('移动失败或不能重复移动到同一分类下');
+    }
+
+    /**添加附件记录
+     * @param $name
+     * @param $att_size
+     * @param $att_type
+     * @param $att_dir
+     * @param $satt_dir
+     * @param $pid
+     * @param $imageType
+     * @param $time
+     * @param $module_type
+     * @return bool
+     */
+    public function attachmentAdd($name, $att_size, $att_type, $att_dir, $satt_dir = '', $pid = 0, $imageType = 1, $time = 0, $module_type = 1)
+    {
+        $data['name'] = $name;
+        $data['att_dir'] = $att_dir;
+        $data['satt_dir'] = $satt_dir;
+        $data['att_size'] = $att_size;
+        $data['att_type'] = $att_type;
+        $data['image_type'] = $imageType;
+        $data['module_type'] = $module_type;
+        $data['time'] = $time ? $time : time();
+        $data['pid'] = $pid;
+        if (!$this->save($data)) {
+            throw new ValidateException('添加附件失败');
+        }
+        return true;
+    }
+
+    /**
+     * 清除昨日海报
+     * @return bool
+     * @throws \Exception
+     */
+    public function emptyYesterdayAttachment()
+    {
+        try {
+            $list = $this->getYesterday();
+            foreach ($list as $key => $item) {
+                $upload = UploadService::init((int)$item['image_type']);
+                if ($item['image_type'] == 1) {
+                    $att_dir = $item['att_dir'];
+                    if ($att_dir && strstr($att_dir, 'uploads') !== false) {
+                        if (strstr($att_dir, 'http') === false)
+                            $upload->delete($att_dir);
+                        else {
+                            $filedir = substr($att_dir, strpos($att_dir, 'uploads'));
+                            if ($filedir) $upload->delete($filedir);
+                        }
+                    }
+                } else {
+                    if ($item['name']) $upload->delete($item['name']);
+                }
+            }
+            $this->delYesterday();
+            return true;
+        } catch (\Exception $e) {
+            $this->delYesterday();
+            return true;
+        }
+    }
+
+    /**
+     * 获取昨日系统生成
+     * @param int $type
+     * @param int $relationId
+     * @return Collection
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getYesterday(int $type = 1, $relationId = 0)
+    {
+        return $this->model->where('type', $type)->when($relationId, function ($query) use ($relationId) {
+            $query->where('relation_id', $relationId);
+        })->whereTime('time', 'yesterday')->where('module_type', 2)->field(['name', 'att_dir', 'att_id', 'image_type'])->select();
+    }
+
+    /**
+     * 删除昨日生成海报
+     * @throws \Exception
+     */
+    public function delYesterday()
+    {
+        $this->model->whereTime('time', 'yesterday')->where('module_type', 2)->delete();
+    }
+
+
+    /**
+     * 门店图片上传
+     * @param int $pid
+     * @param string $file
+     * @param int $relationId
+     * @param int $type
+     * @param int $upload_type
+     * @param string $uploadToken
+     * @return mixed
+     */
+    public function storeUpload(int $pid, string $file, int $relationId, int $type = 2, int $upload_type = 0, string $uploadToken = '')
+    {
+        try {
+            if ($type == 4) {
+                $upload_type = 0;
+            } else {
+                if ($upload_type == 0) {
+                    $upload_type = (int)sys_config('upload_type', 1);
+                }
+            }
+            $path = make_path('attach/' . ($type == 4 ? 'supplier' : ($type == 2 ? 'store' : '')), 2, true);
+            $upload = UploadService::init($upload_type);
+            $res = $upload->to($path)->validate()->setAuthThumb(true)->move($file);
+            if ($res === false) {
+                throw new UploadException($upload->getError());
+            } else {
+                $fileInfo = $upload->getUploadInfo();
+                //保存附件记录
+                $this->saveAttachment($fileInfo, $pid, $type, $relationId, 1, $upload_type, $uploadToken);
+                return $res->filePath;
+            }
+        } catch (\Exception $e) {
+            throw new UploadException($e->getMessage());
+        }
+    }
+
+    /**
+     * 视频分片上传
+     * @param $data
+     * @param $file
+     * @param int $type
+     * @param int $relation_id
+     * @return mixed
+     */
+    public function videoUpload($data, $file, int $type = 1, int $relation_id = 0)
+    {
+        $public_dir = app()->getRootPath() . 'public';
+        $store_dir = '';
+        switch ($type) {
+            case 2:
+                $store_dir = 'store' . DIRECTORY_SEPARATOR;
+                break;
+            case 4:
+                $store_dir = 'supplier' . DIRECTORY_SEPARATOR;
+                break;
+        }
+        $dir = '/uploads/video/' . $store_dir . date('Y') . DIRECTORY_SEPARATOR . date('m') . DIRECTORY_SEPARATOR . date('d');
+        $all_dir = $public_dir . $dir;
+        if (!is_dir($all_dir)) mkdir($all_dir, 0777, true);
+        $filename = $all_dir . '/' . $data['filename'] . '__' . $data['chunkNumber'];
+        move_uploaded_file($file->getPathName(), $filename);
+        $res['code'] = 0;
+        $res['msg'] = 'error';
+        $res['file_path'] = '';
+        if ($data['chunkNumber'] == $data['totalChunks']) {
+            $blob = '';
+            for ($i = 1; $i <= $data['totalChunks']; $i++) {
+                $blob .= file_get_contents($all_dir . '/' . $data['filename'] . '__' . $i);
+            }
+            file_put_contents($all_dir . '/' . $data['filename'], $blob);
+            for ($i = 1; $i <= $data['totalChunks']; $i++) {
+                @unlink($all_dir . '/' . $data['filename'] . '__' . $i);
+            }
+            if (file_exists($all_dir . '/' . $data['filename'])) {
+                $res['code'] = 2;
+                $res['msg'] = 'success';
+                $res['file_path'] = $dir . '/' . $data['filename'];
+            }
+        } else {
+            if (file_exists($all_dir . '/' . $data['filename'] . '__' . $data['chunkNumber'])) {
+                $res['code'] = 1;
+                $res['msg'] = 'waiting';
+                $res['file_path'] = '';
+            }
+        }
+        $res['name'] = $res['dir'] = $res['file_path'];
+        if (strpos($res['file_path'], 'http') === false) {
+            $res['dir'] = $res['file_path'] = sys_config('site_url') . $res['file_path'];
+        }
+        if ($res['code'] == 2) {
+            $res['size'] = getFileHeaders($res['file_path'])['size'] ?? 0;
+            $this->saveAttachment($res, (int)($data['pid'] ?? 0), $type, $relation_id, 2);
+        }
+        return $res;
+    }
+
+    /**
+     * 云端上传的视频保存记录
+     * @param array $data
+     * @param int $type
+     * @param int $relation_id
+     * @param int $upload_type
+     * @return bool
+     */
+    public function saveOssVideoAttachment(array $data, int $type = 1, int $relation_id = 0, int $upload_type = 1)
+    {
+        $fileInfo = [];
+        $fileInfo['name'] = $fileInfo['real_name'] = $data['path'];
+        $fileInfo['cover_image'] = $data['cover_image'];
+        if (!$fileInfo['cover_image'] && $upload_type != 1) {//云端视频
+            $res = UploadService::init($upload_type)->videoCoverImage($data['path'], 'mid');
+            $fileInfo['cover_image'] = $res['mid'] ?? '';
+        }
+        $fileInfo['size'] = getFileHeaders($data['path'])['size'] ?? 0;
+        $this->saveAttachment($fileInfo, (int)$data['pid'], $type, $relation_id, 2, $upload_type);
+        return true;
+    }
+
+    /**
+     * 保存附件信息
+     * @param $fileInfo
+     * @param int $pid
+     * @param int $type
+     * @param int $relation_id
+     * @param int $file_type
+     * @param int $upload_type
+     * @param string $uploadToken
+     * @return bool
+     */
+    public function saveAttachment($fileInfo, int $pid = 0, int $type = 1, int $relation_id = 0, int $file_type = 1, int $upload_type = 1, string $uploadToken = '')
+    {
+        $fileType = pathinfo($fileInfo['name'], PATHINFO_EXTENSION);
+        if ($fileInfo && !in_array($fileType, ['xlsx', 'xls'])) {
+            $data['file_type'] = $file_type;
+            $data['type'] = $type == 0 ? 1 : $type;
+            $data['relation_id'] = $relation_id;
+            $data['name'] = $fileInfo['name'];
+            $data['real_name'] = $fileInfo['real_name'] ?? $fileInfo['name'] ?? '';
+            $data['att_dir'] = $fileInfo['dir'] ?? $fileInfo['name'] ?? '';
+            if ($data['att_dir'] && strpos($data['att_dir'], 'http') === false) {
+                $data['att_dir'] = sys_config('site_url') . $data['att_dir'];
+            }
+            $data['satt_dir'] = $fileInfo['thumb_path'] ?? $fileInfo['cover_image'] ?? '';
+            if ($file_type == 2) {
+                if (!$data['satt_dir']) {//视频 默认封面
+                    $data['satt_dir'] = sys_config('site_url') . '/statics/images/video_default_cover.png';
+                }
+                if ($data['real_name'] && strpos($data['real_name'], '/') !== false) {
+                    $nameArr = explode('/', $data['real_name']);
+                    $data['real_name'] = end($nameArr) ?? $data['real_name'];
+                }
+            }
+
+            $data['att_size'] = $fileInfo['size'] ?? '';
+            $data['att_type'] = $fileInfo['type'] ?? '';
+            $data['image_type'] = $upload_type;
+            $data['module_type'] = 1;
+            $data['time'] = $fileInfo['time'] ?? time();
+            $data['pid'] = $pid;
+            $data['scan_token'] = $uploadToken;
+            $this->save($data);
+        }
+        return true;
+    }
+
+    /**
+     * 网络图片上传
+     * @param array $data
+     * @param int $type
+     * @param int $relation_id
+     * @return bool
+     * @throws \Exception
+     */
+    public function onlineUpload(array $data, int $type = 1, int $relation_id = 0)
+    {
+        //生成附件目录
+        if (make_path('attach', 3, true) === '') {
+            throw new AdminException('无法创建文件夹,请检查您的上传目录权限');
+        }
+
+        //上传图片
+        /** @var SystemAttachmentServices $systemAttachmentService */
+        $systemAttachmentService = app()->make(SystemAttachmentServices::class);
+        $siteUrl = sys_config('site_url');
+
+        foreach ($data['images'] as $image) {
+            try {
+                $uploadValue = app()->make(DownloadImageService::class)->thumb(true)->downloadImage($image);
+            } catch (\Throwable $e) {
+                throw new AdminException('文件下载失败,请更换链接');
+            }
+
+            if (is_array($uploadValue)) {
+                //TODO 拼接图片地址
+                if ($uploadValue['image_type'] == 1) {
+                    $imagePath = $siteUrl . $uploadValue['path'];
+                } else {
+                    $imagePath = $uploadValue['path'];
+                }
+                //写入数据库
+                if (!$uploadValue['is_exists']) {
+                    $systemAttachmentService->save([
+                        'type' => $type == 0 ? 1 : $type,
+                        'relation_id' => $relation_id,
+                        'name' => $uploadValue['name'],
+                        'real_name' => $uploadValue['name'],
+                        'att_dir' => $imagePath,
+                        'satt_dir' => $imagePath,
+                        'att_size' => $uploadValue['size'],
+                        'att_type' => $uploadValue['mime'],
+                        'image_type' => $uploadValue['image_type'],
+                        'module_type' => 1,
+                        'time' => time(),
+                        'pid' => $data['pid']
+                    ]);
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 删除门店、供应商附件
+     * @param array $where
+     * @param int $type
+     * @param int $relation_id
+     * @return bool
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function delAttachment(array $where = [], int $type = 0, int $relation_id = 0)
+    {
+        $where['type'] = $type;
+        $where['relation_id'] = $relation_id;
+        //删除附件
+        $attachments = $this->getColumn($where, 'att_id');
+        if ($attachments) {
+            $idsArr = array_chunk($attachments, 100);
+            foreach ($idsArr as $ids) {
+                $this->del(implode(',', $ids));
+            }
+        }
+        return true;
+    }
+}

+ 359 - 0
app/services/system/config/SystemConfigServices.php

@@ -0,0 +1,359 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\services\system\config;
+
+
+use app\model\system\config\SystemConfig;
+use qiniu\basic\BaseServices;
+use qiniu\exceptions\AdminException;
+use qiniu\services\FileService;
+use qiniu\services\wechat\contract\ServeConfigInterface;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\exception\ValidateException;
+
+/**
+ * 系统配置
+ * Class SystemConfigServices
+ * @package app\services\system\config
+ * @mixin SystemConfig
+ */
+class SystemConfigServices extends BaseServices implements ServeConfigInterface
+{
+
+
+    /**
+     * 表单数据切割符号
+     * @var string
+     */
+    protected $cuttingStr = '=>';
+
+    /**
+     * SystemConfigServices constructor.
+     * @param SystemConfig $model
+     */
+    public function __construct(SystemConfig $model)
+    {
+        $this->model = $model;
+    }
+
+    /**
+     * 获取单个系统配置
+     * @param string $configName
+     * @param int $store_id
+     * @param null $default
+     * @return mixed|null
+     */
+    public function getConfigValue(string $configName, int $store_id = 0, $default = null)
+    {
+        if ($store_id) {//查门店、供应商
+            /** @var SystemStoreConfigServices $service */
+            $service = app()->make(SystemStoreConfigServices::class);
+            return $service->getConfig($configName, $store_id);
+        } else {//平台
+            $value = $this->search(['menu_name' => $configName])->value('value');
+            return is_null($value) ? $default : json_decode($value, true);
+        }
+    }
+
+    /**
+     * 获取全部配置
+     * @param array $configName
+     * @param int $store_id
+     * @return array
+     */
+    public function getConfigAll(array $configName = [], int $store_id = 0)
+    {
+        if ($store_id) {
+            /** @var SystemStoreConfigServices $service */
+            $service = app()->make(SystemStoreConfigServices::class);
+            return $service->getConfigAll($configName, $store_id);
+        } else {
+            $get_all = function (array $configName = [], int $storeId = 0) {
+                if ($configName) {
+                    return $this->search(['menu_name' => $configName])->column('value', 'menu_name');
+                } else {
+                    return $this->getModel()->column('value', 'menu_name');
+                }
+            };
+            return array_map(function ($item) {
+                return json_decode($item, true);
+            }, $get_all($configName));
+        }
+    }
+
+
+    /**
+     * 获取配置并分页
+     * @param array $where
+     * @return array
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getConfigList(array $where)
+    {
+        [$page, $limit] = $this->getPageValue();
+        $list = $this->search($where)->page($page, $limit)->order('sort desc,id asc')->select()->toArray();
+        $count = $this->getCount($where);
+        foreach ($list as &$item) {
+            $item['value'] = $item['value'] ? json_decode($item['value'], true) ?: '' : '';
+            if ($item['type'] == 'radio' || $item['type'] == 'checkbox') {
+                $item['value'] = $this->getRadioOrCheckboxValueInfo($item['menu_name'], $item['value']);
+            }
+            if ($item['type'] == 'upload' && !empty($item['value'])) {
+                $item['value'] = set_file_url($item['value']);
+                $tidy_srr = [];
+                foreach ($item['value'] as $key => $value) {
+                    $tidy_srr[$key]['filepath'] = $value;
+                    $tidy_srr[$key]['filename'] = basename($value);
+                }
+                $item['value'] = $tidy_srr;
+            }
+        }
+        return compact('count', 'list');
+    }
+
+    /**
+     * 获取单选按钮或者多选按钮的显示值
+     * @param string $menu_name
+     * @param $value
+     * @return string
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getRadioOrCheckboxValueInfo(string $menu_name, $value): string
+    {
+        $option = [];
+        $config_one = $this->getOne(['menu_name' => $menu_name]);
+        if (!$config_one) {
+            return '';
+        }
+        $parameter = explode("\n", $config_one['parameter']);
+        foreach ($parameter as $k => $v) {
+            if (isset($v) && strlen($v) > 0) {
+                $data = explode($this->cuttingStr, $v);
+                $option[$data[0]] = $data[1];
+            }
+        }
+        $str = '';
+        if (is_array($value)) {
+            foreach ($value as $v) {
+                $str .= $option[$v] . ',';
+            }
+        } else {
+            $str .= !empty($value) ? $option[$value] ?? '' : $option[0] ?? '';
+        }
+        return $str;
+    }
+
+    /**
+     * 获取系统配置信息
+     * @param int $tabId
+     * @return array
+     */
+    public function getReadList(int $tabId)
+    {
+        $info = $this->getConfigTabAllList($tabId);
+        foreach ($info as $k => $v) {
+            if (!is_null(json_decode($v['value'])))
+                $info[$k]['value'] = json_decode($v['value'], true);
+            if ($v['type'] == 'upload' && !empty($v['value'])) {
+                if ($v['upload_type'] == 1 || $v['upload_type'] == 3) $info[$k]['value'] = explode(',', $v['value']);
+            }
+        }
+        return $info;
+    }
+
+    public function getConfigTabAllList(int $tabId, int $status = 1, int $type = 1, int $relation_id = 0)
+    {
+        $where['tab_id'] = $tabId;
+        if ($status == 1) $where['status'] = $status;
+        if ($relation_id != 0) $where['is_store'] = 1;
+        return $this->search($where)
+            ->when(isset($relation_id) && $relation_id != 0, function ($query) use ($type, $relation_id) {
+                $query->with(['storeConfig' => function ($querys) use ($type, $relation_id) {
+                    $querys->where('type', $type)->where('relation_id', $relation_id);
+                }]);
+            })
+            ->order('sort desc')->select()->toArray();
+    }
+
+    /**
+     * 获取上传配置中的上传类型
+     * @param string $configName
+     * @return array
+     */
+    public function getUploadTypeList(string $configName)
+    {
+        return $this->search(['menu_name' => $configName])->column('upload_type', 'type');
+    }
+
+    /**
+     * radio 和 checkbox规则的判断
+     * @param $data
+     * @return bool
+     */
+    public function valiDateRadioAndCheckbox($data)
+    {
+        $option = [];
+        $option_new = [];
+        $data['parameter'] = str_replace("\r\n", "\n", $data['parameter']);//防止不兼容
+        $parameter = explode("\n", $data['parameter']);
+        if (count($parameter) < 2) {
+            throw new AdminException('请输入正确格式的配置参数');
+        }
+        foreach ($parameter as $k => $v) {
+            if (isset($v) && !empty($v)) {
+                $option[$k] = explode('=>', $v);
+            }
+        }
+        if (count($option) < 2) {
+            throw new AdminException('请输入正确格式的配置参数');
+        }
+        $bool = 1;
+        foreach ($option as $k => $v) {
+            $option_new[$k] = $option[$k][0];
+            foreach ($v as $kk => $vv) {
+                $vv_num = strlen($vv);
+                if (!$vv_num) {
+                    $bool = 0;
+                }
+            }
+        }
+        if (!$bool) {
+            throw new AdminException('请输入正确格式的配置参数');
+        }
+        $num1 = count($option_new);//提取该数组的数目
+        $arr2 = array_unique($option_new);//合并相同的元素
+        $num2 = count($arr2);//提取合并后数组个数
+        if ($num1 > $num2) {
+            throw new AdminException('请输入正确格式的配置参数');
+        }
+        return true;
+    }
+
+    /**
+     * 验证参数
+     * @param $data
+     * @return bool
+     */
+    public function valiDateValue($data)
+    {
+        if (!$data || !isset($data['required']) || !$data['required']) {
+            return true;
+        }
+        $valids = explode(',', $data['required']);
+        foreach ($valids as $valid) {
+            $valid = explode(':', $valid);
+            if (isset($valid[0]) && isset($valid[1])) {
+                $k = strtolower(trim($valid[0]));
+                $v = strtolower(trim($valid[1]));
+                switch ($k) {
+                    case 'required':
+                        if ($v == 'true' && $data['value'] === '') {
+                            throw new ValidateException(($data['info'] ?? '') . '请输入默认值');
+                        }
+                        break;
+                    case 'url':
+                        if ($v == 'true' && !check_link($data['value'])) {
+                            throw new ValidateException(($data['info'] ?? '') . '请输入正确url');
+                        }
+                        break;
+                }
+            }
+        }
+    }
+
+
+    /**
+     * 检测缩略图水印配置是否更改
+     * @param array $post
+     * @return bool
+     */
+    public function checkThumbParam(array $post)
+    {
+        unset($post['upload_type'], $post['image_watermark_status']);
+        /** @var SystemConfigTabServices $systemConfigTabServices */
+        $systemConfigTabServices = app()->make(SystemConfigTabServices::class);
+        //上传配置->基础配置
+        $tab_id = $systemConfigTabServices->getColumn(['eng_title' => 'upload_base'], 'id');
+        if ($tab_id) {
+            $all = $this->getColumn(['config_tab_id' => $tab_id], 'value', 'menu_name');
+            if (array_intersect(array_keys($all), array_keys($post))) {
+                foreach ($post as $key => $item) {
+                    //配置更改删除原来生成的缩略图
+                    if (isset($all[$key]) && $item != $all[$key]) {
+                        try {
+                            FileService::delDir(public_path('uploads/thumb_water'));
+                            break;
+                        } catch (\Throwable $e) {
+
+                        }
+                    }
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 获取全部配置
+     * @param array $configName
+     * @param int $storeId
+     * @param int $type 0 正常结构 1:只返回key value
+     * @return array
+     */
+    public function getConfigAllField(array $configName = [], int $storeId = 0, int $type = 0)
+    {
+        $list = $this->withSearchSelect(['menu_name', 'store_id'], ['menu_name' => $configName, 'store_id' => $storeId])->column(implode(',', ['info', 'type', 'value', 'desc', 'parameter']), 'menu_name');
+        foreach ($list as &$item) {
+            $item['value'] = json_decode($item['value'], true);
+        }
+        $value = [];
+        foreach ($configName as $key) {
+            if ($type) {
+                $value[$key] = $list[$key]['value'] ?? '';
+            } else {
+                $value[$key] = $list[$key] ?? ['info' => '', 'type' => 'text', 'value' => '', 'desc' => '', 'parameter' => ''];
+            }
+        }
+        return $value;
+    }
+
+
+    public function getOptions(string $parameter)
+    {
+        $parameter = explode("\n", $parameter);
+        $options = [];
+        foreach ($parameter as $v) {
+            if (strstr($v, $this->cuttingStr) !== false) {
+                $pdata = explode($this->cuttingStr, $v);
+                $options[] = ['label' => $pdata[1], 'value' => (int)$pdata[0]];
+            }
+        }
+        return $options;
+    }
+
+    /**
+     * 获取配置
+     * @param string $key
+     * @param null $default
+     * @return mixed
+     */
+    public function getConfig(string $key, $default = null)
+    {
+        return sys_config($key, $default);
+    }
+}

+ 105 - 0
app/services/system/config/SystemConfigTabServices.php

@@ -0,0 +1,105 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\services\system\config;
+
+
+use app\model\system\config\SystemConfigTab;
+use qiniu\basic\BaseServices;
+use qiniu\exceptions\AdminException;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+
+/**
+ * 系统配置分类
+ * Class SystemConfigTabServices
+ * @package app\services\system\config
+ * @mixin SystemConfigTab
+ */
+class SystemConfigTabServices extends BaseServices
+{
+    /**
+     * SystemConfigTabServices constructor.
+     * @param SystemConfigTab $model
+     */
+    public function __construct(SystemConfigTab $model)
+    {
+        $this->model = $model;
+    }
+
+
+    /**
+     * 获取配置分类
+     * @param array $searchWhere
+     * @param array $field
+     * @param array $where
+     * @return array
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getConfigTabAll(array $searchWhere, array $field = ['*'], array $where = [])
+    {
+        return $this->search($searchWhere)->when(count($where), function ($query) use ($where) {
+            $query->where($where);
+        })->field($field)->order('sort desc,id asc')->select()->toArray();
+    }
+
+    /**
+     * 系统设置头部分类读取
+     * @param int $pid
+     * @param int $is_store
+     * @return array
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getConfigTab(int $pid, int $is_store = 0)
+    {
+        $searchWhere = ['status' => 1, 'pid' => $pid, 'is_store' => $is_store];
+        $field = ['id', 'id as value', 'title as label', 'pid', 'icon'];
+        $list = $this->getConfigTabAll($searchWhere, $field);
+        return get_tree_children($list, $pid);
+    }
+
+
+    /**
+     * 获取配置分类列表
+     * @param array $where
+     * @return array
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getConfgTabList(array $where)
+    {
+        $list = $this->getConfigTabAll($where);
+        $count = $this->getCount($where);
+        $list = get_tree_children($list, $where['pid'] ?? 0);
+        return compact('list', 'count');
+    }
+
+    /**
+     * 获取配置分类选择下拉树
+     * @return array
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getSelectForm()
+    {
+        $menuList = $this->getConfigTabAll([], ['id as value', 'pid', 'title as label']);
+        $menus = [['value' => 0, 'label' => '顶级按钮']];
+        $list = get_tree_children($menuList, 0, 'value');
+        return array_merge($menus, $list);
+    }
+}

+ 417 - 0
app/services/system/config/SystemStorageServices.php

@@ -0,0 +1,417 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\services\system\config;
+
+
+use app\model\system\config\SystemStorage;
+use qiniu\basic\BaseServices;
+use qiniu\services\CacheService;
+use qiniu\services\SystemConfigService;
+use qiniu\services\UploadService;
+use think\db\exception\DbException;
+use think\exception\ValidateException;
+
+/**
+ * Class SystemStorageServices
+ * @package app\services\system\config
+ * @mixin SystemStorage
+ */
+class SystemStorageServices extends BaseServices
+{
+    /**
+     * SystemStorageServices constructor.
+     * @param SystemStorage $model
+     */
+    public function __construct(SystemStorage $model)
+    {
+        $this->model = $model;
+    }
+
+    /**
+     * @param array $where
+     * @return array
+     * @throws DbException
+     */
+    public function getStorageList(array $where)
+    {
+        [$page, $limit] = $this->getPageValue();
+        $config = $this->getStorageConfig((int)$where['type']);
+        $where['access_key'] = $config['accessKey'];
+        $list = $this->getList($where,'*', $page, $limit);
+        foreach ($list as &$item) {
+            $item['_add_time'] = date('Y-m-d H:i:s', $item['add_time']);
+            $item['_update_time'] = date('Y-m-d H:i:s', $item['update_time']);
+            $service = UploadService::init($item['type']);
+            $region = $service->getRegion();
+            foreach ($region as $value) {
+                if (strstr($item['region'], $value['value'])) {
+                    $item['_region'] = $value['label'];
+                }
+            }
+            if (!$item['cname']) {
+                $item['cname'] = str_replace(['http://', 'https://'], '', $item['domain']);
+            }
+        }
+        $count = $this->getCount($where);
+        return compact('list', 'count');
+    }
+
+    /**
+     * @param int $type
+     * @return array
+     */
+    public function getStorageConfig(int $type)
+    {
+        $config = [
+            'accessKey' => '',
+            'secretKey' => ''
+        ];
+        switch ($type) {
+            case 2://七牛
+                $config = [
+                    'accessKey' => sys_config('qiniu_accessKey', ''),
+                    'secretKey' => sys_config('qiniu_secretKey', ''),
+                ];
+                break;
+            case 3:// oss 阿里云
+                $config = [
+                    'accessKey' => sys_config('accessKey', ''),
+                    'secretKey' => sys_config('secretKey', ''),
+                ];
+                break;
+            case 4:// cos 腾讯云
+                $config = [
+                    'accessKey' => sys_config('tengxun_accessKey', ''),
+                    'secretKey' => sys_config('tengxun_secretKey', ''),
+                    'appid' => sys_config('tengxun_appid', ''),
+                ];
+                break;
+        }
+        return $config;
+    }
+
+    /**
+     * 删除空间
+     * @param int $id
+     * @return bool
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public function deleteStorage(int $id)
+    {
+        $storageInfo = $this->get(['is_delete' => 0, 'id' => $id]);
+        if (!$storageInfo) {
+            throw new ValidateException('删除的云存储不存在');
+        }
+        if ($storageInfo->status) {
+            throw new ValidateException('云存储正在使用中,需要启动其他空间才能删除');
+        }
+
+        try {
+            $upload = UploadService::init($storageInfo->type);
+            $res = $upload->deleteBucket($storageInfo->name, $storageInfo->region);
+            if (false === $res) {
+                throw new ValidateException($upload->getError());
+            }
+        } catch (\Throwable $e) {
+            throw new ValidateException($e->getMessage());
+        }
+        $storageInfo->is_delete = 1;
+        $storageInfo->save();
+        return true;
+    }
+
+    public function saveConfig(int $type, array $data)
+    {
+        //保存配置信息
+        if (1 !== $type) {
+            $accessKey = $secretKey = $appid = $storageRegion = '';
+            if (isset($data['accessKey']) && isset($data['secretKey']) && $data['accessKey'] && $data['secretKey']) {
+                $accessKey = $data['accessKey'];
+                $secretKey = $data['secretKey'];
+                unset($data['accessKey'], $data['secretKey']);
+            }
+            if (isset($data['appid']) && $data['appid']) {
+                $appid = $data['appid'];
+                unset($data['appid']);
+            }
+            if (isset($data['storageRegion']) && $data['storageRegion']) {
+                $storageRegion = $data['storageRegion'];
+                unset($data['storageRegion']);
+            }
+            if (!$accessKey || !$secretKey) {
+                return true;
+            }
+            /** @var SystemConfigServices $make */
+            $make = app()->make(SystemConfigServices::class);
+            switch ($type) {
+                case 2://七牛
+                    $make->update('qiniu_accessKey', ['value' => json_encode($accessKey)], 'menu_name');
+                    $make->update('qiniu_secretKey', ['value' => json_encode($secretKey)], 'menu_name');
+                    break;
+                case 3:// oss 阿里云
+                    $make->update('accessKey', ['value' => json_encode($accessKey)], 'menu_name');
+                    $make->update('secretKey', ['value' => json_encode($secretKey)], 'menu_name');
+                    break;
+                case 4:// cos 腾讯云
+                    $make->update('tengxun_accessKey', ['value' => json_encode($accessKey)], 'menu_name');
+                    $make->update('tengxun_secretKey', ['value' => json_encode($secretKey)], 'menu_name');
+                    $make->update('tengxun_appid', ['value' => json_encode($appid)], 'menu_name');
+                    break;
+            }
+            CacheService::redisHandler(SystemConfigService::getTag())->clear();
+        }
+    }
+
+    /**
+     * 保存云存储
+     * @param int $type
+     * @param array $data
+     * @return mixed
+     */
+    public function saveStorage(int $type, array $data)
+    {
+        //保存配置信息
+        $this->saveConfig($type, $data);
+        $name = $data['name'];
+
+        switch ($type) {
+            case 3://阿里云oss
+                $data['region'] = $this->getReagionHost($type, $data['region']);
+                break;
+            case 4://腾讯云cos
+                $name = $data['name'] . '-' . sys_config('tengxun_appid');
+                break;
+        }
+
+        if ($this->dao->count(['type' => $type, 'name' => $name])) {
+            throw new ValidateException('云空间名称不能重复');
+        }
+        //保存云存储
+        $data['type'] = $type;
+        $upload = UploadService::init($type);
+        $res = $upload->createBucket($data['name'], $data['region'], $data['acl']);
+        if (false === $res) {
+            throw new ValidateException($upload->getError());
+        }
+
+        $data['domain'] = $this->getDomain($type, $data['name'], $data['region'], sys_config('tengxun_appid'));
+        if (2 === $type) {
+            $domianList = $upload->getDomian($data['name']);
+            $data['domain'] = $domianList[count($domianList) - 1];
+        } else {
+            $data['cname'] = $data['domain'];
+        }
+
+        $data['name'] = $name;
+        $data['add_time'] = time();
+        $data['update_time'] = time();
+        $config = $this->getStorageConfig($type);
+        $data['access_key'] = $config['accessKey'];
+        return $this->save($data);
+    }
+
+    /**
+     * 同步云储存桶
+     * @param int $type
+     * @return bool
+     * @throws \OSS\Core\OssException
+     */
+    public function synchronization(int $type)
+    {
+        $data = [];
+        switch ($type) {
+            case 2://七牛
+                $config = $this->getStorageConfig($type);
+                $upload = UploadService::init($type);
+                $list = $upload->listbuckets();
+                if (false === $list) {
+                    throw new ValidateException('同步失败,失败原因:' . $upload->getError());
+                }
+                foreach ($list as $item) {
+                    if (!$this->dao->count(['name' => $item['id'], 'is_delete' => 0, 'access_key' => $config['accessKey']])) {
+                        $data[] = [
+                            'type' => $type,
+                            'access_key' => $config['accessKey'],
+                            'name' => $item['id'],
+                            'region' => $item['region'],
+                            'acl' => $item['private'] == 0 ? 'public-read' : 'private',
+                            'status' => 0,
+                            'is_delete' => 0,
+                            'add_time' => time(),
+                            'update_time' => time()
+                        ];
+                    }
+                }
+                break;
+            case 3:// oss 阿里云
+                $upload = UploadService::init($type);
+                $list = $upload->listbuckets();
+                $config = $this->getStorageConfig($type);
+                foreach ($list as $item) {
+                    if (!$this->dao->count(['name' => $item['name'], 'is_delete' => 0, 'access_key' => $config['accessKey']])) {
+                        $region = $this->getReagionHost($type, $item['location']);
+                        $data[] = [
+                            'type' => $type,
+                            'access_key' => $config['accessKey'],
+                            'name' => $item['name'],
+                            'region' => $region,
+                            'acl' => 'public-read',
+                            'domain' => $this->getDomain($type, $item['name'], $region),
+                            'status' => 0,
+                            'is_delete' => 0,
+                            'add_time' => strtotime($item['createTime']),
+                            'update_time' => time()
+                        ];
+                    }
+                }
+                break;
+            case 4:// cos 腾讯云
+                $upload = UploadService::init($type);
+                $list = $upload->listbuckets();
+                $config = $this->getStorageConfig($type);
+                foreach ($list as $item) {
+                    if (($id = $this->dao->value(['name' => $item['Name'], 'is_delete' => 0, 'access_key' => $config['accessKey']], 'id'))) {
+                        $this->dao->update($id, [
+                            'update_time' => time(),
+                            'region' => $item['Location'],
+                            'name' => $item['Name'],
+                            'domain' => sys_config('tengxun_appid') ? $this->getDomain($type, $item['Name'], $item['Location']) : '',
+                        ]);
+                    } else {
+                        $data[] = [
+                            'type' => $type,
+                            'access_key' => $config['accessKey'],
+                            'name' => $item['Name'],
+                            'region' => $item['Location'],
+                            'acl' => 'public-read',
+                            'status' => 0,
+                            'domain' => sys_config('tengxun_appid') ? $this->getDomain($type, $item['Name'], $item['Location']) : '',
+                            'is_delete' => 0,
+                            'add_time' => strtotime($item['CreationDate']),
+                            'update_time' => time()
+                        ];
+                    }
+                }
+                break;
+        }
+        if ($data) {
+            $this->saveAll($data);
+        }
+        return true;
+    }
+
+    /**
+     * @param int $type
+     * @param string $reagion
+     * @return mixed|string
+     */
+    public function getReagionHost(int $type, string $reagion)
+    {
+        $upload = UploadService::init($type);
+        $reagionList = $upload->getRegion();
+        foreach ($reagionList as $item) {
+            if (strstr($item['value'], $reagion) !== false) {
+                return $item['value'];
+            }
+        }
+        return '';
+    }
+
+    /**
+     * 获取域名
+     * @param int $type
+     * @param string $name
+     * @param string $reagion
+     * @param string $appid
+     * @return string
+     */
+    public function getDomain(int $type, string $name, string $reagion, string $appid = '')
+    {
+        $domainName = '';
+        switch ($type) {
+            case 3:// oss 阿里云
+                $domainName = 'https://' . $name . '.' . $reagion;
+                break;
+            case 4:// cos 腾讯云
+                $domainName = 'https://' . $name . ($appid ? '-' . $appid : '') . '.cos.' . $reagion . '.myqcloud.com';
+                break;
+        }
+        return $domainName;
+    }
+
+
+    /**
+     * 获取云存储配置
+     * @param int $type
+     * @return array|string[]
+     */
+    public function getConfig(int $type)
+    {
+        $res = ['name' => '', 'region' => '', 'domain' => ''];
+        try {
+            $config = $this->dao->get(['type' => $type, 'status' => 1, 'is_delete' => 0]);
+            if ($config) {
+                return ['name' => $config->name, 'region' => $config->region, 'domain' => $config->domain];
+            }
+        } catch (\Throwable $e) {
+
+        }
+        return $res;
+
+    }
+
+
+    /**
+     * 修改域名并绑定
+     * @param int $id
+     * @param string $domain
+     * @return bool
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public function updateDomain(int $id, string $domain, array $data = [])
+    {
+        $info = $this->get($id);
+        if (!$info) {
+            throw new ValidateException('没有查询到数据');
+        }
+        if ($info->domain != $domain) {
+            $info->domain = $domain;
+            $upload = UploadService::init($info->type);
+            //是否添加过域名不存在需要绑定域名
+            $domainList = $upload->getDomian($info->name, $info->region);
+            $domainParse = parse_url($domain);
+            if (!in_array($domainParse['host'], $domainList)) {
+                //绑定域名到云储存桶
+                $res = $upload->bindDomian($info->name, $domain, $info->region);
+                if (false === $res) {
+                    throw new ValidateException($upload->getError());
+                }
+            }
+            //七牛云需要通过接口获取cname
+            if (2 === ((int)$info->type)) {
+                $resDomain = $upload->getDomianInfo($domain);
+                $info->cname = $resDomain['cname'] ?? '';
+            }
+            return $info->save();
+        }
+
+        if ($info->cdn != $data['cdn']) {
+            $info->cdn = $data['cdn'];
+            $info->save();
+        }
+        return true;
+    }
+}

+ 139 - 0
app/services/system/config/SystemStoreConfigServices.php

@@ -0,0 +1,139 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\services\system\config;
+
+
+use app\model\system\config\SystemStoreConfig;
+use qiniu\basic\BaseServices;
+use think\db\exception\DbException;
+
+/**
+ * Class StoreConfigServices
+ * @package app\services\store
+ * @mixin SystemStoreConfig
+ */
+class SystemStoreConfigServices extends BaseServices
+{
+
+    /**
+     * 表单数据切割符号
+     * @var string
+     */
+    protected $cuttingStr = '=>';
+
+    /**
+     * StoreConfigServices constructor.
+     * @param SystemStoreConfig $model
+     */
+    public function __construct(SystemStoreConfig $model)
+    {
+        $this->model = $model;
+    }
+
+    /**
+     * 保存或者更新门店配置
+     * @param array $data
+     * @param int $store_id
+     * @throws DbException
+     */
+    public function saveConfig(array $data, int $store_id = 0)
+    {
+        $config = [];
+        foreach ($data as $key => $value) {
+            if ($this->getCount(['key_name' => $key, 'store_id' => $store_id])) {
+                $this->update(['key_name' => $key, 'store_id' => $store_id], ['value' => json_encode($value)]);
+            } else {
+                $config[] = [
+                    'key_name' => $key,
+                    'store_id' => $store_id,
+                    'value' => json_encode($value)
+                ];
+            }
+        }
+        if ($config) {
+            $this->saveRows($config);
+        }
+    }
+
+    /**
+     * 获取配置
+     * @param string $key
+     * @param int $store_id
+     * @param null $default
+     * @return mixed|null
+     */
+    public function getConfig(string $key, int $store_id = 0, $default = null)
+    {
+        $value = $this->search(['key_name' => $key, 'store_id' => $store_id])->value('value');
+        return is_null($value) ? $default : json_decode($value, true);
+    }
+
+
+    /**
+     * 获取门店配置
+     * @param array $keys
+     * @param int $store_id
+     * @return array
+     */
+    public function getConfigAll(array $keys, int $store_id = 0)
+    {
+        $config = $this->search(['key_name' => $keys, 'store_id' => $store_id])->column('value', 'key_name');
+        return array_map(function ($item) {
+            return json_decode($item, true);
+        }, $config);
+    }
+
+    public function getOptions(string $parameter)
+    {
+        $parameter = explode("\n", $parameter);
+        $options = [];
+        foreach ($parameter as $v) {
+            if (strstr($v, $this->cuttingStr) !== false) {
+                $pdata = explode($this->cuttingStr, $v);
+                $options[] = ['label' => $pdata[1], 'value' => (int)$pdata[0]];
+            }
+        }
+        return $options;
+    }
+
+    /**
+     * @param array $configName
+     * @param int $store_id
+     * @param int $group
+     * @return array
+     */
+    public function getConfigAllField(array $configName = [], int $store_id = 0, int $group = 0)
+    {
+        /** @var SystemConfigServices $systemConfigServices */
+        $systemConfigServices = app()->make(SystemConfigServices::class);
+        $list = $systemConfigServices->getConfigAllListByWhere(['menu_name' => $configName], $store_id, ['menu_name', 'info', 'type', 'value', 'desc', 'parameter']);
+        if ($list) {
+            foreach ($list as &$item) {
+                if ($store_id != 0) {
+                    $item['value'] = $item['store_value'] ?? '';
+                }
+                $item['value'] = json_decode($item['value'], true);
+            }
+            $list = array_combine(array_column($list, 'menu_name'), $list);
+        }
+
+        $value = [];
+        foreach ($configName as $key) {
+            if ($group) {
+                $value[$key] = $list[$key]['value'] ?? '';
+            } else {
+                $value[$key] = $list[$key] ?? ['info' => '', 'type' => 'text', 'value' => '', 'desc' => '', 'parameter' => ''];
+            }
+        }
+        return $value;
+    }
+}

+ 135 - 0
app/services/system/config/SystemUserLevelServices.php

@@ -0,0 +1,135 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+declare (strict_types=1);
+
+namespace app\services\system\config;
+
+use app\services\user\UserServices;
+use qiniu\basic\BaseServices;
+use app\model\system\config\SystemUserLevel;
+use qiniu\exceptions\AdminException;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+
+/**
+ * 系统设置用户等级
+ * Class SystemUserLevelServices
+ * @package app\services\user\level
+ * @mixin SystemUserLevel
+ */
+class SystemUserLevelServices extends BaseServices
+{
+
+    /**
+     * SystemUserLevelServices constructor.
+     * @param SystemUserLevel $model
+     */
+    public function __construct(SystemUserLevel $model)
+    {
+        $this->model = $model;
+    }
+
+    /**
+     * 单个等级
+     * @param int|array $id
+     * @param string $field
+     * @return array
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getLevel($id, string $field = '*')
+    {
+        if (is_array($id))
+            return $this->getOne($id, $field);
+        return $this->getOne(['id' => $id], $field);
+    }
+
+    /**
+     * 获取所有等级列表
+     * @param array $where
+     * @param string $field
+     * @return array
+     * @throws DbException
+     */
+    public function getLevelList(array $where, string $field = '*')
+    {
+        $where_data = [];
+        if (isset($where['is_show']) && $where['is_show'] !== '') $where_data[] = ['is_show', '=', $where['is_show']];
+        if (isset($where['title']) && $where['title']) $where_data[] = ['name', 'LIKE', "%$where[title]%"];
+        [$page, $limit] = $this->getPageValue();
+        $list = $this->getList($where_data, $field ?? '*', $page, $limit);
+        foreach ($list as &$item) {
+            $item['image'] = set_file_url($item['image']);
+            $item['icon'] = set_file_url($item['icon']);
+        }
+        $count = $this->getCount($where_data);
+        return compact('list', 'count');
+    }
+
+    /**
+     * 会员等级添加或者修改
+     * @param array $data
+     * @return mixed
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function create(array $data)
+    {
+        $gradeLevel = $this->getLevel(['grade' => $data['grade']]);
+        $nameLevel = $this->getLevel(['name' => $data['name']]);
+
+        if ($gradeLevel || $nameLevel) {
+            throw new AdminException('已检测到您设置过的会员等级,此等级不可重复');
+        }
+
+        /** @var SystemUserLevelTaskServices $taskService */
+        $taskService = app()->make(SystemUserLevelTaskServices::class);
+        $data['task'] = $taskService->checkTask($data['task'] ?? []);
+
+        return parent::create($data);
+    }
+
+    public function update($id, array $data, ?string $key = null)
+    {
+
+        $gradeLevel = $this->getLevel(['grade' => $data['grade']]);
+        $nameLevel = $this->getLevel(['name' => $data['name']]);
+
+        if (($gradeLevel && $gradeLevel['id'] != $id) || ($nameLevel && $nameLevel['id'] != $id)) {
+            throw new AdminException('已检测到您设置过的会员等级,此等级不可重复');
+        }
+
+        /** @var SystemUserLevelTaskServices $taskService */
+        $taskService = app()->make(SystemUserLevelTaskServices::class);
+        $data['task'] = $taskService->checkTask($data['task'] ?? []);
+
+        return parent::update($id, $data);
+    }
+
+
+    public function delete($id, ?string $key = null)
+    {
+        $level = $this->getLevel($id);
+        if (!$level) {
+            throw new AdminException('数据不存在');
+        }
+        /** @var UserServices $userServices */
+        $userServices = app()->make(UserServices::class);
+        if ($userServices->be(['level' => $level['id']])) {
+            throw new AdminException('存在用户已是该等级,无法删除');
+        }
+        return parent::delete($id, $key); // TODO: Change the autogenerated stub
+    }
+
+}

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini