user 2 månader sedan
förälder
incheckning
952d566a12
100 ändrade filer med 3773 tillägg och 0 borttagningar
  1. 18 0
      auto_install.json
  2. 26 0
      build.example.php
  3. 49 0
      config/log.php
  4. 161 0
      crmeb/command/Workerman.php
  5. 37 0
      crmeb/exceptions/UploadException.php
  6. 73 0
      crmeb/filetree.txt
  7. 18 0
      crmeb/interfaces/ListenerInterface.php
  8. 192 0
      crmeb/services/CacheService.php
  9. 156 0
      crmeb/services/HttpService.php
  10. 53 0
      crmeb/services/LockService.php
  11. 313 0
      crmeb/services/SpreadsheetExcelService.php
  12. 9 0
      crmeb/services/crud/stubs/controller/create.stub
  13. 52 0
      crmeb/services/crud/stubs/controller/crudController.stub
  14. 10 0
      crmeb/services/crud/stubs/controller/edit.stub
  15. 12 0
      crmeb/services/crud/stubs/controller/index.stub
  16. 10 0
      crmeb/services/crud/stubs/model/getattr.stub
  17. 1 0
      crmeb/services/crud/stubs/route/delete.stub
  18. 1 0
      crmeb/services/crud/stubs/route/update.stub
  19. 10 0
      crmeb/services/crud/stubs/view/api/getCrudCreateApi.stub
  20. 280 0
      crmeb/services/easywechat/v3pay/BaseClient.php
  21. 36 0
      crmeb/services/easywechat/v3pay/ServiceProvider.php
  22. 62 0
      crmeb/services/pay/BasePay.php
  23. 344 0
      crmeb/services/pay/extend/allinpay/AllinPay.php
  24. 102 0
      crmeb/services/pay/storage/AliPay.php
  25. 154 0
      crmeb/services/pay/storage/AllinPay.php
  26. 65 0
      crmeb/services/sms/Sms.php
  27. 133 0
      crmeb/services/template/storage/Wechat.php
  28. 77 0
      crmeb/services/upload/extend/cos/Scope.php
  29. 213 0
      crmeb/services/upload/extend/cos/Signature.php
  30. 438 0
      crmeb/services/upload/storage/Jdoss.php
  31. 198 0
      crmeb/services/workerman/chat/ChatService.php
  32. 119 0
      crmeb/traits/ModelTrait.php
  33. 102 0
      crmeb/utils/JwtAuth.php
  34. 208 0
      crmeb/utils/Terminal.php
  35. 24 0
      filetree.txt
  36. 0 0
      public/admin/css.worker.js
  37. 1 0
      public/admin/system_static/css/chunk-054ceee2.42b60f9c.css
  38. 1 0
      public/admin/system_static/css/chunk-06f6b9ec.726a2d93.css
  39. 1 0
      public/admin/system_static/css/chunk-0a437896.46acedbb.css
  40. 0 0
      public/admin/system_static/css/chunk-2260d7bc.14c700eb.css
  41. 0 0
      public/admin/system_static/css/chunk-255a5262.c1857241.css
  42. 1 0
      public/admin/system_static/css/chunk-27866995.f3bf3511.css
  43. 1 0
      public/admin/system_static/css/chunk-2dd5f758.21cda798.css
  44. 1 0
      public/admin/system_static/css/chunk-2ef23dd9.f386949a.css
  45. 1 0
      public/admin/system_static/css/chunk-2fcc8f66.89adb531.css
  46. 1 0
      public/admin/system_static/css/chunk-37c962e4.d378c462.css
  47. 0 0
      public/admin/system_static/css/chunk-39128d0a.9c8c48d7.css
  48. 1 0
      public/admin/system_static/css/chunk-407053db.de7b2639.css
  49. 0 0
      public/admin/system_static/css/chunk-42301c54.98e7291e.css
  50. 1 0
      public/admin/system_static/css/chunk-459e289b.71ba06fd.css
  51. 0 0
      public/admin/system_static/css/chunk-520bc5d1.0f7e4e10.css
  52. 1 0
      public/admin/system_static/css/chunk-54ffe028.8864ee88.css
  53. 0 0
      public/admin/system_static/css/chunk-5552d05c.b8f99837.css
  54. 0 0
      public/admin/system_static/css/chunk-60512542.a01f46a7.css
  55. 1 0
      public/admin/system_static/css/chunk-69ebc320.2470f470.css
  56. 0 0
      public/admin/system_static/css/chunk-6b55a8d4.fbce5b98.css
  57. 0 0
      public/admin/system_static/css/chunk-7273a738.ccf08c68.css
  58. 0 0
      public/admin/system_static/css/chunk-7a95730c.0b5c5be7.css
  59. 1 0
      public/admin/system_static/css/chunk-80a89046.030e9cdb.css
  60. 1 0
      public/admin/system_static/css/chunk-824bdc78.03faf95f.css
  61. 1 0
      public/admin/system_static/css/chunk-9b878236.c55a5fcc.css
  62. 0 0
      public/admin/system_static/css/chunk-ac9c889e.7a6f46fc.css
  63. 1 0
      public/admin/system_static/css/chunk-e80cb8da.bfd2a79d.css
  64. 0 0
      public/admin/system_static/css/chunk-f010ee82.2c12ce16.css
  65. 0 0
      public/admin/system_static/css/chunk-vendors.1310688b.css
  66. BIN
      public/admin/system_static/fonts/element-icons.535877f5.woff
  67. BIN
      public/admin/system_static/img/bg2.c636f6a6.png
  68. BIN
      public/admin/system_static/img/bluesgin.032bae4b.png
  69. BIN
      public/admin/system_static/img/default.6b914f9c.jpg
  70. BIN
      public/admin/system_static/img/member.b885cf62.png
  71. BIN
      public/admin/system_static/img/mobilehead.1c931282.png
  72. BIN
      public/admin/system_static/img/no-msg.74b02921.png
  73. BIN
      public/admin/system_static/img/no_user.a09b282b.png
  74. BIN
      public/admin/system_static/img/no_zf.e61fe9b5.png
  75. BIN
      public/admin/system_static/img/oragesgin.00077d3a.png
  76. BIN
      public/admin/system_static/img/redsgin.d8b0c12e.png
  77. BIN
      public/admin/system_static/img/sort01.e157a5ea.jpg
  78. BIN
      public/admin/system_static/img/sort02.feab1b79.jpg
  79. 0 0
      public/admin/system_static/js/app.6172b51c.js
  80. 0 0
      public/admin/system_static/js/chunk-008e3316.4897ef32.js
  81. 1 0
      public/admin/system_static/js/chunk-054ceee2.c3584afb.js
  82. 0 0
      public/admin/system_static/js/chunk-06f6b9ec.eb040a4f.js
  83. 0 0
      public/admin/system_static/js/chunk-089b48fd.7e9b3eab.js
  84. 0 0
      public/admin/system_static/js/chunk-176009a8.aec761b6.js
  85. 0 0
      public/admin/system_static/js/chunk-1eb01899.b37b8714.js
  86. 0 0
      public/admin/system_static/js/chunk-226ef389.55807df0.js
  87. 0 0
      public/admin/system_static/js/chunk-27866995.f7a5a1e6.js
  88. 0 0
      public/admin/system_static/js/chunk-2d0aab07.53f43767.js
  89. 0 0
      public/admin/system_static/js/chunk-2d0aeb45.65f1ee16.js
  90. 0 0
      public/admin/system_static/js/chunk-2d0b21d7.4e9c6d4a.js
  91. 0 0
      public/admin/system_static/js/chunk-2d0c46d1.781a78ba.js
  92. 0 0
      public/admin/system_static/js/chunk-2d0c8f4c.a45adcd1.js
  93. 0 0
      public/admin/system_static/js/chunk-2d0d0645.0a3abe36.js
  94. 0 0
      public/admin/system_static/js/chunk-2d0de971.a66aa084.js
  95. 0 0
      public/admin/system_static/js/chunk-2d0e488e.182beb0c.js
  96. 0 0
      public/admin/system_static/js/chunk-2d221799.cadc0e5d.js
  97. 0 0
      public/admin/system_static/js/chunk-2d221814.9cd07d48.js
  98. 0 0
      public/admin/system_static/js/chunk-2d2295e9.c8fc2374.js
  99. 0 0
      public/admin/system_static/js/chunk-2ef23dd9.c8d49cac.js
  100. 0 0
      public/admin/system_static/js/chunk-3899d053.ebd17e02.js

+ 18 - 0
auto_install.json

@@ -0,0 +1,18 @@
+{       
+        "php_ext":"pathinfo,fileinfo,redis",                   
+        "chmod":[
+            {"mode":777,"path":"/runtime"},
+            {"mode":777,"path":"/public"},
+            {"mode":777,"path":"/.env"},
+            {"mode":777,"path":"/.constant"},
+            {"mode":777,"path":"/.version"}
+         ],      
+        "success_url":"/index.php",                             
+        "php_versions":"71,72,73,74", 
+        "db_config":"", 
+        "admin_username":"",
+        "admin_password":"",
+        "run_path":"/public",
+        "remove_file":[],
+        "enable_functions":["pcntl_signal","pcntl_signal_dispatch","pcntl_fork","pcntl_wait","pcntl_alarm","proc_open"]
+}

+ 26 - 0
build.example.php

@@ -0,0 +1,26 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+/**
+ * php think build 自动生成应用的目录结构的定义示例
+ */
+return [
+    // 需要自动创建的文件
+    '__file__'   => [],
+    // 需要自动创建的目录
+    '__dir__'    => ['controller', 'model', 'view'],
+    // 需要自动创建的控制器
+    'controller' => ['Index'],
+    // 需要自动创建的模型
+    'model'      => ['User'],
+    // 需要自动创建的模板
+    'view'       => ['index/index'],
+];

+ 49 - 0
config/log.php

@@ -0,0 +1,49 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+use think\facade\Env;
+
+// +----------------------------------------------------------------------
+// | 日志设置
+// +----------------------------------------------------------------------
+return [
+    // 默认日志记录通道
+    'default'      => Env::get('log.channel', 'file'),
+    // 日志记录级别
+    'level'        => ['error', 'warning', 'fail', 'success', 'info', 'notice', 'crontab', 'crmeb', 'listener'],
+    // 日志类型记录的通道 ['error'=>'email',...]
+    'type_channel' => [],
+    //是否开启业务成功日志
+    'success_log'  => false,
+    //是否开启业务失败日志
+    'fail_log'     => false,
+    //是否开启定时任务日志
+    'timer_log'    => false,
+    //是否开启自定事件日志
+    'listener_log'    => false,
+    // 日志通道列表
+    'channels'     => [
+        'file' => [
+            // 日志记录方式
+            'type'        => 'File',
+            // 日志保存目录
+            'path'        => app()->getRuntimePath() . 'log' . DIRECTORY_SEPARATOR,
+            // 单文件日志写入
+            'single'      => false,
+            // 独立日志级别
+            'apart_level' => ['error', 'fail', 'success', 'crontab', 'crmeb', 'listener'],
+            // 最大日志文件数量
+            'max_files'   => 60,
+            'time_format' => 'Y-m-d H:i:s',
+            'format'      => '%s|%s|%s'
+        ],
+        // 其它日志通道配置
+    ],
+];

+ 161 - 0
crmeb/command/Workerman.php

@@ -0,0 +1,161 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+namespace crmeb\command;
+
+use app\services\system\config\SystemConfigServices;
+use Channel\Server;
+use crmeb\services\workerman\chat\ChatService;
+use crmeb\services\workerman\WorkermanService;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\input\Option;
+use think\console\Output;
+use Workerman\Worker;
+
+class Workerman extends Command
+{
+    /**
+     * @var array
+     */
+    protected $config = [];
+
+    /**
+     * @var Worker
+     */
+    protected $workerServer;
+
+    /**
+     * @var Worker
+     */
+    protected $chatWorkerServer;
+
+    /**
+     * @var Server
+     */
+    protected $channelServer;
+
+    /**
+     * @var Input
+     */
+    public $input;
+
+    /**
+     * @var Output
+     */
+    public $output;
+
+    protected function configure()
+    {
+        // 指令配置
+        $this->setName('workerman')
+            ->addArgument('status', Argument::REQUIRED, 'start/stop/reload/status/connections')
+            ->addArgument('server', Argument::OPTIONAL, 'admin/chat/channel')
+            ->addOption('d', null, Option::VALUE_NONE, 'daemon(守护进程)方式启动')
+            ->setDescription('start/stop/restart workerman');
+    }
+
+    protected function init(Input $input, Output $output)
+    {
+        global $argv;
+        $argv[1] = $input->getArgument('status') ?: 'start';
+        $server = $input->getArgument('server');
+        if ($input->hasOption('d')) {
+            $argv[2] = '-d';
+        } else {
+            unset($argv[2]);
+        }
+
+        $this->config = config('workerman');
+
+        return $server;
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        $server = $this->init($input, $output);
+        /** @var SystemConfigServices $services */
+        $services = app()->make(SystemConfigServices::class);
+        $sslConfig = $services->getSslFilePath();
+//        $confing['wss_open'] = $sslConfig['wssOpen'] ?? 0;
+        $confing['wss_open'] = 0;
+        $confing['wss_local_cert'] = $sslConfig['wssLocalCert'] ?? '';
+        $confing['wss_local_pk'] = $sslConfig['wssLocalpk'] ?? '';
+        // 证书最好是申请的证书
+        if ($confing['wss_open']) {
+            $context = [
+                'ssl' => [
+                    // 请使用绝对路径
+                    'local_cert' => realpath('public' . $confing['wss_local_cert']), // 也可以是crt文件
+                    'local_pk' => realpath('public' . $confing['wss_local_pk']),
+                    'verify_peer' => false,
+                ]
+            ];
+        } else {
+            $context = [];
+        }
+        Worker::$pidFile = app()->getRootPath() . 'runtime/workerman.pid';
+        if (!$server || $server == 'admin') {
+            var_dump('admin');
+            //创建 admin 长连接服务
+            $this->workerServer = new Worker($this->config['admin']['protocol'] . '://' . $this->config['admin']['ip'] . ':' . $this->config['admin']['port'], $context);
+            $this->workerServer->count = $this->config['admin']['serverCount'];
+            if ($confing['wss_open']) {
+                $this->workerServer->transport = 'ssl';
+            }
+        }
+
+        if (!$server || $server == 'chat') {
+            var_dump('chat');
+            //创建 h5 chat 长连接服务
+            $this->chatWorkerServer = new Worker($this->config['chat']['protocol'] . '://' . $this->config['chat']['ip'] . ':' . $this->config['chat']['port'], $context);
+            $this->chatWorkerServer->count = $this->config['chat']['serverCount'];
+            if ($confing['wss_open']) {
+                $this->chatWorkerServer->transport = 'ssl';
+            }
+        }
+
+        if (!$server || $server == 'channel') {
+            var_dump('channel');
+            //创建内部通讯服务
+            $this->channelServer = new Server($this->config['channel']['ip'], $this->config['channel']['port']);
+        }
+        $this->bindHandle();
+        try {
+            Worker::runAll();
+        } catch (\Exception $e) {
+            $output->warning($e->getMessage());
+        }
+    }
+
+    protected function bindHandle()
+    {
+        if (!is_null($this->workerServer)) {
+            $server = new WorkermanService($this->workerServer, $this->channelServer);
+            // 连接时回调
+            $this->workerServer->onConnect = [$server, 'onConnect'];
+            // 收到客户端信息时回调
+            $this->workerServer->onMessage = [$server, 'onMessage'];
+            // 进程启动后的回调
+            $this->workerServer->onWorkerStart = [$server, 'onWorkerStart'];
+            // 断开时触发的回调
+            $this->workerServer->onClose = [$server, 'onClose'];
+        }
+
+        if (!is_null($this->chatWorkerServer)) {
+            $chatServer = new ChatService($this->chatWorkerServer, $this->channelServer);
+            $this->chatWorkerServer->onConnect = [$chatServer, 'onConnect'];
+            $this->chatWorkerServer->onMessage = [$chatServer, 'onMessage'];
+            $this->chatWorkerServer->onWorkerStart = [$chatServer, 'onWorkerStart'];
+            $this->chatWorkerServer->onClose = [$chatServer, 'onClose'];
+        }
+    }
+}

+ 37 - 0
crmeb/exceptions/UploadException.php

@@ -0,0 +1,37 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace crmeb\exceptions;
+
+/**
+ * Class AuthException
+ * @package crmeb\exceptions
+ */
+class UploadException extends \RuntimeException
+{
+    public function __construct($message, $replace = [], $code = 0, \Throwable $previous = null)
+    {
+        if (is_array($message)) {
+            $errInfo = $message;
+            $message = $errInfo[1] ?? '未知错误';
+            if ($code === 0) {
+                $code = $errInfo[0] ?? 400;
+            }
+        }
+
+        if (is_numeric($message)) {
+            $code = $message;
+            $message = getLang($message, $replace);
+        }
+
+        parent::__construct($message, $code, $previous);
+    }
+}

+ 73 - 0
crmeb/filetree.txt

@@ -0,0 +1,73 @@
+.
+├── basic
+│   ├── BaseController.php //控制器基类
+│   ├── BaseJobs.php //队列基类
+│   ├── BaseManager.php //驱动管理基类
+│   ├── BaseModel.php //model基类
+│   └── BaseStorage.php //驱动基类
+├── command
+│   ├── stubs //命令模版
+│   ├── Dao.php //创建Dao类命令
+│   ├── Service.php //创建Service类命令
+│   ├── Timer.php //定时任务命令类
+│   └── Workerman.php //workman命令类
+├── exceptions
+│   ├── AdminException.php //后台管理端异常处理
+│   ├── ApiException.php //移动端接口异常处理
+│   ├── AuthException.php //用户授权异常处理
+│   ├── PayException.php //支付异常处理
+│   ├── SmsException.php //短信异常处理
+│   ├── TemplateException.php //微信模版消息异常处理
+│   ├── UploadException.php //上传异常处理
+│   └── WechatReplyException.php //微信自动回复异常处理
+├── interfaces
+│   ├── JobInterface.php //队列任务
+│   ├── ListenerInterface.php //事件
+│   ├── MiddlewareInterface.php //中间件
+│   └── ProviderInterface.php //容器
+├── services
+│   ├── app //应用端
+│   ├── easywechat //easywechat
+│   ├── express //物流查询
+│   ├── pay //支付
+│   ├── printer //打印机
+│   ├── copyproduct //采集商品
+│   ├── serve //服务平台
+│   ├── sms //短信
+│   ├── template //模版消息
+│   ├── upload //上传
+│   ├── workerman //workerman
+│   ├── AccessTokenServeService.php //获取token
+│   ├── AliPayService.php
+│   ├── CacheService.php //缓存类
+│   ├── FileService.php //文件处理类
+│   ├── FormBuilder.php //自动生成表单类
+│   ├── GroupDataService.php //组合数据类
+│   ├── HttpService.php //curl 请求处理类
+│   ├── MysqlBackupService.php //数据库备份管理类
+│   ├── SpreadsheetExcelService.php //excel处理类
+│   ├── SystemConfigService.php //系统配置获取类
+│   ├── UpgradeService.php //系统更新类
+│   └──  UploadService.php //文件上传类
+├── traits
+│   ├── ErrorTrait.php //错误处理扩展类
+│   ├── JwtAuthModelTrait.php //Jwt鉴权扩展类
+│   ├── ModelTrait.php //model扩展类
+│   ├── QueueTrait.php //队列扩展累
+│   ├── SearchDaoTrait.php //dao扩展类
+│   └── ServicesTrait.php //Services扩展类
+├── utils
+│   ├── Arr.php //操作数组帮助类
+│   ├── Canvas.php //图片处理类
+│   ├── Captcha.php //验证码类
+│   ├── DownloadImage.php //远程图片下载类
+│   ├── ErrorCode.php //错误码处理类
+│   ├── Hook.php //钩子类
+│   ├── Json.php //Json处理类
+│   ├── JwtAuth.php //Jwt鉴权类
+│   ├── QRcode.php //二维码获取类 TODO
+│   ├── Queue.php //队列类
+│   └── Str.php //字符处理类
+└── filetree.txt
+
+17 directories, 70 files

+ 18 - 0
crmeb/interfaces/ListenerInterface.php

@@ -0,0 +1,18 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace crmeb\interfaces;
+
+
+interface ListenerInterface
+{
+    public function handle($event): void;
+}

+ 192 - 0
crmeb/services/CacheService.php

@@ -0,0 +1,192 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace crmeb\services;
+
+use think\facade\Cache;
+use think\facade\Config;
+use think\cache\TagSet;
+
+/**
+ * CRMEB 缓存类
+ * Class CacheService
+ * @package crmeb\services
+ */
+class CacheService
+{
+    /**
+     * 过期时间
+     * @var int
+     */
+    protected static $expire;
+
+    /**
+     * 写入缓存
+     * @param string $name 缓存名称
+     * @param mixed $value 缓存值
+     * @param int|null $expire 缓存时间,为0读取系统缓存时间
+     */
+    public static function set(string $name, $value, int $expire = 0, string $tag = 'crmeb')
+    {
+        try {
+            return Cache::tag($tag)->set($name, $value, $expire);
+        } catch (\Throwable $e) {
+            return false;
+        }
+    }
+
+    /**
+     * 如果不存在则写入缓存
+     * @param string $name
+     * @param mixed $default
+     * @param int|null $expire
+     * @param string $tag
+     * @return mixed|string|null
+     */
+    public static function remember(string $name, $default = '', int $expire = 0, string $tag = 'crmeb')
+    {
+        try {
+            return Cache::tag($tag)->remember($name, $default, $expire);
+        } catch (\Throwable $e) {
+            try {
+                if (is_callable($default)) {
+                    return $default();
+                } else {
+                    return $default;
+                }
+            } catch (\Throwable $e) {
+                return null;
+            }
+        }
+    }
+
+    /**
+     * 读取缓存
+     * @param string $name
+     * @param mixed $default
+     * @return mixed|string
+     */
+    public static function get(string $name, $default = '')
+    {
+        return Cache::get($name) ?? $default;
+    }
+
+    /**
+     * 删除缓存
+     * @param string $name
+     * @return bool
+     */
+    public static function delete(string $name)
+    {
+        return Cache::delete($name);
+    }
+
+    /**
+     * 清空缓存池
+     * @return bool
+     */
+    public static function clear(string $tag = 'crmeb')
+    {
+        return Cache::tag($tag)->clear();
+    }
+
+    /**
+     * 清空全部缓存
+     * @return bool
+     * @author 吴汐
+     * @email 442384644@qq.com
+     * @date 2023/12/19
+     */
+    public static function clearAll()
+    {
+        return Cache::clear();
+    }
+
+    /**
+     * 检查缓存是否存在
+     * @param string $key
+     * @return bool
+     */
+    public static function has(string $key)
+    {
+        try {
+            return Cache::has($key);
+        } catch (\Throwable $e) {
+            return false;
+        }
+    }
+
+    /**
+     * 指定缓存类型
+     * @param string $type
+     * @param string $tag
+     * @return TagSet
+     */
+    public static function store(string $type = 'file', string $tag = 'crmeb')
+    {
+        return Cache::store($type)->tag($tag);
+    }
+
+    /**
+     * 检查锁
+     * @param string $key
+     * @param int $timeout
+     * @return bool
+     */
+    public static function setMutex(string $key, int $timeout = 10): bool
+    {
+        $curTime = time();
+        $readMutexKey = "redis:mutex:{$key}";
+        $mutexRes = Cache::store('redis')->handler()->setnx($readMutexKey, $curTime + $timeout);
+        if ($mutexRes) {
+            return true;
+        }
+        //就算意外退出,下次进来也会检查key,防止死锁
+        $time = Cache::store('redis')->handler()->get($readMutexKey);
+        if ($curTime > $time) {
+            Cache::store('redis')->handler()->del($readMutexKey);
+            return Cache::store('redis')->handler()->setnx($readMutexKey, $curTime + $timeout);
+        }
+        return false;
+    }
+
+    /**
+     * 删除锁
+     * @param string $key
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/11/22
+     */
+    public static function delMutex(string $key)
+    {
+        $readMutexKey = "redis:mutex:{$key}";
+        Cache::store('redis')->handler()->del($readMutexKey);
+    }
+
+
+    /**
+     * 数据库锁
+     * @param $key
+     * @param $fn
+     * @param int $ex
+     * @return mixed
+     * @author 吴汐
+     * @email 442384644@qq.com
+     * @date 2023/03/01
+     */
+    public static function lock($key, $fn, int $ex = 6)
+    {
+        if (Config::get('cache.default') == 'file') {
+            return $fn();
+        }
+        return app()->make(LockService::class)->exec($key, $fn, $ex);
+    }
+}

+ 156 - 0
crmeb/services/HttpService.php

@@ -0,0 +1,156 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace crmeb\services;
+
+/**
+ * Class HttpService
+ * @package crmeb\services
+ */
+class HttpService
+{
+    /**
+     * 错误信息
+     * @var string
+     */
+    private static $curlError;
+
+    /**
+     * header头信息
+     * @var string
+     */
+    private static $headerStr;
+
+    /**
+     * 请求状态
+     * @var int
+     */
+    private static $status;
+
+    /**
+     * @return string
+     */
+    public static function getCurlError()
+    {
+        return self::$curlError;
+    }
+
+    /**
+     * @return mixed
+     */
+    public static function getStatus()
+    {
+        return self::$status;
+    }
+
+    /**
+     * 模拟GET发起请求
+     * @param $url
+     * @param array $data
+     * @param bool $header
+     * @param int $timeout
+     * @return bool|string
+     */
+    public static function getRequest($url, $data = array(), $header = false, $timeout = 10)
+    {
+        if (!empty($data)) {
+            $url .= (stripos($url, '?') === false ? '?' : '&');
+            $url .= (is_array($data) ? http_build_query($data) : $data);
+        }
+
+        return self::request($url, 'get', array(), $header, $timeout);
+    }
+
+    /**
+     * curl 请求
+     * @param $url
+     * @param string $method
+     * @param array $data
+     * @param bool $header
+     * @param int $timeout
+     * @return bool|string
+     */
+    public static function request($url, $method = 'get', $data = array(), $header = false, $timeout = 15)
+    {
+        self::$status = null;
+        self::$curlError = null;
+        self::$headerStr = null;
+
+        $curl = curl_init($url);
+        $method = strtoupper($method);
+        //请求方式
+        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
+        //携带参数
+        if ($method == 'POST') {
+            curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($data));
+        } elseif ($method == 'GET' && count($data)) {
+            $url .= '?' . http_build_query($data);
+            curl_setopt($curl, CURLOPT_URL, $url);
+        }
+        //超时时间
+        curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
+        //设置header头
+        if ($header !== false) curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
+
+        curl_setopt($curl, CURLOPT_FAILONERROR, false);
+        //返回抓取数据
+        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+        //输出header头信息
+        curl_setopt($curl, CURLOPT_HEADER, true);
+        //TRUE 时追踪句柄的请求字符串,从 PHP 5.1.3 开始可用。这个很关键,就是允许你查看请求header
+        curl_setopt($curl, CURLINFO_HEADER_OUT, true);
+        //https请求
+        if (1 == strpos("$" . $url, "https://")) {
+            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
+            curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
+        }
+        self::$curlError = curl_error($curl);
+
+        list($content, $status) = [curl_exec($curl), curl_getinfo($curl), curl_close($curl)];
+        self::$status = $status;
+        self::$headerStr = trim(substr($content, 0, $status['header_size']));
+        $content = trim(substr($content, $status['header_size']));
+        return (intval($status["http_code"]) === 200) ? $content : false;
+    }
+
+    /**
+     * 模拟POST发起请求
+     * @param $url
+     * @param $data
+     * @param bool $header
+     * @param int $timeout
+     * @return bool|string
+     */
+    public static function postRequest($url, $data = array(), $header = false, $timeout = 10)
+    {
+        return self::request($url, 'post', $data, $header, $timeout);
+    }
+
+    /**
+     * 获取header头字符串类型
+     * @return mixed
+     */
+    public static function getHeaderStr()
+    {
+        return self::$headerStr;
+    }
+
+    /**
+     * 获取header头数组类型
+     * @return array
+     */
+    public static function getHeader()
+    {
+        $headArr = explode("\r\n", self::$headerStr);
+        return $headArr;
+    }
+
+}

+ 53 - 0
crmeb/services/LockService.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace crmeb\services;
+
+use think\facade\Cache;
+
+class LockService
+{
+    /**
+     * @param $key
+     * @param $fn
+     * @param int $ex
+     * @return mixed
+     * @author 吴汐
+     * @email 442384644@qq.com
+     * @date 2023/03/01
+     */
+    public function exec($key, $fn, int $ex = 6)
+    {
+        try {
+            $this->lock($key, $key, $ex);
+            return $fn();
+        } finally {
+            $this->unlock($key, $key);
+        }
+    }
+
+    public function tryLock($key, $value = '1', $ex = 6)
+    {
+        return Cache::store('redis')->handler()->set('lock_' . $key, $value, ["NX", "EX" => $ex]);
+    }
+
+    public function lock($key, $value = '1', $ex = 6)
+    {
+        if ($this->tryLock($key, $value, $ex)) {
+            return true;
+        }
+        usleep(200);
+        $this->lock($key, $value, $ex);
+    }
+
+    public function unlock($key, $value = '1')
+    {
+        $script = <<< EOF
+if (redis.call("get", "lock_" .. KEYS[1]) == ARGV[1]) then
+	return redis.call("del", "lock_" .. KEYS[1])
+else
+	return 0
+end
+EOF;
+        return Cache::store('redis')->handler()->eval($script, [$key, $value], 1) > 0;
+    }
+}

+ 313 - 0
crmeb/services/SpreadsheetExcelService.php

@@ -0,0 +1,313 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+namespace crmeb\services;
+
+
+use PhpOffice\PhpSpreadsheet\Style\Alignment;
+use PhpOffice\PhpSpreadsheet\Writer\Exception;
+
+class SpreadsheetExcelService
+{
+    //
+    private static $instance = null;
+    //PHPSpreadsheet实例化对象
+    private static $spreadsheet = null;
+    //sheet实例化对象
+    private static $sheet = null;
+    //表头计数
+    protected static $count;
+    //表头占行数
+    protected static $topNumber = 3;
+    //表能占据表行的字母对应self::$cellkey
+    protected static $cells;
+    //表头数据
+    protected static $data = [];
+    //文件名
+    protected static $title = '订单导出';
+    //行宽
+    protected static $width = 20;
+    //行高
+    protected static $height = 50;
+    //保存文件目录
+    protected static $path = './phpExcel/';
+    //设置style
+    private static $styleArray = [
+//         'borders' => [
+//             'allBorders' => [
+// //                PHPExcel_Style_Border里面有很多属性,想要其他的自己去看
+//                // 'style' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THICK,//边框是粗的
+// //                'style' => \PHPExcel_Style_Border::BORDER_DOUBLE,//双重的
+// //                'style' => \PHPExcel_Style_Border::BORDER_HAIR,//虚线
+// //                'style' => \PHPExcel_Style_Border::BORDER_MEDIUM,//实粗线
+// //                'style' => \PHPExcel_Style_Border::BORDER_MEDIUMDASHDOT,//虚粗线
+// //                'style' => \PHPExcel_Style_Border::BORDER_MEDIUMDASHDOTDOT,//点虚粗线
+//                 'style' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN,//细边框
+//                 // 'color' => ['argb' => 'FFFF0000'],
+//             ],
+//         ],
+        'font' => [
+            'bold' => true
+        ],
+        'alignment' => [
+            'horizontal' => Alignment::HORIZONTAL_CENTER,
+            'vertical' => Alignment::VERTICAL_CENTER
+        ]
+    ];
+
+    private function __construct()
+    {
+    }
+
+    private function __clone()
+    {
+    }
+
+    public static function instance()
+    {
+        if (self::$instance === null) {
+            self::$instance = new self();
+            self::$spreadsheet = $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
+            self::$sheet = $spreadsheet->getActiveSheet();
+        }
+        return self::$instance;
+    }
+
+    /**
+     *设置字体格式
+     * @param $title string 必选
+     * return string
+     */
+    public static function setUtf8(string $title)
+    {
+        return iconv('utf-8', 'gb2312', $title);
+    }
+
+    /**
+     *  创建保存excel目录
+     *  return string
+     */
+    public static function savePath()
+    {
+        if (!is_dir(self::$path)) {
+            if (mkdir(self::$path, 0700) == false) {
+                return false;
+            }
+        }
+        //年月一级目录
+        $mont_path = self::$path . date('Ym');
+        if (!is_dir($mont_path)) {
+            if (mkdir($mont_path, 0700) == false) {
+                return false;
+            }
+        }
+        //日二级目录
+        $day_path = $mont_path . '/' . date('d');
+        if (!is_dir($day_path)) {
+            if (mkdir($day_path, 0700) == false) {
+                return false;
+            }
+        }
+        return $day_path;
+    }
+
+    /**
+     * 设置标题
+     * @param $title string || array ['title'=>'','name'=>'','info'=>[]]
+     * @param $Name string
+     * @param $info string || array;
+     * @return $this
+     */
+    public function setExcelTile(string $title = '', string $Name = '', $info = [])
+    {
+        //设置参数
+        if (is_array($title)) {
+            if (isset($title['title'])) $title = $title['title'];
+            if (isset($title['name'])) $Name = $title['name'];
+            if (isset($title['info'])) $info = $title['info'];
+        }
+        if (empty($title))
+            $title = self::$title;
+        else
+            self::$title = $title;
+
+        if (empty($Name)) $Name = time();
+        //设置Excel属性
+        self::$spreadsheet->getProperties()
+            ->setCreator("Neo")
+            ->setLastModifiedBy("Neo")
+            ->setTitle(self::setUtf8($title))
+            ->setSubject($Name)
+            ->setDescription("")
+            ->setKeywords($Name)
+            ->setCategory("");
+        self::$sheet->setTitle($Name);
+        self::$sheet->setCellValue('A1', $title);
+        self::$sheet->setCellValue('A2', self::setCellInfo($info));
+        //文字居中
+        self::$sheet->getStyle('A1')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
+        self::$sheet->getStyle('A2')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
+
+        //合并表头单元格
+        self::$sheet->mergeCells('A1:' . self::$cells . '1');
+        self::$sheet->mergeCells('A2:' . self::$cells . '2');
+
+        self::$sheet->getRowDimension(1)->setRowHeight(40);
+        self::$sheet->getRowDimension(2)->setRowHeight(20);
+
+        //设置表头字体
+        self::$sheet->getStyle('A1')->getFont()->setName('黑体');
+        self::$sheet->getStyle('A1')->getFont()->setSize(20);
+        self::$sheet->getStyle('A1')->getFont()->setBold(true);
+        self::$sheet->getStyle('A2')->getFont()->setName('宋体');
+        self::$sheet->getStyle('A2')->getFont()->setSize(14);
+        self::$sheet->getStyle('A2')->getFont()->setBold(true);
+
+        self::$sheet->getStyle('A3:' . self::$cells . '3')->getFont()->setBold(true);
+        return $this;
+    }
+
+    /**
+     * 设置第二行标题内容
+     * @param $info
+     * @return string|void
+     * @author: 吴汐
+     * @email: 442384644@qq.com
+     * @date: 2023/8/7
+     */
+    private static function setCellInfo($info)
+    {
+        $content = ['操作者:', '导出日期:' . date('Y-m-d', time()), '地址:', '电话:'];
+        if (is_array($info) && !empty($info)) {
+            if (isset($info['name'])) {
+                $content[0] .= $info['name'];
+            } else {
+                $content[0] .= $info[0] ?? '';
+            }
+            if (isset($info['site'])) {
+                $content[2] .= $info['site'];
+            } else {
+                $content[2] .= $info[1] ?? '';
+            }
+            if (isset($info['phone'])) {
+                $content[3] .= $info['phone'];
+            } else {
+                $content[3] .= $info[2] ?? '';
+            }
+            return implode(' ', $content);
+        } else if (is_string($info)) {
+            return empty($info) ? implode(' ', $content) : $info;
+        }
+    }
+
+    /**
+     * 设置头部信息
+     * @param $data array
+     * @return $this
+     */
+    public static function setExcelHeader(array $data)
+    {
+        $span = 'A';
+        foreach ($data as $value) {
+            self::$sheet->getColumnDimension($span)->setWidth(self::$width);
+            self::$sheet->setCellValue($span . self::$topNumber, $value);
+            $span++;
+        }
+        self::$sheet->getRowDimension(3)->setRowHeight(self::$height);
+        self::$cells = $span;
+        return new self;
+    }
+
+    /**
+     *
+     * excl数据导出
+     * @param  $data 需要导出的数据 格式和以前的可是一样
+     *
+     * 特殊处理:合并单元格需要先对数据进行处理
+     */
+    public function setExcelContent($data = [])
+    {
+        if (!empty($data) && is_array($data)) {
+            $span = '';
+            $column = self::$topNumber + 1;
+            // 行写入
+            foreach ($data as $rows) {
+                $span = 'A';
+                // 列写入
+                foreach ($rows as $value) {
+                    self::$sheet->setCellValue($span . $column, $value);
+                    $span++;
+                }
+                $column++;
+            }
+            self::$sheet->getDefaultRowDimension()->setRowHeight(self::$height);
+            //设置内容字体样式
+            self::$sheet->getStyle('A1:' . $span . $column)->applyFromArray(self::$styleArray);
+            //设置边框
+            self::$sheet->getStyle('A1:' . $span . $column)->getBorders()->getAllBorders()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN);
+            //设置自动换行
+            self::$sheet->getStyle('A4:' . $span . $column)->getAlignment()->setWrapText(true);
+        }
+        return new self;
+    }
+
+    /**
+     * 保存表格数据,直接下载
+     * @param string $fileName
+     * @param string $suffix 文件后缀名
+     * @param bool $is_save 是否保存文件
+     * @return string string
+     * @throws Exception
+     */
+    public function excelSave(string $fileName = '', string $suffix = 'xlsx', bool $is_save = false)
+    {
+        if (empty($fileName)) {
+            $fileName = date('YmdHis') . time();
+        }
+        if (empty($suffix)) {
+            $suffix = 'xlsx';
+        }
+        // 重命名表(UTF8编码不需要这一步)
+        if (mb_detect_encoding($fileName) != "UTF-8") {
+            $fileName = iconv("utf-8", "gbk//IGNORE", $fileName);
+        }
+        if ($suffix == 'xlsx') {
+            header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
+            $class = "\PhpOffice\PhpSpreadsheet\Writer\Xlsx";
+        } elseif ($suffix == 'xls') {
+            header('Content-Type:application/vnd.ms-excel');
+            $class = "\PhpOffice\PhpSpreadsheet\Writer\Xls";
+        }
+        // 清理缓存
+//        ob_end_clean();
+        $spreadsheet = self::$spreadsheet;
+        $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
+        if (!$is_save) {//直接下载
+
+            header('Content-Disposition: attachment;filename="' . $fileName . '.' . $suffix . '"');
+            header('Cache-Control: max-age=0');
+            $writer->save('php://output');
+            // 删除清空 释放内存
+            $spreadsheet->disconnectWorksheets();
+            unset($spreadsheet);
+            exit;
+        } else {//保存文件
+            $path = self::savePath() . '/' . $fileName . '.' . $suffix;
+            //$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xlsx');
+            //$writer->save($path);
+            $writer->save(public_path() . $path);
+            // 删除清空 释放内存
+            $spreadsheet->disconnectWorksheets();
+            unset($spreadsheet);
+            return $path;
+        }
+    }
+
+}

+ 9 - 0
crmeb/services/crud/stubs/controller/create.stub

@@ -0,0 +1,9 @@
+    /**
+     * 创建
+     * @return \think\Response
+     * @date {%DATE%}
+     */
+    public function create()
+    {
+        return app('json')->success($this->service->getCrudForm());
+    }

+ 52 - 0
crmeb/services/crud/stubs/controller/crudController.stub

@@ -0,0 +1,52 @@
+<?php
+/**
+ *  +----------------------------------------------------------------------
+ *  | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+ *  +----------------------------------------------------------------------
+ *  | Copyright (c) 2016~{%YEAR%} https://www.crmeb.com All rights reserved.
+ *  +----------------------------------------------------------------------
+ *  | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+ *  +----------------------------------------------------------------------
+ *  | Author: CRMEB Team <admin@crmeb.com>
+ *  +----------------------------------------------------------------------
+ */
+
+/**
+ * {%MODEL_NAME%}
+ * @author crud自动生成代码
+ * @date {%DATE%}
+ */
+
+namespace app\adminapi\controller\crud{%PATH%};
+
+use app\adminapi\controller\AuthController;
+use think\facade\App;
+{%USE_PHP%}
+/**
+ * Class {%NAME_CAMEL%}
+ * @date {%DATE%}
+ * @package app\adminapi\controller\crud{%PATH%}
+ */
+class {%NAME_CAMEL%} extends AuthController
+{
+
+    /**
+     * @var {%NAME_CAMEL%}Services
+     */
+    protected $service;
+
+    /**
+     * {%NAME_CAMEL%}Controller constructor.
+     * @param App $app
+     * @param {%NAME_CAMEL%}Services $service
+     */
+    public function __construct(App $app, {%NAME_CAMEL%}Services $service)
+    {
+        parent::__construct($app);
+        $this->service = $service;
+    }
+
+
+{%CONTENT_PHP%}
+
+}

+ 10 - 0
crmeb/services/crud/stubs/controller/edit.stub

@@ -0,0 +1,10 @@
+    /**
+     * 编辑获取数据
+     * @param $id
+     * @return \think\Response
+     * @date {%DATE%}
+     */
+    public function edit($id)
+    {
+        return app('json')->success($this->service->getCrudForm((int)$id));
+    }

+ 12 - 0
crmeb/services/crud/stubs/controller/index.stub

@@ -0,0 +1,12 @@
+    /**
+     * 列表
+     * @date {%DATE%}
+     * @return \think\Response
+     */
+    public function index()
+    {
+        $where = $this->request->getMore([
+{%FIELD_SEARCH_PHP%}
+        ]);
+        return app('json')->success($this->service->getCrudListIndex($where));
+    }

+ 10 - 0
crmeb/services/crud/stubs/model/getattr.stub

@@ -0,0 +1,10 @@
+    /**
+     * {%NAME%}获取器
+     * @date {%DATE%}
+     * @param string $value
+     * @return string
+     */
+    public function get{%FIELD%}Attr($value)
+    {
+{%CONTENT_PHP%}
+    }

+ 1 - 0
crmeb/services/crud/stubs/route/delete.stub

@@ -0,0 +1 @@
+Route::delete('{%ROUTE%}/:id', 'crud.{%ROUTE_PATH%}{%CONTROLLER%}/delete')->option(['real_name' => '{%MENUS%}删除接口']);

+ 1 - 0
crmeb/services/crud/stubs/route/update.stub

@@ -0,0 +1 @@
+Route::put('{%ROUTE%}/:id', 'crud.{%ROUTE_PATH%}{%CONTROLLER%}/update')->option(['real_name' => '{%MENUS%}修改接口']);

+ 10 - 0
crmeb/services/crud/stubs/view/api/getCrudCreateApi.stub

@@ -0,0 +1,10 @@
+/**
+ * 获取添加表单数据
+ * @return {*}
+ */
+export function get{%NAME_STUDLY%}CreateApi() {
+    return request({
+        url: '{%ROUTE%}/create',
+        method: 'get',
+    });
+}

+ 280 - 0
crmeb/services/easywechat/v3pay/BaseClient.php

@@ -0,0 +1,280 @@
+<?php
+/**
+ *  +----------------------------------------------------------------------
+ *  | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+ *  +----------------------------------------------------------------------
+ *  | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+ *  +----------------------------------------------------------------------
+ *  | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+ *  +----------------------------------------------------------------------
+ *  | Author: CRMEB Team <admin@crmeb.com>
+ *  +----------------------------------------------------------------------
+ */
+
+namespace crmeb\services\easywechat\v3pay;
+
+
+use crmeb\exceptions\PayException;
+use crmeb\services\easywechat\Application;
+use EasyWeChat\Core\AbstractAPI;
+use EasyWeChat\Core\AccessToken;
+use EasyWeChat\Core\Exceptions\HttpException;
+use EasyWeChat\Core\Exceptions\InvalidConfigException;
+use EasyWeChat\Core\Http;
+use EasyWeChat\Encryption\EncryptionException;
+use think\exception\InvalidArgumentException;
+
+
+class BaseClient extends AbstractAPI
+{
+
+    use Certficates;
+
+    /**
+     * @var Application
+     */
+    protected $app;
+
+    const BASE_URL = 'https://api.mch.weixin.qq.com/';
+
+    const KEY_LENGTH_BYTE = 32;
+
+    const AUTH_TAG_LENGTH_BYTE = 16;
+
+    /**
+     * BaseClient constructor.
+     * @param AccessToken $accessToken
+     * @param $app
+     */
+    public function __construct(AccessToken $accessToken, $app)
+    {
+        parent::__construct($accessToken);
+        $this->app = $app;
+    }
+
+    /**
+     * request.
+     *
+     * @param string $endpoint
+     * @param string $method
+     * @param array $options
+     * @param bool $returnResponse
+     */
+    public function request(string $endpoint, string $method = 'POST', array $options = [], $serial = true)
+    {
+        $body = $options['body'] ?? '';
+
+        if (isset($options['json'])) {
+            $body = json_encode($options['json']);
+            $options['body'] = $body;
+            unset($options['json']);
+        }
+
+        $headers = [
+            'Content-Type' => 'application/json',
+            'User-Agent' => 'curl',
+            'Accept' => 'application/json',
+            'Authorization' => $this->getAuthorization($endpoint, $method, $body),
+        ];
+
+        $options['headers'] = array_merge($headers, ($options['headers'] ?? []));
+
+        if ($serial) {
+            $options['headers']['Wechatpay-Serial'] = $this->getCertficatescAttr('serial_no');
+        }
+
+        return $this->_doRequestCurl($method, self::BASE_URL . $endpoint, $options);
+    }
+
+    /**
+     * @param $method
+     * @param $location
+     * @param array $options
+     * @return mixed
+     */
+    private function _doRequestCurl($method, $location, $options = [])
+    {
+        $curl = curl_init();
+        // POST数据设置
+        if (strtolower($method) === 'post') {
+            curl_setopt($curl, CURLOPT_POST, true);
+            curl_setopt($curl, CURLOPT_POSTFIELDS, $options['data'] ?? $options['body'] ?? '');
+        }
+        // CURL头信息设置
+        if (!empty($options['headers'])) {
+            $headers = [];
+            foreach ($options['headers'] as $k => $v) {
+                $headers[] = "$k: $v";
+            }
+            curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
+        }
+        curl_setopt($curl, CURLOPT_URL, $location);
+        curl_setopt($curl, CURLOPT_HEADER, true);
+        curl_setopt($curl, CURLOPT_TIMEOUT, 60);
+        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
+        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
+        $content = curl_exec($curl);
+        $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
+        curl_close($curl);
+        return json_decode(substr($content, $headerSize), true);
+    }
+
+    /**
+     * To id card, mobile phone number and other fields sensitive information encryption.
+     *
+     * @param string $string
+     *
+     * @return string
+     */
+    protected function encryptSensitiveInformation(string $string)
+    {
+        $certificates = $this->app->certficates->get()['certificates'];
+        if (null === $certificates) {
+            throw new InvalidConfigException('config certificate connot be empty.');
+        }
+        $encrypted = '';
+        if (openssl_public_encrypt($string, $encrypted, $certificates, OPENSSL_PKCS1_OAEP_PADDING)) {
+            //base64编码
+            $sign = base64_encode($encrypted);
+        } else {
+            throw new EncryptionException('Encryption of sensitive information failed');
+        }
+        return $sign;
+    }
+
+
+    /**
+     * @param string $url
+     * @param string $method
+     * @param string $body
+     * @return string
+     */
+    protected function getAuthorization(string $url, string $method, string $body)
+    {
+        $nonceStr = uniqid();
+        $timestamp = time();
+        $message = $method . "\n" .
+            '/' . $url . "\n" .
+            $timestamp . "\n" .
+            $nonceStr . "\n" .
+            $body . "\n";
+        openssl_sign($message, $raw_sign, $this->getPrivateKey(), 'sha256WithRSAEncryption');
+        $sign = base64_encode($raw_sign);
+        $schema = 'WECHATPAY2-SHA256-RSA2048 ';
+        $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
+            $this->app['config']['v3_payment']['mchid'], $nonceStr, $timestamp, $this->app['config']['v3_payment']['serial_no'], $sign);
+        return $schema . $token;
+    }
+
+    /**
+     * 获取商户私钥
+     * @return bool|resource
+     */
+    protected function getPrivateKey()
+    {
+        $key_path = $this->app['config']['v3_payment']['key_path'];
+        if (!file_exists($key_path)) {
+            throw new \InvalidArgumentException(
+                "SSL certificate not found: {$key_path}"
+            );
+        }
+        return openssl_pkey_get_private(file_get_contents($key_path));
+    }
+
+    /**
+     * 获取商户公钥
+     * @return bool|resource
+     */
+    protected function getPublicKey()
+    {
+        $key_path = $this->app['config']['v3_payment']['cert_path'];
+        if (!file_exists($key_path)) {
+            throw new \InvalidArgumentException(
+                "SSL certificate not found: {$key_path}"
+            );
+        }
+        return openssl_pkey_get_public(file_get_contents($key_path));
+    }
+
+    /**
+     * 替换url
+     * @param string $url
+     * @param $search
+     * @param $replace
+     * @return array|string|string[]
+     */
+    public function getApiUrl(string $url, $search, $replace)
+    {
+        $newSearch = [];
+        foreach ($search as $key) {
+            $newSearch[] = '{' . $key . '}';
+        }
+        return str_replace($newSearch, $replace, $url);
+    }
+
+    /**
+     * @param int $padding
+     */
+    private static function paddingModeLimitedCheck(int $padding): void
+    {
+        if (!($padding === OPENSSL_PKCS1_OAEP_PADDING || $padding === OPENSSL_PKCS1_PADDING)) {
+            throw new PayException(sprintf("Doesn't supported padding mode(%d), here only support OPENSSL_PKCS1_OAEP_PADDING or OPENSSL_PKCS1_PADDING.", $padding));
+        }
+    }
+
+    /**
+     * 加密数据
+     * @param string $plaintext
+     * @param int $padding
+     * @return string
+     */
+    public function encryptor(string $plaintext, int $padding = OPENSSL_PKCS1_OAEP_PADDING)
+    {
+        self::paddingModeLimitedCheck($padding);
+
+        if (!openssl_public_encrypt($plaintext, $encrypted, $this->getPublicKey(), $padding)) {
+            throw new PayException('Encrypting the input $plaintext failed, please checking your $publicKey whether or nor correct.');
+        }
+
+        return base64_encode($encrypted);
+    }
+
+    /**
+     * decrypt ciphertext.
+     *
+     * @param array $encryptCertificate
+     *
+     * @return string
+     */
+    public function decrypt(array $encryptCertificate)
+    {
+        $ciphertext = base64_decode($encryptCertificate['ciphertext'], true);
+        $associatedData = $encryptCertificate['associated_data'];
+        $nonceStr = $encryptCertificate['nonce'];
+        $aesKey = $this->app['config']['v3_payment']['key'];
+
+        try {
+            // ext-sodium (default installed on >= PHP 7.2)
+            if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) {
+                return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
+            }
+            // ext-libsodium (need install libsodium-php 1.x via pecl)
+            if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()) {
+                return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
+            }
+            // openssl (PHP >= 7.1 support AEAD)
+            if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
+                $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
+                $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);
+                return \openssl_decrypt($ctext, 'aes-256-gcm', $aesKey, \OPENSSL_RAW_DATA, $nonceStr, $authTag, $associatedData);
+            }
+        } catch (\Exception $exception) {
+            throw new InvalidArgumentException($exception->getMessage(), $exception->getCode());
+        } catch (\SodiumException $exception) {
+            throw new InvalidArgumentException($exception->getMessage(), $exception->getCode());
+        }
+        throw new InvalidArgumentException('AEAD_AES_256_GCM 需要 PHP 7.1 以上或者安装 libsodium-php');
+    }
+}
+

+ 36 - 0
crmeb/services/easywechat/v3pay/ServiceProvider.php

@@ -0,0 +1,36 @@
+<?php
+/**
+ *  +----------------------------------------------------------------------
+ *  | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+ *  +----------------------------------------------------------------------
+ *  | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+ *  +----------------------------------------------------------------------
+ *  | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+ *  +----------------------------------------------------------------------
+ *  | Author: CRMEB Team <admin@crmeb.com>
+ *  +----------------------------------------------------------------------
+ */
+
+namespace crmeb\services\easywechat\v3pay;
+
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+
+/**
+ * Class ServiceProvider
+ * @package crmeb\services\easywechat\v3pay
+ */
+class ServiceProvider implements ServiceProviderInterface
+{
+
+    /**
+     * @param Container $pimple
+     */
+    public function register(Container $pimple)
+    {
+        $pimple['v3pay'] = function ($pimple) {
+            return new PayClient($pimple['access_token'], $pimple);
+        };
+    }
+}

+ 62 - 0
crmeb/services/pay/BasePay.php

@@ -0,0 +1,62 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+namespace crmeb\services\pay;
+
+use EasyWeChat\Payment\Order;
+use crmeb\basic\BaseStorage;
+
+/**
+ * Class BasePay
+ * @package crmeb\services\pay
+ */
+abstract class BasePay extends BaseStorage
+{
+    /**
+     * @var string
+     */
+    protected $payType;
+
+    /**
+     * 设置支付类型
+     * @param string $type
+     * @return $this
+     */
+    public function setPayType(string $type)
+    {
+        $this->payType = $type;
+        return $this;
+    }
+
+    /**
+     * 设置支付类型
+     * @param string $type
+     * @return $this
+     */
+    public function authSetPayType()
+    {
+        if (!$this->payType) {
+            if (request()->isPc()) {
+                $this->payType = Order::NATIVE;
+            }
+            if (request()->isApp()) {
+                $this->payType = Order::APP;
+            }
+            if (request()->isRoutine() || request()->isWechat()) {
+                $this->payType = Order::JSAPI;
+            }
+            if (request()->isH5()) {
+                $this->payType = 'h5';
+            }
+        }
+    }
+
+
+}

+ 344 - 0
crmeb/services/pay/extend/allinpay/AllinPay.php

@@ -0,0 +1,344 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+namespace crmeb\services\pay\extend\allinpay;
+
+
+/**
+ *
+ * Class AllinPay
+ * @author 等风来
+ * @email 136327134@qq.com
+ * @date 2022/12/27
+ * @package crmeb\services\pay\extend\allinpay
+ */
+class AllinPay extends Client
+{
+
+    //统一支付接口
+    const UNITODER_PAY_API = 'unitorder/pay';
+
+    //统一扫码接口
+    const UNITORDER_SCANQRPAY = 'unitorder/scanqrpay';
+
+    //退款
+    const UNITODER_TRANX_REFUND = 'tranx/refund';
+
+    //订单查询
+    const  UNITODER_TRANX_QUERY = 'tranx/query';
+
+    const  UNITODER_QPAY_AGREEAPPLY = 'qpay/agreeapply';
+
+    //微信公众号内支付请求地址
+    const UNITODER_H5UNIONPAY = 'https://vsp.allinpay.com/apiweb/h5unionpay/unionorder';
+
+    /**
+     * @var string
+     */
+    protected $api = '';
+
+    /**
+     * 支付类型
+     * @var string
+     */
+    protected $payType = '';
+
+    /**
+     * @var string
+     */
+    protected $version = '';
+
+    /**
+     * @var string
+     */
+    protected $orderKey = 'reqsn';
+
+    /**
+     * @var int
+     */
+    protected $validtime = 720;
+
+    /**
+     * 创建支付订单
+     * @param string $trxamt
+     * @param string $orderId
+     * @param string $body
+     * @param string|null $openId
+     * @param string|null $appid
+     * @param string $frontUrl
+     * @param string $remark
+     * @return mixed
+     */
+    public function create(string $trxamt, string $orderId, string $body, string $returl = null, string $openId = null, string $appid = null, string $frontUrl = '', string $remark = '')
+    {
+        $totalFee = (int)bcmul($trxamt, '100');
+
+        $data = [
+            'trxamt' => $totalFee,
+            $this->orderKey => $orderId,
+            'validtime' => $this->validtime,
+            'notify_url' => $this->notifyUrl
+        ];
+
+        if ($body) {
+            $data['body'] = $body;
+        }
+
+        if ($remark) {
+            $data['remark'] = $remark;
+        }
+
+        if ($this->payType) {
+            $data['paytype'] = $this->payType;
+        }
+
+        if ($returl) {
+            $data['returl'] = $returl;
+        }
+
+        if ($openId) {
+            $data['acct'] = $openId;
+        }
+
+        if ($appid) {
+            $data['sub_appid'] = $appid;
+        }
+
+        if ($frontUrl) {
+            $data['front_url'] = $frontUrl;
+        }
+
+        if ($this->version) {
+            $data['version'] = $this->version;
+        }
+
+        $form = !$this->api;
+        $api = $this->api;
+
+        $this->version = '';
+        $this->payType = '';
+        $this->api = '';
+        $this->orderKey = 'reqsn';
+        $this->validtime = 720;
+
+        return $this->send($api, ['data' => $data, 'form' => $form]);
+    }
+
+    /**
+     * 微信H5支付
+     * @param string $trxamt
+     * @param string $orderId
+     * @param string $body
+     * @param string $returl
+     * @param string $remark
+     * @return mixed
+     */
+    public function h5Pay(string $trxamt, string $orderId, string $body, string $returl, string $remark = '')
+    {
+        $this->version = self::VERSION_NUM_12;
+        return $this->create($trxamt, $orderId, $body, $returl, null, null, '', $remark);
+    }
+
+    /**
+     * 微信js支付
+     * @param string $trxamt
+     * @param string $orderId
+     * @param string $body
+     * @param string $openId
+     * @param string $appId
+     * @param string $frontUrl
+     * @param string $remark
+     * @return mixed
+     */
+    public function wechatPay(string $trxamt, string $orderId, string $body, string $openId, string $appId, string $frontUrl, string $remark = '')
+    {
+        $this->api = self::UNITODER_PAY_API;
+        return $this->create($trxamt, $orderId, $body, null, $openId, $appId, $frontUrl, $remark);
+    }
+
+    /**
+     * app支付 微信和支付宝
+     * @param string $trxamt
+     * @param string $orderId
+     * @param string $body
+     * @param bool $isWechat
+     * @param string $remark
+     * @return mixed
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/1/14
+     */
+    public function appPay(string $trxamt, string $orderId, string $body, string $appid, string $openid = null, bool $isWechat = true, string $remark = '')
+    {
+        $this->api = self::UNITODER_PAY_API;
+        $this->payType = $isWechat ? 'A02' : 'A01';
+        $this->version = self::VERSION_NUM_11;
+        return $this->create($trxamt, $orderId, $body, null, $openid, $appid, '', $remark);
+    }
+
+    /**
+     * PC 收银台支付
+     * @param string $trxamt
+     * @param string $orderId
+     * @param string $body
+     * @return mixed
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/1/15
+     */
+    public function pcPay(string $trxamt, string $orderId, string $body, string $remark, bool $isWechat = true)
+    {
+        $this->api = self::UNITODER_PAY_API;
+        $this->payType = $isWechat ? 'W01' : 'A01';
+        $this->version = self::VERSION_NUM_11;
+        $res = $this->create($trxamt, $orderId, $body, null, null, null, '', $remark);
+        $invalid = time() + 60;
+        if ($isWechat) {
+            $key = 'code_url';
+        } else {
+            $key = 'qrCode';
+        }
+        return ['invalid' => $invalid, 'logo' => sys_config('wap_login_logo'), $key => $res['payinfo']];
+    }
+
+    /**
+     * @param string $trxamt
+     * @param string $orderId
+     * @param string $body
+     * @param string $returl
+     * @param string $remark
+     * @return array
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/1/15
+     */
+    public function miniproPay(string $trxamt, string $orderId, string $body, string $remark = '')
+    {
+        $totalFee = bcmul($trxamt, '100');
+
+        $data = [
+            'paytype' => 'W06',
+            'trxamt' => $totalFee,
+            'reqsn' => $orderId,
+            'notify_url' => $this->notifyUrl,
+            'body' => $body,
+            'validtime' => (string)$this->validtime,
+            'cusid' => $this->cusid,
+            'appid' => $this->appid,
+            'signtype' => $this->signType,
+            'randomstr' => uniqid(),
+            'remark' => $remark,
+            'version' => self::VERSION_NUM_12
+        ];
+
+        $data['sign'] = $this->sign($data);
+
+        return $data;
+    }
+
+    public function agreeapply()
+    {
+        $data = [
+            'meruserid' => 'eesssxxx',
+            'accttype' => '00',
+            'acctno' => '',
+            'idtype' => 0,
+            'idno' => '',
+            'acctname' => '',
+            'mobile' => '',
+            'cvv2' => '',
+            'reqip' => request()->ip(),
+            'reqtime' => date('Y-m-d H:i:s'),
+            'version' => self::VERSION_NUM_11,
+        ];
+
+        $data['cusid'] = $this->cusid;
+        $data['appid'] = $this->appid;
+        $data['signtype'] = $this->signType;
+        $data['randomstr'] = uniqid();
+        $data['sign'] = $this->sign($data);
+
+        return $this->request(self::UNITODER_QPAY_AGREEAPPLY, $data);
+    }
+
+
+    /**
+     * 查询订单
+     * @param string $reqsn
+     * @return array|mixed
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/1/15
+     */
+    public function query(string $reqsn)
+    {
+        $data = [
+            'reqsn' => $reqsn
+        ];
+
+        return $this->send(self::UNITODER_TRANX_QUERY, ['data' => $data]);
+    }
+
+    /**
+     * 发起退款
+     * @param string $trxamt
+     * @param string $reqsn
+     * @return array|mixed
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/1/15
+     */
+    public function refund(string $trxamt, string $orderId, string $reqsn)
+    {
+        $totalFee = (int)bcmul($trxamt, '100');
+
+        $data = [
+            'trxamt' => $totalFee,
+            'reqsn' => $orderId,
+            'oldtrxid' => $reqsn,
+        ];
+
+        return $this->send(self::UNITODER_TRANX_REFUND, ['data' => $data]);
+    }
+
+    /**
+     * 异步回调
+     * @param callable $callback
+     * @return string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/1/15
+     */
+    public function handleNotify(callable $callback)
+    {
+        $params = [];
+        foreach (request()->post() as $key => $val) {
+            $params[$key] = $val;
+        }
+
+        $this->debugLog('通联支付回调数据' . json_encode($params));
+
+        if (count($params) < 1) {
+            //如果参数为空,则不进行处理
+            return "error";
+        }
+
+        $res = $this->validSign($params);
+
+        $this->debugLog('通联支付回调验证:' . json_encode(['res' => $res]));
+
+        if ($res && isset($params['trxstatus']) && $params['trxstatus'] === '0000') {
+            //验签成功
+            return $callback($params) ? 'success' : 'error';
+        } else {
+            return "error";
+        }
+    }
+}

+ 102 - 0
crmeb/services/pay/storage/AliPay.php

@@ -0,0 +1,102 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace crmeb\services\pay\storage;
+
+
+use Alipay\EasySDK\Payment\Common\Models\AlipayTradeFastpayRefundQueryResponse;
+use Alipay\EasySDK\Payment\Common\Models\AlipayTradeRefundResponse;
+use Alipay\EasySDK\Payment\Wap\Models\AlipayTradeWapPayResponse;
+use crmeb\services\pay\BasePay;
+use crmeb\services\pay\PayInterface;
+use crmeb\services\AliPayService;
+
+/**
+ * 支付宝支付
+ * Class AliPay
+ * @package crmeb\services\pay\storage
+ */
+class AliPay extends BasePay implements PayInterface
+{
+
+    protected function initialize(array $config)
+    {
+        // TODO: Implement initialize() method.
+    }
+
+    /**
+     * 创建订单发起支付
+     * @param string $orderId
+     * @param string $totalFee
+     * @param string $attach
+     * @param string $body
+     * @param string $detail
+     * @param string|null $tradeType
+     * @param array $options
+     * @return AlipayTradeWapPayResponse|mixed
+     */
+    public function create(string $orderId, string $totalFee, string $attach, string $body, string $detail, array $options = [])
+    {
+        $code = false;
+        if (request()->isPC() || request()->isRoutine()) {
+            $code = true;
+        }
+
+        return AliPayService::instance()->create($body, $orderId, $totalFee, $attach, $options['quitUrl'] ?? '', $options['returnUrl'] ?? '', $code);
+    }
+
+    /**
+     * 企业支付到零钱
+     * @param string $openid
+     * @param string $orderId
+     * @param string $amount
+     * @param array $options
+     * @return bool|mixed
+     */
+    public function merchantPay(string $openid, string $orderId, string $amount, array $options = [])
+    {
+        return false;
+    }
+
+    /**
+     * 退款
+     * @param string $outTradeNo
+     * @param string $totalAmount
+     * @param string $refund_id
+     * @param array $options
+     * @return AlipayTradeRefundResponse|mixed
+     */
+    public function refund(string $outTradeNo, array $options = [])
+    {
+        return AliPayService::instance()->refund($outTradeNo, $options['totalAmount'], $options['refund_id']);
+    }
+
+    /**
+     * 查询退款
+     * @param string $outTradeNo
+     * @param string $outRequestNo
+     * @param array $other
+     * @return AlipayTradeFastpayRefundQueryResponse|mixed
+     */
+    public function queryRefund(string $outTradeNo, string $outRequestNo, array $other = [])
+    {
+        return AliPayService::instance()->queryRefund($outTradeNo, $outRequestNo);
+    }
+
+    /**
+     * 支付异步回调
+     * @return mixed|string
+     */
+    public function handleNotify()
+    {
+        return AliPayService::handleNotify();
+    }
+}

+ 154 - 0
crmeb/services/pay/storage/AllinPay.php

@@ -0,0 +1,154 @@
+<?php
+/**
+ *  +----------------------------------------------------------------------
+ *  | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+ *  +----------------------------------------------------------------------
+ *  | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+ *  +----------------------------------------------------------------------
+ *  | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+ *  +----------------------------------------------------------------------
+ *  | Author: CRMEB Team <admin@crmeb.com>
+ *  +----------------------------------------------------------------------
+ */
+
+namespace crmeb\services\pay\storage;
+
+use app\services\pay\PayServices;
+use crmeb\exceptions\AdminException;
+use crmeb\exceptions\PayException;
+use crmeb\services\pay\BasePay;
+use crmeb\services\pay\PayInterface;
+use crmeb\services\pay\extend\allinpay\AllinPay as AllinPayService;
+use EasyWeChat\Payment\Order;
+use think\facade\Event;
+
+/**
+ * 通联支付
+ * Class AllinPay
+ * @author 等风来
+ * @email 136327134@qq.com
+ * @date 2023/2/1
+ * @package crmeb\services\pay\storage
+ */
+class AllinPay extends BasePay implements PayInterface
+{
+
+    /**
+     * @var AllinPayService
+     */
+    protected $pay;
+
+    /**
+     * @param array $config
+     * @return mixed|void
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/1/15
+     */
+    protected function initialize(array $config)
+    {
+        $this->pay = new AllinPayService([
+            'appid' => sys_config('allin_appid'),
+            'cusid' => sys_config('allin_cusid'),
+            'privateKey' => sys_config('allin_private_key'),
+            'publicKey' => sys_config('allin_public_key'),
+            'notifyUrl' => trim(sys_config('site_url')) . '/api/pay/notify/allin',
+            'isBeta' => false,
+        ]);
+    }
+
+    /**
+     * 创建支付
+     * @param string $orderId
+     * @param string $totalFee
+     * @param string $attach
+     * @param string $body
+     * @param string $detail
+     * @param array $options
+     * @return array|mixed
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/1/15
+     */
+    public function create(string $orderId, string $totalFee, string $attach, string $body, string $detail, array $options = [])
+    {
+        $this->authSetPayType();
+
+        $options['returl'] = sys_config('site_url') . '/pages/index/index';
+        if ($options['returl']) {
+            $options['returl'] = str_replace('http://', 'https://', $options['returl']);
+        }
+        $options['appid'] = sys_config('routine_appId');
+
+
+        $notifyUrl = trim(sys_config('site_url')) . '/api/pay/notify/allin' . $attach;
+        $this->pay->setNotifyUrl($notifyUrl);
+
+        switch ($this->payType) {
+            case Order::APP:
+                return $this->pay->appPay($totalFee, $orderId, $body, '', '', false, $attach);
+            case Order::JSAPI:
+                if (request()->isRoutine()) {
+                    return $this->pay->miniproPay($totalFee, $orderId, $body, $attach);
+                } else {
+                    return $this->pay->h5Pay($totalFee, $orderId, $body, $options['returl'] ?? '', $attach);
+                }
+            case Order::NATIVE:
+                return $this->pay->pcPay($totalFee, $orderId, $body, $attach, !empty($options['wechat']));
+            default:
+                throw new PayException('通联支付:支付类型错误或者暂不支持此环境下支付');
+        }
+    }
+
+    public function merchantPay(string $openid, string $orderId, string $amount, array $options = [])
+    {
+        throw new PayException('通联支付:暂不支持商家转账');
+    }
+
+    /**
+     * 发起退款
+     * @param string $outTradeNo
+     * @param array $options
+     * @return array|mixed
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/1/15
+     */
+    public function refund(string $outTradeNo, array $options = [])
+    {
+        $result = $this->pay->refund($options['refund_price'], $options['order_id'], $outTradeNo);
+        if ($result['retcode'] != 'SUCCESS') throw new AdminException($result['retmsg']);
+    }
+
+    public function queryRefund(string $outTradeNo, string $outRequestNo, array $other = [])
+    {
+        // TODO: Implement queryRefund() method.
+    }
+
+    /**
+     * 异步回调
+     * @return mixed|string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/1/15
+     */
+    public function handleNotify(string $attach = '')
+    {
+        $attach = str_replace('allin', '', $attach);
+
+        return $this->pay->handleNotify(function ($notify) use ($attach) {
+
+            if (isset($notify['cusorderid'])) {
+
+                $data = [
+                    'attach' => $attach,
+                    'out_trade_no' => $notify['cusorderid'],
+                    'transaction_id' => $notify['trxid']
+                ];
+
+                return Event::until('NotifyListener', [$data, PayServices::ALLIN_PAY]);
+            }
+            return false;
+        });
+    }
+}

+ 65 - 0
crmeb/services/sms/Sms.php

@@ -0,0 +1,65 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace crmeb\services\sms;
+
+use crmeb\basic\BaseManager;
+use crmeb\services\AccessTokenServeService;
+use crmeb\services\sms\storage\yihaotong;
+use think\Container;
+use think\facade\Config;
+
+
+/**
+ * Class Sms1
+ * @package crmeb\services\sms
+ * @mixin yihaotong
+ */
+class Sms extends BaseManager
+{
+
+    /**
+     * 空间名
+     * @var string
+     */
+    protected $namespace = '\\crmeb\\services\\sms\\storage\\';
+
+    /**
+     * 默认驱动
+     * @return mixed
+     */
+    protected function getDefaultDriver()
+    {
+        return Config::get('sms.default');
+    }
+
+    /**
+     * 获取类的实例
+     * @param $class
+     * @return mixed|void
+     */
+    protected function invokeClass($class)
+    {
+        if (!class_exists($class)) {
+            throw new \RuntimeException('class not exists: ' . $class);
+        }
+        $this->getConfigFile();
+
+        if (!$this->config) {
+            $this->config = Config::get($this->configFile . '.stores.' . $this->name, []);
+        }
+
+        $handleAccessToken = new AccessTokenServeService($this->config['sms_account'] ?? '', $this->config['sms_token'] ?? '');
+        $handle = Container::getInstance()->invokeClass($class, [$this->name, $handleAccessToken, $this->configFile, $this->config]);
+        $this->config = [];
+        return $handle;
+    }
+}

+ 133 - 0
crmeb/services/template/storage/Wechat.php

@@ -0,0 +1,133 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+namespace crmeb\services\template\storage;
+
+use app\services\message\SystemNotificationServices;
+use crmeb\services\template\BaseMessage;
+use crmeb\services\app\WechatService;
+use think\facade\Log;
+
+class Wechat extends BaseMessage
+{
+    protected $error;
+
+    /**
+     * 初始化
+     * @param array $config
+     * @return mixed|void
+     */
+    protected function initialize(array $config)
+    {
+        parent::initialize($config); // TODO: Change the autogenerated stub
+    }
+
+    /**
+     * @param string $templateId
+     * @return mixed
+     */
+    public function getTempId(string $templateId)
+    {
+        return app()->make(SystemNotificationServices::class)->value(['wechat_tempkey' => $templateId], 'wechat_tempid');
+    }
+
+    /**
+     * 发送消息
+     * @param string $tempid
+     * @param array $data
+     * @return bool|mixed
+     */
+    public function send(string $tempid, array $data = [], $wechatToRoutine = 0)
+    {
+        if (!$tempid) {
+            return $this->setError('Template ID does not exist');
+        }
+        if (!$this->openId) {
+            return $this->setError('Openid does not exist');
+        }
+        try {
+            $res = WechatService::sendTemplate($this->openId, $tempid, $data, $this->toUrl, $this->color, $wechatToRoutine);
+            $this->clear();
+            return $res;
+        } catch (\Exception $e) {
+            Log::error('发送给openid为:' . $this->openId . '微信模板消息失败,模板id为:' . $tempid . ';错误原因为:' . $e->getMessage());
+            return $this->setError($e->getMessage());
+        }
+    }
+
+    /**
+     * 获取所有模板
+     * @return \EasyWeChat\Support\Collection|mixed
+     */
+    public function list()
+    {
+        return WechatService::noticeService()->getPrivateTemplates();
+    }
+
+    /**
+     * 添加模板消息
+     * @param string $shortId
+     * @return \EasyWeChat\Support\Collection|mixed
+     */
+    public function add(string $shortId)
+    {
+        return WechatService::noticeService()->addTemplate($shortId);
+    }
+
+    /**
+     * 删除模板消息
+     * @param string $templateId
+     * @return \EasyWeChat\Support\Collection|mixed
+     */
+    public function delete(string $templateId)
+    {
+        return WechatService::noticeService()->deletePrivateTemplate($templateId);
+    }
+
+    /**
+     * 返回所有支持的行业列表
+     * @return \EasyWeChat\Support\Collection
+     */
+    public function getIndustry()
+    {
+        return WechatService::noticeService()->getIndustry();
+    }
+
+    /**
+     * 设置模版消息行业
+     * @return \EasyWeChat\Support\Collection
+     */
+    public function setIndustry($one, $two)
+    {
+        return WechatService::noticeService()->setIndustry($one, $two);
+    }
+
+    /**
+     * 设置错误信息
+     * @param string|null $error
+     * @return bool
+     */
+    protected function setError(?string $error = null)
+    {
+        $this->error = $error ?: '未知错误';
+        return false;
+    }
+
+    /**
+     * 获取错误信息
+     * @return string
+     */
+    public function getError()
+    {
+        $error = $this->error;
+        $this->error = null;
+        return $error;
+    }
+}

+ 77 - 0
crmeb/services/upload/extend/cos/Scope.php

@@ -0,0 +1,77 @@
+<?php
+/**
+ *  +----------------------------------------------------------------------
+ *  | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+ *  +----------------------------------------------------------------------
+ *  | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+ *  +----------------------------------------------------------------------
+ *  | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+ *  +----------------------------------------------------------------------
+ *  | Author: CRMEB Team <admin@crmeb.com>
+ *  +----------------------------------------------------------------------
+ */
+
+namespace crmeb\services\upload\extend\cos;
+
+
+class Scope
+{
+    protected $action;
+    protected $bucket;
+    protected $region;
+    protected $resourcePrefix;
+    protected $effect = 'allow';
+
+    public function __construct($action, $bucket, $region, $resourcePrefix)
+    {
+        $this->action = $action;
+        $this->bucket = $bucket;
+        $this->region = $region;
+        $this->resourcePrefix = $resourcePrefix;
+    }
+
+    public function set_effect($isAllow)
+    {
+        if ($isAllow) {
+            $this->effect = 'allow';
+        } else {
+            $this->effect = 'deny';
+        }
+    }
+
+    public function get_action()
+    {
+        if ($this->action == null) {
+            throw new \Exception("action == null");
+        }
+        return $this->action;
+    }
+
+    public function get_resource()
+    {
+        if ($this->bucket == null) {
+            throw new \Exception("bucket == null");
+        }
+        if ($this->region == null) {
+            throw new \Exception("region == null");
+        }
+        if ($this->resourcePrefix == null) {
+            throw new \Exception("resourcePrefix == null");
+        }
+        $index = strripos($this->bucket, '-');
+        if ($index < 0) {
+            throw new Exception("bucket is invalid: " . $this->bucket);
+        }
+        $appid = substr($this->bucket, $index + 1);
+        if (!(strpos($this->resourcePrefix, '/') === 0)) {
+            $this->resourcePrefix = '/' . $this->resourcePrefix;
+        }
+        return 'qcs::cos:' . $this->region . ':uid/' . $appid . ':' . $this->bucket . $this->resourcePrefix;
+    }
+
+    public function get_effect()
+    {
+        return $this->effect;
+    }
+
+}

+ 213 - 0
crmeb/services/upload/extend/cos/Signature.php

@@ -0,0 +1,213 @@
+<?php
+/**
+ *  +----------------------------------------------------------------------
+ *  | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+ *  +----------------------------------------------------------------------
+ *  | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+ *  +----------------------------------------------------------------------
+ *  | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+ *  +----------------------------------------------------------------------
+ *  | Author: CRMEB Team <admin@crmeb.com>
+ *  +----------------------------------------------------------------------
+ */
+
+namespace crmeb\services\upload\extend\cos;
+
+/**
+ * Class 生成签名
+ * @author 等风来
+ * @email 136327134@qq.com
+ * @date 2022/9/26
+ * @package crmeb\services\upload\extend\cos
+ */
+class Signature
+{
+    /**
+     * @var string
+     */
+    private $accessKey;
+
+    /**
+     * @var string
+     */
+    private $secretKey;
+
+    /**
+     * @var array
+     */
+    private $options;
+
+    /**
+     * Signature constructor.
+     * @param string $accessKey
+     * @param string $secretKey
+     * @param array $options
+     * @param string $token
+     */
+    public function __construct(string $accessKey, string $secretKey, array $options = [], string $token = '')
+    {
+        $this->accessKey = $accessKey;
+        $this->secretKey = $secretKey;
+        $this->options = $options;
+        $this->token = $token;
+        $this->signHeader = [
+            'cache-control',
+            'content-disposition',
+            'content-encoding',
+            'content-length',
+            'content-md5',
+            'content-type',
+            'expect',
+            'expires',
+            'host',
+            'if-match',
+            'if-modified-since',
+            'if-none-match',
+            'if-unmodified-since',
+            'origin',
+            'range',
+            'response-cache-control',
+            'response-content-disposition',
+            'response-content-encoding',
+            'response-content-language',
+            'response-content-type',
+            'response-expires',
+            'transfer-encoding',
+            'versionid',
+        ];
+        date_default_timezone_set('PRC');
+    }
+
+    public function needCheckHeader($header)
+    {
+        if ($this->startWith($header, 'x-cos-')) {
+            return true;
+        }
+        if (in_array($header, $this->signHeader)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/9/29
+     * @param $haystack
+     * @param $needle
+     * @return bool
+     */
+    protected function startWith($haystack, $needle)
+    {
+        $length = strlen($needle);
+        if ($length == 0) {
+            return true;
+        }
+        return (substr($haystack, 0, $length) === $needle);
+    }
+
+    /**
+     * @param string $method
+     * @param string $urlPath
+     * @param array $querys
+     * @param array $headers
+     * @return string[]
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/9/26
+     */
+    public function signRequest(string $method, string $urlPath, array $querys = [], array $headers = [])
+    {
+        $authorization = $this->createAuthorization($method, $urlPath, $querys, $headers);
+        return ['Authorization' => $authorization];
+    }
+
+    /**
+     * @param string $method
+     * @param string $urlPath
+     * @param array $querys
+     * @param array $headers
+     * @param string $expires
+     * @return string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/9/26
+     */
+    public function createAuthorization(string $method, string $urlPath, array $querys = [], array $headers = [], $expires = '+30 minutes')
+    {
+        if (is_null($expires) || !strtotime($expires)) {
+            $expires = '+30 minutes';
+        }
+        $signTime = ( string )(time() - 60) . ';' . ( string )(strtotime($expires));
+        $urlParamListArray = [];
+        foreach ($querys as $query) {
+            if (!empty($query)) {
+                $tmpquery = explode('=', $query);
+                //为了保证CI的key中有=号的情况也能正常通过,ci在这层之前已经encode了,这里需要拆开重新encode,防止上方explode拆错
+                $key = strtolower(rawurlencode(urldecode($tmpquery[0])));
+                if (count($tmpquery) >= 2) {
+                    $value = $tmpquery[1];
+                } else {
+                    $value = "";
+                }
+                //host开关
+                if (!$this->options['signHost'] && $key == 'host') {
+                    continue;
+                }
+                $urlParamListArray[$key] = $key . '=' . $value;
+            }
+        }
+        ksort($urlParamListArray);
+        $urlParamList = join(';', array_keys($urlParamListArray));
+        $httpParameters = join('&', array_values($urlParamListArray));
+
+        $headerListArray = [];
+        foreach ($headers as $key => $value) {
+            $key = strtolower(urlencode($key));
+            $value = rawurlencode($value);
+            if (!$this->options['signHost'] && $key == 'host') {
+                continue;
+            }
+            if ($this->needCheckHeader($key)) {
+                $headerListArray[$key] = $key . '=' . $value;
+            }
+        }
+        ksort($headerListArray);
+        $headerList = join(';', array_keys($headerListArray));
+        $httpHeaders = join('&', array_values($headerListArray));
+        $httpString = strtolower($method) . "\n" . urldecode($urlPath) . "\n" . $httpParameters .
+            "\n" . $httpHeaders . "\n";
+
+        $sha1edHttpString = sha1($httpString);
+        $stringToSign = "sha1\n$signTime\n$sha1edHttpString\n";
+        $signKey = hash_hmac('sha1', $signTime, trim($this->secretKey));
+        $signature = hash_hmac('sha1', $stringToSign, $signKey);
+        $authorization = 'q-sign-algorithm=sha1&q-ak=' . trim($this->accessKey) .
+            "&q-sign-time=$signTime&q-key-time=$signTime&q-header-list=$headerList&q-url-param-list=$urlParamList&" .
+            "q-signature=$signature";
+        return $authorization;
+    }
+
+    /**
+     * @param string $url
+     * @param string $method
+     * @param string $urlPath
+     * @param array $querys
+     * @param array $headers
+     * @param string $expires
+     * @return string[]
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/9/26
+     */
+    public function createPresignedUrl(string $url, string $method, string $urlPath, array $querys = [], array $headers = [], string $expires = '+30 minutes')
+    {
+        $authorization = $this->createAuthorization($method, $urlPath, $querys, $headers, $expires);
+        $uri = $url;
+        $query = 'sign=' . urlencode($authorization) . '&' . implode('&', $querys);
+        if ($this->token != null) {
+            $query = $query . '&x-cos-security-token=' . $this->token;
+        }
+        return [$uri, $query];
+    }
+}

+ 438 - 0
crmeb/services/upload/storage/Jdoss.php

@@ -0,0 +1,438 @@
+<?php
+
+namespace crmeb\services\upload\storage;
+
+use Aws\Acm\Exception\AcmException;
+use Aws\S3\S3Client;
+use crmeb\exceptions\AdminException;
+use crmeb\exceptions\UploadException;
+use crmeb\services\upload\BaseUpload;
+use Guzzle\Http\EntityBody;
+
+/**
+ * 京东云COS文件上传
+ * Class Jdoss
+ * @package crmeb\services\upload\storage
+ */
+class Jdoss extends BaseUpload
+{
+
+
+    /**
+     * 应用id
+     * @var string
+     */
+    protected $appid;
+
+    /**
+     * accessKey
+     * @var mixed
+     */
+    protected $accessKey;
+
+    /**
+     * secretKey
+     * @var mixed
+     */
+    protected $secretKey;
+
+    /**
+     * 句柄
+     * @var S3Client
+     */
+    protected $handle;
+
+    /**
+     * 空间域名 Domain
+     * @var mixed
+     */
+    protected $uploadUrl;
+
+    /**
+     * 存储空间名称  公开空间
+     * @var mixed
+     */
+    protected $storageName;
+
+    /**
+     * COS使用  所属地域
+     * @var mixed|null
+     */
+    protected $storageRegion;
+
+    /**
+     * @var string
+     */
+    protected $cdn;
+
+    /**
+     * 水印位置
+     * @var string[]
+     */
+    protected $position = [
+        '1' => 'northwest',//:左上
+        '2' => 'north',//:中上
+        '3' => 'northeast',//:右上
+        '4' => 'west',//:左中
+        '5' => 'center',//:中部
+        '6' => 'east',//:右中
+        '7' => 'southwest',//:左下
+        '8' => 'south',//:中下
+        '9' => 'southeast',//:右下
+    ];
+
+    /**
+     * 初始化
+     * @param array $config
+     * @return mixed|void
+     */
+    public function initialize(array $config)
+    {
+        parent::initialize($config);
+        $this->accessKey = $config['accessKey'] ?? null;
+        $this->secretKey = $config['secretKey'] ?? null;
+        $this->uploadUrl = $this->checkUploadUrl($config['uploadUrl'] ?? '');
+        $this->storageName = $config['storageName'] ?? null;
+        $this->storageRegion = $config['storageRegion'] ?? null;
+        $this->cdn = $config['cdn'] ?? null;
+        $this->waterConfig['watermark_text_font'] = 'simfang仿宋.ttf';
+    }
+
+    /**
+     * @return S3Client
+     *
+     * @date 2023/06/05
+     * @author yyw
+     */
+    protected function app()
+    {
+        if (!$this->accessKey || !$this->secretKey) {
+            throw new UploadException(400721);
+        }
+        $this->handle = new S3Client([
+            'version' => 'latest',
+            'region' => $this->storageRegion,
+            'endpoint' => "http://s3.{$this->storageRegion}.jdcloud-oss.com",
+            'signature_version' => 'v4',
+            'use_path_style_endpoint' => true,
+            'credentials' => [
+                'key' => $this->accessKey,
+                'secret' => $this->secretKey,
+            ],
+        ]);
+        return $this->handle;
+    }
+
+    public function move(string $file = 'file')
+    {
+        $fileHandle = app()->request->file($file);
+        if (!$fileHandle) {
+            return $this->setError('上传的文件不存在');
+        }
+        if ($this->validate) {
+            if (!in_array(strtolower(pathinfo($fileHandle->getOriginalName(), PATHINFO_EXTENSION)), $this->validate['fileExt'])) {
+                return $this->setError('不合法的文件后缀');
+            }
+            if (filesize($fileHandle) > $this->validate['filesize']) {
+                return $this->setError('文件过大');
+            }
+            if (!in_array($fileHandle->getOriginalMime(), $this->validate['fileMime'])) {
+                return $this->setError('不合法的文件类型');
+            }
+        }
+        $key = $this->saveFileName($fileHandle->getRealPath(), $fileHandle->getOriginalExtension());
+        $key = $this->getUploadPath($key);
+        try {
+            $uploadInfo = $this->app()->putObject([
+                'Bucket' => $this->storageName,
+                'Key' => $key,
+                'SourceFile' => $fileHandle->getRealPath()
+            ]);
+            if (!isset($uploadInfo['ObjectURL'])) {
+                return $this->setError('Upload failure');
+            }
+            $this->fileInfo->uploadInfo = $uploadInfo;
+            $this->fileInfo->realName = $fileHandle->getOriginalName();
+            $this->fileInfo->filePath = ($this->cdn ?: $this->uploadUrl) . '/' . $key;
+            $this->fileInfo->fileName = $key;
+            $this->fileInfo->filePathWater = $this->water($this->fileInfo->filePath);
+            $this->authThumb && $this->thumb($this->fileInfo->filePath);
+            return $this->fileInfo;
+        } catch (\Throwable $e) {
+            return $this->setError($e->getMessage());
+        }
+    }
+
+    public function stream($fileContent, string $key = null)
+    {
+        try {
+            if (!$key) {
+                $key = $this->saveFileName();
+            }
+            $key = $this->getUploadPath($key);
+            $fileContent = (string)EntityBody::factory($fileContent);
+            $uploadInfo = $this->app()->putObject([
+                'Bucket' => $this->storageName,
+                'Key' => $key,
+                'Body' => $fileContent
+            ]);
+            $uploadInfo = $uploadInfo->toArray();
+            if (isset($uploadInfo['@metadata']['statusCode']) && $uploadInfo['@metadata']['statusCode'] !== 200) {
+                return $this->setError('Upload failure');
+            }
+            $this->fileInfo->uploadInfo = $uploadInfo;
+            $this->fileInfo->realName = $key;
+            $this->fileInfo->filePath = ($this->cdn ?: $this->uploadUrl) . '/' . $key;
+            $this->fileInfo->fileName = $key;
+            $this->fileInfo->filePathWater = $this->water($this->fileInfo->filePath);
+            $this->thumb($this->fileInfo->filePath);
+            return $this->fileInfo;
+        } catch (\Throwable $e) {
+            return $this->setError($e->getMessage());
+        }
+    }
+
+    public function delete(string $key)
+    {
+        try {
+            return $this->app()->deleteObject([
+                'Bucket' => $this->storageName,
+                'Key' => $key
+            ]);
+        } catch (\Throwable $e) {
+            return $this->setError($e->getMessage());
+        }
+    }
+
+
+    public function listbuckets(string $region, bool $line = false, bool $shared = false)
+    {
+        try {
+            $res = $this->app()->listBuckets();
+            return $res ?? [];
+        } catch (\Throwable $e) {
+            return [];
+        }
+    }
+
+    public function createBucket(string $name, string $region = '', string $acl = 'public-read')
+    {
+        $regionData = $this->getRegion();
+        $regionData = array_column($regionData, 'value');
+        if (!in_array($region, $regionData)) {
+            return $this->setError('COS:无效的区域!');
+        }
+        $this->storageRegion = $region;
+        $app = $this->app();
+        //检测桶
+        try {
+            $app->headBucket([
+                'Bucket' => $name
+            ]);
+        } catch (\Throwable $e) {
+            //桶不存在返回404
+            if (strstr('404', $e->getMessage())) {
+                return $this->setError('COS:' . $e->getMessage());
+            }
+        }
+        //创建桶
+        try {
+            $res = $app->createBucket([
+                'Bucket' => $name,
+                'ACL' => $acl,
+            ]);
+        } catch (\Throwable $e) {
+            if (strstr('[curl] 6', $e->getMessage())) {
+                return $this->setError('COS:无效的区域!!');
+            } else if (strstr('Access Denied.', $e->getMessage())) {
+                return $this->setError('COS:无权访问');
+            }
+            return $this->setError('COS:' . $e->getMessage());
+        }
+        return $res;
+    }
+
+    public function getRegion()
+    {
+        return [
+            [
+                'value' => 'cn-north-1',
+                'label' => '华北-北京'
+            ],
+            [
+                'value' => 'cn-east-1',
+                'label' => '华东-宿迁'
+            ],
+            [
+                'value' => 'cn-east-2',
+                'label' => '华东-上海'
+            ],
+            [
+                'value' => 'cn-south-1',
+                'label' => '华南-广州'
+            ]
+        ];
+    }
+
+    public function deleteBucket(string $name, string $region = '')
+    {
+        try {
+            $this->storageRegion = $region;
+            $this->app()->deleteBucket([
+                'Bucket' => $name, // REQUIRED
+            ]);
+            return true;
+        } catch (AcmException $e) {
+            return $this->setError($e->getMessage());
+        }
+    }
+
+    public function getDomian(string $name, string $region = null)
+    {
+        try {
+            $this->storageRegion = $region;
+            $res = $this->app()->getBucketPolicy([
+                'Bucket' => $name
+            ]);
+            return $res['DomainName'] ?? [];
+        } catch (\Throwable $e) {
+            return $this->setError($e->getMessage());
+        }
+    }
+
+    public function bindDomian(string $name, string $domain, string $region = null)
+    {
+        try {
+            $this->storageRegion = $region;
+            $this->app()->putBucketWebsite([
+                'Bucket' => $name,
+                'WebsiteConfiguration' => [
+                    'RedirectAllRequestsTo' => [
+                        'HostName' => $domain,
+                        'Protocol' => 'http'
+                    ]
+                ]
+            ]);
+            return true;
+        } catch (\Throwable $e) {
+            return $this->setError($e->getMessage());
+        }
+    }
+
+    public function setBucketCors(string $name, string $region)
+    {
+        $this->storageRegion = $region;
+        try {
+            $this->app()->putBucketCors([
+                'Bucket' => $name, // REQUIRED
+                'CORSConfiguration' => [ // REQUIRED
+                    'CORSRules' => [ // REQUIRED
+                        [
+                            'AllowedHeaders' => ['*'],
+                            'AllowedMethods' => ['POST', 'GET', 'PUT', 'DELETE', 'HEAD'], // REQUIRED
+                            'AllowedOrigins' => ['*'], // REQUIRED
+                            'ExposeHeaders' => ['Etag'],
+                            'MaxAgeSeconds' => 0
+                        ],
+                    ],
+                ]
+            ]);
+            return true;
+        } catch (\Throwable $e) {
+            return $this->setError($e->getMessage());
+        }
+    }
+
+    /**
+     * 获取OSS上传密钥
+     * @return mixed|void
+     */
+    public function getTempKeys($key = '', $path = '', $contentType = '', $expires = '+10 minutes')
+    {
+        try {
+            $app = $this->app();
+            $cmd = $app->getCommand(
+                'PutObject', [
+                    'Bucket' => $this->storageName,
+                    'Key' => $key,
+//                    'SourceFile' => $path,
+                    'ContentType' => $contentType
+                ]
+            );
+            $request = $app->createPresignedRequest($cmd, $expires, ['Scheme' => 'https']);
+            return [
+                'upload_url' => (string)$request->getUri(),
+                'type' => 'JDOSS',
+                'url' => $this->uploadUrl . '/' . $key
+            ];
+        } catch (\Throwable $e) {
+            return $this->setError($e->getMessage());
+        }
+    }
+
+    /**
+     * 缩略图
+     * @param string $filePath
+     * @param string $fileName
+     * @param string $type
+     * @return array|mixed
+     */
+    public function thumb(string $filePath = '', string $fileName = '', string $type = 'all')
+    {
+        $filePath = $this->getFilePath($filePath);
+        $data = ['big' => $filePath, 'mid' => $filePath, 'small' => $filePath];
+        $this->fileInfo->filePathBig = $this->fileInfo->filePathMid = $this->fileInfo->filePathSmall = $this->fileInfo->filePathWater = $filePath;
+        if ($filePath) {
+            $config = $this->thumbConfig;
+            foreach ($this->thumb as $v) {
+                if ($type == 'all' || $type == $v) {
+                    $height = 'thumb_' . $v . '_height';
+                    $width = 'thumb_' . $v . '_width';
+                    $key = 'filePath' . ucfirst($v);
+                    if (sys_config('image_thumbnail_status', 1) && isset($config[$height]) && isset($config[$width]) && $config[$height] && $config[$width]) {
+                        $this->fileInfo->$key = $filePath . '?x-oss-process=image/resize,h_' . $config[$height] . ',w_' . $config[$width];
+                        $this->fileInfo->$key = $this->water($this->fileInfo->$key);
+                        $data[$v] = $this->fileInfo->$key;
+                    } else {
+                        $this->fileInfo->$key = $this->water($this->fileInfo->$key);
+                        $data[$v] = $this->fileInfo->$key;
+                    }
+                }
+            }
+        }
+        return $data;
+    }
+
+    /**
+     * 水印
+     * @param string $filePath
+     * @return mixed|string
+     */
+    public function water(string $filePath = '')
+    {
+        $filePath = $this->getFilePath($filePath);
+        $waterConfig = $this->waterConfig;
+        $waterPath = $filePath;
+        if ($waterConfig['image_watermark_status'] && $filePath) {
+            if (strpos($filePath, '?x-oss-process') === false) {
+                $filePath .= '?x-oss-process=image';
+            }
+            switch ($waterConfig['watermark_type']) {
+                case 1://图片
+                    if (!$waterConfig['watermark_image']) {
+                        throw new AdminException(400722);
+                    }
+                    $waterPath = $filePath .= '/watermark,image_' . base64_encode($waterConfig['watermark_image']) . ',t_' . $waterConfig['watermark_opacity'] . ',g_' . ($this->position[$waterConfig['watermark_position']] ?? 'nw') . ',x_' . $waterConfig['watermark_x'] . ',y_' . $waterConfig['watermark_y'];
+                    break;
+                case 2://文字
+                    if (!$waterConfig['watermark_text']) {
+                        throw new AdminException(400723);
+                    }
+                    $waterConfig['watermark_text_color'] = str_replace('#', '', $waterConfig['watermark_text_color']);
+                    $waterPath = $filePath .= '/watermark,text_' . base64_encode($waterConfig['watermark_text']) . ',color_' . $waterConfig['watermark_text_color'] . ',size_' . $waterConfig['watermark_text_size'] . ',g_' . ($this->position[$waterConfig['watermark_position']] ?? 'nw') . ',x_' . $waterConfig['watermark_x'] . ',y_' . $waterConfig['watermark_y'];
+                    break;
+            }
+        }
+        return $waterPath;
+    }
+}

+ 198 - 0
crmeb/services/workerman/chat/ChatService.php

@@ -0,0 +1,198 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace crmeb\services\workerman\chat;
+
+
+use app\services\kefu\service\StoreServiceRecordServices;
+use app\services\kefu\service\StoreServiceServices;
+use Channel\Client;
+use crmeb\services\workerman\ChannelService;
+use crmeb\services\workerman\Response;
+use Workerman\Connection\TcpConnection;
+use Workerman\Lib\Timer;
+use Workerman\Worker;
+
+class ChatService
+{
+    /**
+     * @var Worker
+     */
+    protected $worker;
+
+    /**
+     * @var TcpConnection[]
+     */
+    protected $connections = [];
+
+    /**
+     * @var TcpConnection[]
+     */
+    protected $user = [];
+
+    /**
+     * 在线客服
+     * @var TcpConnection[]
+     */
+    protected $kefuUser = [];
+
+    /**
+     * @var ChatHandle
+     */
+    protected $handle;
+
+    /**
+     * @var Response
+     */
+    protected $response;
+
+    /**
+     * @var int
+     */
+    protected $timer;
+
+    public function __construct(Worker $worker)
+    {
+        $this->worker = $worker;
+        $this->handle = new ChatHandle($this);
+        $this->response = new Response();
+    }
+
+    public function setUser(TcpConnection $connection)
+    {
+        $this->user[$connection->user->uid] = $connection;
+    }
+
+    /**
+     * 获得当前在线客服
+     * @return TcpConnection[]
+     */
+    public function kefuUser()
+    {
+        return $this->kefuUser;
+    }
+
+    /**
+     * 设置当前在线客服
+     * @param TcpConnection $connection
+     */
+    public function setKefuUser(TcpConnection $connection, bool $isUser = true)
+    {
+        $this->kefuUser[$connection->kefuUser->uid] = $connection;
+        if ($isUser) {
+            $this->user[$connection->user->uid] = $connection;
+        }
+    }
+
+    public function user($key = null)
+    {
+        return $key ? ($this->user[$key] ?? false) : $this->user;
+    }
+
+    public function onConnect(TcpConnection $connection)
+    {
+        $this->connections[$connection->id] = $connection;
+        $connection->lastMessageTime = time();
+    }
+
+    public function onMessage(TcpConnection $connection, $res)
+    {
+        $connection->lastMessageTime = time();
+        $res = json_decode($res, true);
+        if (!$res || !isset($res['type']) || !$res['type'] || $res['type'] == 'ping') {
+            return $this->response->connection($connection)->success('ping', ['now' => time()]);
+        }
+        if (!method_exists($this->handle, $res['type'])) return;
+        try {
+            $this->handle->{$res['type']}($connection, $res + ['data' => []], $this->response->connection($connection));
+        } catch (\Throwable $e) {
+        }
+    }
+
+
+    public function onWorkerStart(Worker $worker)
+    {
+        ChannelService::connet();
+
+        Client::on('crmeb_chat', function ($eventData) use ($worker) {
+            if (!isset($eventData['type']) || !$eventData['type']) return;
+            $ids = isset($eventData['ids']) && count($eventData['ids']) ? $eventData['ids'] : array_keys($this->user);
+            $fun = $eventData['fun'] ?? false;
+            foreach ($ids as $id) {
+                if (isset($this->user[$id])) {
+                    if ($fun) {
+                        $this->handle->{$eventData['type']}($this->user[$id], $eventData + ['data' => []], $this->response->connection($this->user[$id]));
+                    } else {
+                        $this->response->connection($this->user[$id])->success($eventData['type'], $eventData['data'] ?? null);
+                    }
+                }
+            }
+        });
+
+        $this->timer = Timer::add(15, function () use (&$worker) {
+            $time_now = time();
+            foreach ($worker->connections as $connection) {
+                if ($time_now - $connection->lastMessageTime > 12) {
+                    //定时器判断当前用户是否下线
+                    if (isset($connection->user->uid) && !isset($connection->user->isTourist)) {
+                        /** @var StoreServiceRecordServices $service */
+                        $service = app()->make(StoreServiceRecordServices::class);
+                        $service->updateRecord(['to_uid' => $connection->user->uid], ['online' => 0]);
+                    }
+                    $this->response->connection($connection)->close('timeout');
+                    //广播给客服谁下线了
+                    foreach ($this->kefuUser as $uid => &$conn) {
+                        if (isset($connection->user->uid) && $connection->user->uid != $uid) {
+                            if (isset($conn->onlineUids) && ($key = array_search($connection->user->uid, $conn->onlineUids)) !== false) {
+                                unset($conn->onlineUids[$key]);
+                            }
+                            $this->response->connection($conn)->send('user_online', ['to_uid' => $connection->user->uid, 'online' => 0]);
+                        }
+                    }
+                }
+            }
+        });
+
+        Timer::add(2, function () use (&$worker) {
+            $uids = [];
+            foreach ($this->user() as $uid => $connection) {
+                if (!isset($connection->isTourist)) {
+                    $uids[] = $uid;
+                }
+            }
+            if ($uids) {
+                //除了当前在线的其他全部都下线
+                /** @var StoreServiceRecordServices $service */
+                $service = app()->make(StoreServiceRecordServices::class);
+                $service->updateOnline(['notUid' => $uids], ['online' => 0]);
+            }
+            $kefuUid = array_keys($this->kefuUser());
+            if ($kefuUid) {
+                /** @var StoreServiceServices $kefuService */
+                $kefuService = app()->make(StoreServiceServices::class);
+                $kefuService->updateOnline(['notUid' => $kefuUid], ['online' => 0]);
+            }
+        });
+    }
+
+
+    public function onClose(TcpConnection $connection)
+    {
+        var_dump('close');
+        unset($this->connections[$connection->id]);
+        if (isset($connection->user->uid)) {
+            unset($this->user[$connection->user->uid]);
+        }
+        if (isset($connection->kefuUser->uid)) {
+            unset($this->kefuUser[$connection->kefuUser->uid]);
+        }
+    }
+}

+ 119 - 0
crmeb/traits/ModelTrait.php

@@ -0,0 +1,119 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace crmeb\traits;
+
+use think\Model;
+
+/**
+ * Trait ModelTrait
+ * @package crmeb\traits
+ */
+trait ModelTrait
+{
+    /**
+     * 时间段搜索器
+     * @param Model $query
+     * @param $value
+     */
+    public function searchTimeAttr($query, $value, $data)
+    {
+        if ($value) {
+            $timeKey = $data['timeKey'] ?? 'add_time';
+            if (is_array($value)) {
+                $startTime = $value[0] ?? 0;
+                $endTime = $value[1] ?? 0;
+                if ($startTime || $endTime) {
+                    if ($startTime == $endTime || $endTime == strtotime(date('Y-m-d', $endTime))) {
+                        $endTime = $endTime + 86399;
+                    }
+                    $query->whereBetween($timeKey, [$startTime, $endTime]);
+                }
+            } elseif (is_string($value)) {
+                switch ($value) {
+                    case 'today':
+                    case 'week':
+                    case 'month':
+                    case 'year':
+                    case 'yesterday':
+                    case 'last year':
+                    case 'last week':
+                    case 'last month':
+                        $query->whereTime($timeKey, $value);
+                        break;
+                    case 'quarter':
+                        [$startTime, $endTime] = $this->getMonth();
+                        $query->whereBetween($timeKey, [strtotime($startTime), strtotime($endTime)]);
+                        break;
+                    case 'lately7':
+                        $query->whereBetween($timeKey, [strtotime("-7 day"), time()]);
+                        break;
+                    case 'lately30':
+                        $query->whereBetween($timeKey, [strtotime("-30 day"), time()]);
+                        break;
+                    default:
+                        if (strstr($value, '-') !== false) {
+                            [$startTime, $endTime] = explode('-', $value);
+                            $startTime = trim($startTime) ? strtotime($startTime) : 0;
+                            $endTime = trim($endTime) ? strtotime($endTime) : 0;
+                            if ($startTime && $endTime) {
+                                if ($startTime == $endTime || $endTime == strtotime(date('Y-m-d', $endTime))) {
+                                    $endTime = $endTime + 86399;
+                                }
+                                $query->whereBetween($timeKey, [$startTime, $endTime]);
+                            } else if (!$startTime && $endTime) {
+                                $query->whereTime($timeKey, '<', $endTime + 86400);
+                            } else if ($startTime && !$endTime) {
+                                $query->whereTime($timeKey, '>=', $startTime);
+                            }
+                        }
+                        break;
+                }
+            }
+        }
+    }
+
+    /**
+     * 获取本季度 time
+     * @param int $ceil
+     * @return array
+     */
+    public function getMonth(int $ceil = 0)
+    {
+        if ($ceil != 0) {
+            $season = ceil(date('n') / 3) - $ceil;
+        } else {
+            $season = ceil(date('n') / 3);
+        }
+        $firstday = date('Y-m-01', mktime(0, 0, 0, ($season - 1) * 3 + 1, 1, date('Y')));
+        $lastday = date('Y-m-t', mktime(0, 0, 0, $season * 3, 1, date('Y')));
+        return [$firstday, $lastday];
+    }
+
+    /**
+     * 获取某个字段内的值
+     * @param $value
+     * @param string $filed
+     * @param string $valueKey
+     * @param array|string[] $where
+     * @return mixed
+     */
+    public function getFieldValue($value, string $filed, ?string $valueKey = '', ?array $where = [])
+    {
+        $model = $this->where($filed, $value);
+        if ($where) {
+            $model->where(...$where);
+        }
+        return $model->value($valueKey ?: $filed);
+    }
+
+
+}

+ 102 - 0
crmeb/utils/JwtAuth.php

@@ -0,0 +1,102 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace crmeb\utils;
+
+
+use crmeb\exceptions\AdminException;
+use crmeb\services\CacheService;
+use Firebase\JWT\JWT;
+use think\facade\Env;
+
+/**
+ * Jwt
+ * Class JwtAuth
+ * @package crmeb\utils
+ */
+class JwtAuth
+{
+
+    /**
+     * token
+     * @var string
+     */
+    protected $token;
+
+    /**
+     * 获取token
+     * @param int|string $id
+     * @param string $type
+     * @param array $params
+     * @return array
+     */
+    public function getToken($id, string $type, array $params = []): array
+    {
+        $host = app()->request->host();
+        $time = time();
+        $exp_time = strtotime('+ 30day');
+        $params += [
+            'iss' => $host,
+            'aud' => $host,
+            'iat' => $time,
+            'nbf' => $time,
+            'exp' => $exp_time,
+        ];
+        $params['jti'] = compact('id', 'type');
+        $token = JWT::encode($params, Env::get('app.app_key', 'default'));
+
+        return compact('token', 'params');
+    }
+
+    /**
+     * 解析token
+     * @param string $jwt
+     * @return array
+     */
+    public function parseToken(string $jwt): array
+    {
+        $this->token = $jwt;
+        list($headb64, $bodyb64, $cryptob64) = explode('.', $this->token);
+        $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64));
+        return [$payload->jti->id, $payload->jti->type, $payload->pwd ?? ''];
+    }
+
+    /**
+     * 验证token
+     */
+    public function verifyToken()
+    {
+        JWT::$leeway = 60;
+
+        JWT::decode($this->token, Env::get('app.app_key', 'default'), array('HS256'));
+
+        $this->token = null;
+    }
+
+    /**
+     * 获取token并放入令牌桶
+     * @param $id
+     * @param string $type
+     * @param array $params
+     * @return array
+     * @throws \Psr\SimpleCache\InvalidArgumentException
+     */
+    public function createToken($id, string $type, array $params = [])
+    {
+        $tokenInfo = $this->getToken($id, $type, $params);
+        $exp = $tokenInfo['params']['exp'] - $tokenInfo['params']['iat'] + 60;
+        $res = CacheService::set(md5($tokenInfo['token']), ['uid' => $id, 'type' => $type, 'token' => $tokenInfo['token'], 'exp' => $exp], (int)$exp, $type);
+        if (!$res) {
+            throw new AdminException(100023);
+        }
+        return $tokenInfo;
+    }
+}

+ 208 - 0
crmeb/utils/Terminal.php

@@ -0,0 +1,208 @@
+<?php
+/**
+ *  +----------------------------------------------------------------------
+ *  | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+ *  +----------------------------------------------------------------------
+ *  | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+ *  +----------------------------------------------------------------------
+ *  | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+ *  +----------------------------------------------------------------------
+ *  | Author: CRMEB Team <admin@crmeb.com>
+ *  +----------------------------------------------------------------------
+ */
+
+namespace crmeb\utils;
+
+use think\Response;
+use think\console\Output;
+
+/**
+ * 执行命令
+ * Class Terminal
+ * @author 等风来
+ * @email 136327134@qq.com
+ * @date 2023/4/13
+ * @package crmeb\utils
+ */
+class Terminal
+{
+    /**
+     * 命令
+     * @var \string[][]
+     */
+    protected $command = [
+        'npm-build' => [
+            'run_root' => '',
+            'command' => 'npm run build',
+        ],
+        'npm-install' => [
+            'run_root' => '',
+            'command' => 'npm run install',
+        ],
+    ];
+
+    /**
+     * 执行内容保存地址
+     * @var string
+     */
+    protected $outputFile;
+
+    /**
+     * 执行状态
+     * @var integer
+     */
+    protected $procStatus;
+
+    /**
+     * 响应内容
+     * @var string
+     */
+    protected $outputContent;
+
+    /**
+     * @var
+     */
+    protected $output;
+
+    /**
+     * Terminal constructor.
+     */
+    public function __construct()
+    {
+        $this->command['npm-build']['run_root'] = str_replace('DS', DS, config('app.admin_template_path'));
+        $this->command['npm-install']['run_root'] = str_replace('DS', DS, config('app.admin_template_path'));
+
+        $outputDir = root_path() . 'runtime' . DIRECTORY_SEPARATOR . 'terminal';
+        $this->outputFile = $outputDir . DIRECTORY_SEPARATOR . 'exec.log';
+        if (!is_dir($outputDir)) {
+            mkdir($outputDir, 0755, true);
+        }
+        file_put_contents($this->outputFile, '');
+    }
+
+    /**
+     * @param Output $output
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/4/13
+     */
+    public function setOutput(Output $output)
+    {
+        $this->output = $output;
+    }
+
+    /**
+     * @return string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/4/13
+     */
+    public function adminTemplatePath()
+    {
+        return $this->command['npm-install']['run_root'];
+    }
+
+    /**
+     * 执行
+     * @param string $name
+     * @return string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/4/13
+     */
+    public function run(string $name)
+    {
+        if (!function_exists('proc_open')) {
+            throw new \RuntimeException('缺少proc_open函数无法运行');
+        }
+
+        if (!isset($this->command[$name])) {
+            throw new \RuntimeException('运行的命令不存在');
+        }
+
+        $command = $this->command[$name];
+
+        $descriptorspec = [0 => ['pipe', 'r'], 1 => ['file', $this->outputFile, 'w'], 2 => ['file', $this->outputFile, 'w']];
+        $process = proc_open($command['command'], $descriptorspec, $pipes, $command['run_root'], null, ['suppress_errors' => true]);
+        if (is_resource($process)) {
+            while ($this->getProcStatus($process)) {
+                $contents = file_get_contents($this->outputFile);
+                if (strlen($contents) && $this->outputContent != $contents) {
+                    $newOutput = str_replace($this->outputContent, '', $contents);
+                    if (preg_match('/\r\n|\r|\n/', $newOutput)) {
+                        $this->echoOutputFlag($newOutput);
+                        $this->outputContent = $contents;
+                    }
+                }
+                usleep(500000);
+            }
+            foreach ($pipes as $pipe) {
+                fclose($pipe);
+            }
+            proc_close($process);
+        }
+
+        return $this->output('run success');
+    }
+
+    /**
+     * 判断状态
+     * @param $process
+     * @return bool
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/4/13
+     */
+    public function getProcStatus($process): bool
+    {
+        $status = proc_get_status($process);
+        if ($status['running']) {
+            $this->procStatus = 1;
+            return true;
+        } elseif ($this->procStatus === 1) {
+            $this->procStatus = 0;
+            $this->output('exit: ' . $status['exitcode']);
+            if ($status['exitcode'] === 0) {
+                $this->echoOutputFlag('success');
+            } else {
+                $this->echoOutputFlag('error');
+            }
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 直接输入响应
+     * @param string $message
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/4/13
+     */
+    public function echoOutputFlag(string $message)
+    {
+        if ($this->output && $this->output instanceof Output) {
+            $this->output->info($message);
+        } else {
+            echo $this->output($message);
+            @ob_flush();
+        }
+    }
+
+    /**
+     * 返回响应内容
+     * @param string $data
+     * @return string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2023/4/13
+     */
+    private function output($data)
+    {
+        $data = [
+            'message' => $data,
+        ];
+        return Response::create($data, 'json')->getContent();
+    }
+}

+ 24 - 0
filetree.txt

@@ -0,0 +1,24 @@
+.
+├── app 程序主目录
+├── backup 数据库/程序升级备份目录
+├── config 系统配置
+├── crmeb crmeb核心文件目录
+├── public 程序入口目录
+├── route 程序入口路由配置
+├── runtime 缓存目录
+├── temp docker临时文件目录
+├── vendor 扩展包compose 安装目录
+├── LICENSE.txt 开源协议
+├── README.md 系统说明
+├── baota.sh 宝塔快速配置环境文件
+├── build.example.php
+├── composer.json
+├── composer.lock
+├── filetree.txt
+├── think tp命令文件入口
+├── timer.pid 定时任务生成文件
+├── updatesql.sql
+├── workerman.bat windows 启动workerman命令文件
+└── workerman.pid workerman生成文件
+
+9 directories, 12 files

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/css.worker.js


+ 1 - 0
public/admin/system_static/css/chunk-054ceee2.42b60f9c.css

@@ -0,0 +1 @@
+.box[data-v-25599488]{width:100%;height:100%;background:#fff}[data-v-25599488] .el-card__body{min-height:700px;padding:16px 16px 16px 0}[data-v-25599488] .conter .pictrueList{max-width:100%}

+ 1 - 0
public/admin/system_static/css/chunk-06f6b9ec.726a2d93.css

@@ -0,0 +1 @@
+[data-v-29743fd4] .ivu-table-cell-tree{border:0;font-size:15px;background-color:unset}[data-v-29743fd4] .ivu-table-cell-tree .ivu-icon-ios-add:before{content:"\F11F"}[data-v-29743fd4] .ivu-table-cell-tree .ivu-icon-ios-remove:before{content:"\F116"}.button[data-v-29743fd4]{width:300px}

+ 1 - 0
public/admin/system_static/css/chunk-0a437896.46acedbb.css

@@ -0,0 +1 @@
+[data-v-3a4c0be8]{-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}[data-v-3a4c0be8] .el-card__body{padding:60px 20px}.left[data-v-3a4c0be8]{min-width:390px;min-height:550px;position:relative;padding-left:40px}.top[data-v-3a4c0be8]{position:absolute;top:0}.bottom[data-v-3a4c0be8],.textbot[data-v-3a4c0be8]{position:absolute;bottom:0}.textbot[data-v-3a4c0be8]{left:55px;width:100%}.active[data-v-3a4c0be8]{border:1px solid var(--prev-color-primary)!important;color:var(--prev-color-primary)!important}.li[data-v-3a4c0be8]{float:left;width:92px;height:48px;line-height:48px;border-left:1px solid #e7e7eb;background:#fafafa;text-align:center;cursor:pointer;color:#999;position:relative}.text[data-v-3a4c0be8]{height:50px;white-space:nowrap;width:100%;overflow:hidden;text-overflow:ellipsis;padding:0 5px}.text[data-v-3a4c0be8]:hover{color:#000}.add[data-v-3a4c0be8]{position:absolute;bottom:65px;width:100%;line-height:40px;background:#fafafa}.arrow[data-v-3a4c0be8]{position:absolute;bottom:-16px;left:36px;width:0;height:0;font-size:0;border:8px solid;border-color:#fafafa #f4f5f9 #f4f5f9 #f4f5f9}.tianjia[data-v-3a4c0be8]{position:absolute;bottom:107px;width:100%;line-height:48px;background:#fafafa}.tianjia[data-v-3a4c0be8] :first-child{border:none}.addadd[data-v-3a4c0be8]{width:100%;line-height:40px;border-top:1px solid #f0f0f0;background:#fafafa;height:40px}.right[data-v-3a4c0be8]{background:#fff;min-height:400px}.spwidth[data-v-3a4c0be8]{width:100%}.userAlert[data-v-3a4c0be8]{margin-top:16px!important}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/css/chunk-2260d7bc.14c700eb.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/css/chunk-255a5262.c1857241.css


+ 1 - 0
public/admin/system_static/css/chunk-27866995.f3bf3511.css

@@ -0,0 +1 @@
+[data-v-49892e8c] .ivu-form-item-content{line-height:unset!important}

+ 1 - 0
public/admin/system_static/css/chunk-2dd5f758.21cda798.css

@@ -0,0 +1 @@
+.card_box_cir1[data-v-50f2a66a] .iconfont{font-size:26px;color:#fff}.one[data-v-50f2a66a]{background:#e4ecff}.two[data-v-50f2a66a]{background:#fff3e0}.three[data-v-50f2a66a]{background:#eaf9e1}.four[data-v-50f2a66a]{background:#ffeaf4}.five[data-v-50f2a66a]{background:#f1e4ff}.one1[data-v-50f2a66a]{background:#4d7cfe}.two1[data-v-50f2a66a]{background:#ffab2b}.three1[data-v-50f2a66a]{background:#6dd230}.four1[data-v-50f2a66a]{background:#ff85c0}.five1[data-v-50f2a66a]{background:#b37feb}.card_box[data-v-50f2a66a]{width:100%;height:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px}.card_box .card_box_cir[data-v-50f2a66a]{width:60px;height:60px;overflow:hidden;margin-right:20px}.card_box .card_box_cir .card_box_cir1[data-v-50f2a66a],.card_box .card_box_cir[data-v-50f2a66a]{border-radius:50%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.card_box .card_box_cir .card_box_cir1[data-v-50f2a66a]{width:48px;height:48px}.card_box .card_box_txt .sp1[data-v-50f2a66a]{display:block;color:#252631;font-size:24px}.card_box .card_box_txt .sp2[data-v-50f2a66a]{display:block;color:#98a9bc;font-size:12px}.tab_data[data-v-34676e74] .ivu-form-item-content{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.z-price[data-v-34676e74]{color:red}.f-price[data-v-34676e74]{color:green}

+ 1 - 0
public/admin/system_static/css/chunk-2ef23dd9.f386949a.css

@@ -0,0 +1 @@
+.video-style[data-v-241355b2]{width:40%;height:180px;border-radius:10px;background-color:#707070;margin-top:10px;position:relative;overflow:hidden}.video-style .iconv[data-v-241355b2]{color:#fff;line-height:180px;width:50px;height:50px;display:inherit;font-size:26px;position:absolute;top:-74px;left:50%;margin-left:-25px}.video-style .mark[data-v-241355b2]{position:absolute;width:100%;height:30px;top:0;background-color:rgba(0,0,0,.5);text-align:center}

+ 1 - 0
public/admin/system_static/css/chunk-2fcc8f66.89adb531.css

@@ -0,0 +1 @@
+.radio[data-v-35556d70]{margin-bottom:14px}.radio[data-v-35556d70] .name{width:125px;text-align:right;padding-right:12px}

+ 1 - 0
public/admin/system_static/css/chunk-37c962e4.d378c462.css

@@ -0,0 +1 @@
+[data-v-6c39d62e] .el-tabs__item{height:54px!important;line-height:54px!important}.message[data-v-6c39d62e] .ivu-table-header thead tr th{padding:8px 16px}.message[data-v-6c39d62e] .ivu-tabs-tab{border-radius:0!important}.table-box[data-v-6c39d62e]{padding:20px}.is-table[data-v-6c39d62e]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.btn[data-v-6c39d62e]{padding:6px 0;cursor:pointer;font-size:10px;border-radius:3px}.is-switch-close[data-v-6c39d62e]{background-color:#504444}.is-switch[data-v-6c39d62e]{background-color:#eb5252}.notice-list[data-v-6c39d62e]{background-color:#308cf5;margin:0 15px}.table[data-v-6c39d62e]{padding:0 18px}.alert_title[data-v-6c39d62e]{margin-bottom:5px;font-weight:700}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/css/chunk-39128d0a.9c8c48d7.css


+ 1 - 0
public/admin/system_static/css/chunk-407053db.de7b2639.css

@@ -0,0 +1 @@
+.box[data-v-203f8ae6]{width:100%;background:#fff}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/css/chunk-42301c54.98e7291e.css


+ 1 - 0
public/admin/system_static/css/chunk-459e289b.71ba06fd.css

@@ -0,0 +1 @@
+.ivu-form-item[data-v-43a9957e]{margin-bottom:0}.picBox[data-v-43a9957e]{display:inline-block;cursor:pointer}.picBox .upLoad[data-v-43a9957e]{width:58px;height:58px;line-height:58px;border:1px dotted rgba(0,0,0,.1);border-radius:4px;background:rgba(0,0,0,.02)}.picBox .pictrue[data-v-43a9957e]{width:60px;height:60px;border:1px dotted rgba(0,0,0,.1);margin-right:10px}.picBox .pictrue img[data-v-43a9957e]{width:100%;height:100%}[data-v-43a9957e] .ivu-menu-vertical .ivu-menu-item-group-title,[data-v-43a9957e] .ivu-menu-vertical.ivu-menu-light:after{display:none}.left-wrapper[data-v-43a9957e]{height:904px;background:#fff;border-right:1px solid #f2f2f2}.menu-item[data-v-43a9957e]{z-index:50;position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;word-break:break-all}.icon-box[data-v-43a9957e]{z-index:3;position:absolute;right:20px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);display:none}:hover .icon-box[data-v-43a9957e]{display:block}.right-menu[data-v-43a9957e]{z-index:10;position:absolute;right:-106px;top:-11px;width:auto;min-width:121px}.tabBox_img[data-v-43a9957e]{width:36px}.tabBox_img height 36px[data-v-43a9957e]{border-radius:4px}.tabBox_img cursor pointer img[data-v-43a9957e]{width:100%;height:100%}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/css/chunk-520bc5d1.0f7e4e10.css


+ 1 - 0
public/admin/system_static/css/chunk-54ffe028.8864ee88.css

@@ -0,0 +1 @@
+.QRpic[data-v-f9c3e408]{width:180px;height:180px}.QRpic img[data-v-f9c3e408]{width:100%;height:100%}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/css/chunk-5552d05c.b8f99837.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/css/chunk-60512542.a01f46a7.css


+ 1 - 0
public/admin/system_static/css/chunk-69ebc320.2470f470.css

@@ -0,0 +1 @@
+.tabBox_img[data-v-087bfb5d]{width:36px;height:36px;border-radius:4px;cursor:pointer}.tabBox_img img[data-v-087bfb5d]{width:100%;height:100%}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/css/chunk-6b55a8d4.fbce5b98.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/css/chunk-7273a738.ccf08c68.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/css/chunk-7a95730c.0b5c5be7.css


+ 1 - 0
public/admin/system_static/css/chunk-80a89046.030e9cdb.css

@@ -0,0 +1 @@
+.tabBox_img[data-v-2e682095]{width:36px;height:36px;border-radius:4px;cursor:pointer}.tabBox_img img[data-v-2e682095]{width:100%;height:100%}

+ 1 - 0
public/admin/system_static/css/chunk-824bdc78.03faf95f.css

@@ -0,0 +1 @@
+[data-v-6ff58f58] .ivu-menu-vertical .ivu-menu-item-group-title,[data-v-6ff58f58] .ivu-menu-vertical.ivu-menu-light:after{display:none}.left-wrapper[data-v-6ff58f58]{height:904px;background:#fff;border-right:1px solid #f2f2f2}.menu-item[data-v-6ff58f58]{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;word-break:break-all}.menu-item .icon-box[data-v-6ff58f58]{z-index:3;position:absolute;right:20px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);display:none}.menu-item:hover .icon-box[data-v-6ff58f58]{display:block}.menu-item .right-menu[data-v-6ff58f58]{z-index:10;position:absolute;right:-106px;top:-11px;width:auto;min-width:121px}.tabBox-img[data-v-6ff58f58]{width:36px;height:36px;border-radius:4px;cursor:pointer}.tabBox-img img[data-v-6ff58f58]{width:100%;height:100%}.ivu-menu[data-v-6ff58f58]{z-index:auto}.header[data-v-6ff58f58],.headers[data-v-6ff58f58]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;background-color:#f2f2f2;padding:8px}.header .search[data-v-6ff58f58],.headers .search[data-v-6ff58f58]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.header .search>div[data-v-6ff58f58],.headers .search>div[data-v-6ff58f58]{margin-right:10px}.search[data-v-6ff58f58] .ivu-select-selection{border:1px solid #dcdee2!important}.headers[data-v-6ff58f58]{background-color:#fff;margin-bottom:20px}[data-v-6ff58f58] .ivu-modal-mask,[data-v-6ff58f58] .ivu-modal-wrap{z-index:100!important}.add-task[data-v-6ff58f58]{margin:10px 0}

+ 1 - 0
public/admin/system_static/css/chunk-9b878236.c55a5fcc.css

@@ -0,0 +1 @@
+[data-v-eb060402] .ivu-tag-cyan .ivu-tag-text{color:#19be6b!important}.ivu-tag-cyan[data-v-eb060402]{background:rgba(25,190,170,.1);border-color:#19be6b!important}.tabBox_img[data-v-eb060402]{width:36px;height:36px;border-radius:4px;cursor:pointer}.tabBox_img img[data-v-eb060402]{width:100%;height:100%}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/css/chunk-ac9c889e.7a6f46fc.css


+ 1 - 0
public/admin/system_static/css/chunk-e80cb8da.bfd2a79d.css

@@ -0,0 +1 @@
+.tabBox_img[data-v-87f93410]{width:36px;height:36px;border-radius:4px;cursor:pointer}.tabBox_img img[data-v-87f93410]{width:100%;height:100%}.modelBox .ivu-table-header[data-v-87f93410],.modelBox[data-v-87f93410]{width:100%!important}.trees-coadd[data-v-87f93410]{width:100%;height:385px}.trees-coadd .scollhide[data-v-87f93410]{width:100%;height:100%;overflow-x:hidden;overflow-y:scroll}.scollhide[data-v-87f93410]::-webkit-scrollbar{display:none}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/css/chunk-f010ee82.2c12ce16.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/css/chunk-vendors.1310688b.css


BIN
public/admin/system_static/fonts/element-icons.535877f5.woff


BIN
public/admin/system_static/img/bg2.c636f6a6.png


BIN
public/admin/system_static/img/bluesgin.032bae4b.png


BIN
public/admin/system_static/img/default.6b914f9c.jpg


BIN
public/admin/system_static/img/member.b885cf62.png


BIN
public/admin/system_static/img/mobilehead.1c931282.png


BIN
public/admin/system_static/img/no-msg.74b02921.png


BIN
public/admin/system_static/img/no_user.a09b282b.png


BIN
public/admin/system_static/img/no_zf.e61fe9b5.png


BIN
public/admin/system_static/img/oragesgin.00077d3a.png


BIN
public/admin/system_static/img/redsgin.d8b0c12e.png


BIN
public/admin/system_static/img/sort01.e157a5ea.jpg


BIN
public/admin/system_static/img/sort02.feab1b79.jpg


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/app.6172b51c.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-008e3316.4897ef32.js


+ 1 - 0
public/admin/system_static/js/chunk-054ceee2.c3584afb.js

@@ -0,0 +1 @@
+(window.webpackJsonp=window.webpackJsonp||[]).push([["chunk-054ceee2"],{"015e":function(t,e,i){"use strict";i("0cf1")},"0cf1":function(t,e,i){},8084:function(t,e,i){"use strict";i.r(e);var s={components:{uploadFile:i("b0e7").a},name:"system_file",data:function(){return{pageLimit:12,uploadShow:!1}},mounted:function(){this.uploadShow=!0},methods:{}};i("015e"),i=i("2877"),i=Object(i.a)(s,(function(){var t=this._self._c;return t("div",[t("el-card",{staticClass:"ivu-mt",attrs:{bordered:!1,shadow:"never"}},[t("div",{ref:"picBox",staticClass:"box"},[t("upload-file",{attrs:{isPage:!0,isShow:0,pageLimit:this.pageLimit}})],1)])],1)}),[],!1,null,"25599488",null);e.default=i.exports}}]);

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-06f6b9ec.eb040a4f.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-089b48fd.7e9b3eab.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-176009a8.aec761b6.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-1eb01899.b37b8714.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-226ef389.55807df0.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-27866995.f7a5a1e6.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-2d0aab07.53f43767.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-2d0aeb45.65f1ee16.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-2d0b21d7.4e9c6d4a.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-2d0c46d1.781a78ba.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-2d0c8f4c.a45adcd1.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-2d0d0645.0a3abe36.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-2d0de971.a66aa084.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-2d0e488e.182beb0c.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-2d221799.cadc0e5d.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-2d221814.9cd07d48.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-2d2295e9.c8fc2374.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-2ef23dd9.c8d49cac.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
public/admin/system_static/js/chunk-3899d053.ebd17e02.js


Vissa filer visades inte eftersom för många filer har ändrats