zory 2 tuần trước cách đây
mục cha
commit
01fa987a87
80 tập tin đã thay đổi với 3927 bổ sung0 xóa
  1. 20 0
      .gitignore
  2. 21 0
      LICENSE
  3. 133 0
      app/command/Model.php
  4. 29 0
      app/controller/admin/Dashboard.php
  5. 34 0
      app/controller/common/Common.php
  6. 144 0
      app/controller/common/Login.php
  7. 78 0
      app/controller/common/Menu.php
  8. 141 0
      app/extra/basic/Base.php
  9. 30 0
      app/extra/basic/Model.php
  10. 103 0
      app/extra/basic/Service.php
  11. 27 0
      app/extra/service/system/MenuService.php
  12. 183 0
      app/extra/service/system/SystemService.php
  13. 38 0
      app/extra/service/system/UserService.php
  14. 135 0
      app/extra/tools/CodeExtend.php
  15. 73 0
      app/extra/tools/DataExtend.php
  16. 122 0
      app/extra/tools/UploadExtend.php
  17. 406 0
      app/functions.php
  18. 45 0
      app/middleware/AuthMiddleware.php
  19. 42 0
      app/middleware/StaticFile.php
  20. 51 0
      app/model/saas/SaasAgent.php
  21. 48 0
      app/model/system/SystemConfig.php
  22. 45 0
      app/model/system/SystemData.php
  23. 49 0
      app/model/system/SystemExport.php
  24. 54 0
      app/model/system/SystemMenu.php
  25. 48 0
      app/model/system/SystemOplog.php
  26. 64 0
      app/model/system/SystemUser.php
  27. 10 0
      app/process/Http.php
  28. 305 0
      app/process/Monitor.php
  29. 14 0
      app/view/index/view.html
  30. 82 0
      composer.json
  31. 26 0
      config/app.php
  32. 21 0
      config/autoload.php
  33. 18 0
      config/bootstrap.php
  34. 6 0
      config/container.php
  35. 15 0
      config/dependence.php
  36. 5 0
      config/event.php
  37. 17 0
      config/exception.php
  38. 32 0
      config/log.php
  39. 15 0
      config/middleware.php
  40. 32 0
      config/plugin/hhink/webman-sms/app.php
  41. 8 0
      config/plugin/hzdad/codecheck/app.php
  42. 35 0
      config/plugin/linfly/annotation/annotation.php
  43. 4 0
      config/plugin/linfly/annotation/app.php
  44. 19 0
      config/plugin/linfly/annotation/bootstrap.php
  45. 17 0
      config/plugin/linfly/annotation/route.php
  46. 71 0
      config/plugin/shopwwi/auth/app.php
  47. 12 0
      config/plugin/tinywan/captcha/app.php
  48. 76 0
      config/plugin/tinywan/storage/app.php
  49. 24 0
      config/plugin/webman/console/app.php
  50. 4 0
      config/plugin/webman/event/app.php
  51. 17 0
      config/plugin/webman/event/bootstrap.php
  52. 7 0
      config/plugin/webman/event/command.php
  53. 14 0
      config/plugin/webman/rate-limiter/app.php
  54. 17 0
      config/plugin/webman/rate-limiter/bootstrap.php
  55. 8 0
      config/plugin/webman/rate-limiter/middleware.php
  56. 4 0
      config/plugin/webman/redis-queue/app.php
  57. 7 0
      config/plugin/webman/redis-queue/command.php
  58. 32 0
      config/plugin/webman/redis-queue/log.php
  59. 11 0
      config/plugin/webman/redis-queue/process.php
  60. 21 0
      config/plugin/webman/redis-queue/redis.php
  61. 49 0
      config/process.php
  62. 29 0
      config/redis.php
  63. 21 0
      config/route.php
  64. 10 0
      config/server.php
  65. 65 0
      config/session.php
  66. 23 0
      config/static.php
  67. 38 0
      config/think-cache.php
  68. 42 0
      config/think-orm.php
  69. 13 0
      config/translation.php
  70. 22 0
      config/view.php
  71. BIN
      public/favicon.ico
  72. 3 0
      resource/translations/en/messages.php
  73. 40 0
      resource/translations/zh_CN/messages.php
  74. 5 0
      start.php
  75. 25 0
      support/Request.php
  76. 24 0
      support/Response.php
  77. 139 0
      support/bootstrap.php
  78. 71 0
      webman
  79. 3 0
      windows.bat
  80. 136 0
      windows.php

+ 20 - 0
.gitignore

@@ -0,0 +1,20 @@
+.DS_Store
+/vendor
+/runtime
+
+# local env files
+.env.local
+.env
+.env.*.local
+
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+/composer.lock

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 walkor<walkor@workerman.net> and contributors (see https://github.com/walkor/webman/contributors)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 133 - 0
app/command/Model.php

@@ -0,0 +1,133 @@
+<?php
+
+namespace app\command;
+
+use support\think\Db;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use think\db\exception\BindParamException;
+use Webman\Console\Util;
+
+class Model extends Command
+{
+
+
+    protected static string $defaultName = 'model:all';
+    protected static string $defaultDescription = '统一生成Model';
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $output->writeln("生成Model开始====>".getDateFull());
+        try {
+            $table = Db::query("SHOW TABLES");
+        } catch (BindParamException $e) {
+
+        }
+        $connection = config("think-orm.default");
+        $database = config("think-orm.connections.$connection.database");
+        foreach ($table as $val) {
+            $dir = explode("_",$val["Tables_in_{$database}"]);
+            $dirPath = app_path("model").DIRECTORY_SEPARATOR.$dir[0];
+            $namespace = str_replace('/', '\\', 'app/model/' . $dir[0]);
+            $class = Util::nameToClass($val["Tables_in_{$database}"]);
+            $file = $dirPath . DIRECTORY_SEPARATOR . "$class.php";
+            if (file_exists($file)) { // 存在,跳过
+                $output->writeln("已存在,跳过====>".getDateFull()."====>".$file);
+            } else {
+                $path = pathinfo($file, PATHINFO_DIRNAME);
+                if (!is_dir($path)) {
+                    mkdir($path, 0777, true);
+                }
+                $properties = '';
+                $pk = 'id';
+                foreach (Db::query("select COLUMN_NAME,DATA_TYPE,COLUMN_KEY,COLUMN_COMMENT from INFORMATION_SCHEMA.COLUMNS where table_name = '".$val["Tables_in_{$database}"]."' and table_schema = '$database' ORDER BY ordinal_position") as $item) {
+                    if ($item['COLUMN_KEY'] === 'PRI') {
+                        $pk = $item['COLUMN_NAME'];
+                        $item["COLUMN_COMMENT"] .= "(主键)";
+                    }
+                    $type = $this->getType($item["DATA_TYPE"]);
+                    $properties .= " * @property $type \${$item["COLUMN_NAME"]} {$item["COLUMN_COMMENT"]}\n";
+                }
+                $properties = rtrim($properties) ?: ' *';
+                $table_val = $val["Tables_in_{$database}"];
+                $model_content = <<<EOF
+<?php
+
+namespace $namespace;
+
+use app\\extra\\basic\\Model;
+
+
+/**
+$properties
+ */
+class $class extends Model
+{
+    /**
+     * The connection name for the model.
+     *
+     * @var string|null
+     */
+    protected \$connection = 'mysql';
+    
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected string \$table = "$table_val";
+    
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected string \$primaryKey = "$pk";
+    
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public bool \$timestamps = false;
+
+
+}
+
+EOF;
+                file_put_contents($file, $model_content);
+                $output->writeln("生成成功====>".getDateFull()."====>".$file);
+            }
+        }
+        return self::SUCCESS;
+    }
+
+
+    protected function getType(string $type): string
+    {
+        if (str_contains($type, 'int')) {
+            return 'integer';
+        }
+        switch ($type) {
+            case 'varchar':
+            case 'string':
+            case 'text':
+            case 'date':
+            case 'time':
+            case 'guid':
+            case 'datetimetz':
+            case 'datetime':
+            case 'decimal':
+            case 'enum':
+                return 'string';
+            case 'boolean':
+                return 'integer';
+            case 'float':
+                return 'float';
+            default:
+                return 'mixed';
+        }
+    }
+
+}

+ 29 - 0
app/controller/admin/Dashboard.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use support\Response;
+use Webman\Annotation\Middleware;
+
+#[Controller(prefix: "/api/dashboard"),Middleware(AuthMiddleware::class)]
+class Dashboard extends Base
+{
+
+    /**
+     * @return Response
+     */
+    #[Route(path: "get",methods: "get")]
+    public function getDashData(): Response
+    {
+        try {
+            return successTrans("success.data",[]);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 34 - 0
app/controller/common/Common.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace app\controller\common;
+
+use app\extra\basic\Base;
+use app\model\system\SystemConfig;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use support\Response;
+use Tinywan\Captcha\Captcha;
+
+/**
+ *
+ */
+#[Controller(prefix: "/api/service")]
+class Common extends Base
+{
+
+    /**
+     *
+     */
+    #[Route(path: "data",methods: "get")]
+    public function getServiceData(): Response
+    {
+        try {
+            $captcha = Captcha::base64();
+            $service = (new SystemConfig)->where("type","service")->column("value","name");
+            return successTrans("success.data",compact('captcha','service'));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 144 - 0
app/controller/common/Login.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace app\controller\common;
+
+use app\extra\basic\Base;
+use app\extra\service\basic\SmsService;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasAgent;
+use app\model\system\SystemUser;
+use Hzdad\Codecheck\Codecheck;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use Shopwwi\WebmanAuth\Auth;
+use support\Request;
+use support\Response;
+use think\facade\Db;
+use Tinywan\Captcha\Captcha;
+use Webman\Annotation\Middleware;
+
+
+#[Controller(prefix: "/api/login")]
+class Login extends Base
+{
+
+    /**
+     * 登陆
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "user",methods: "post")]
+    public function setLogin(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "username.require"  => trans("empty.user"),
+                "password.require"  => trans("empty.passwd"),
+                "code.require"      => trans("empty.code"),
+                "key.require"       => trans("empty.data"),
+            ],"post");
+            if (!is_array($param)) return error($param);
+            if (Captcha::check($param['code'],$param['key']) === false) return errorTrans("error.captcha");
+            $map = ["is_deleted" => 0,"username" => $param['username']];
+            [$state,$msg,$user] = $this->checkLogin($map,2,$param);
+            if (!$state) return error($msg);
+            return successTrans("success.login",get_object_vars((new Auth)->guard("admin")->login($user)));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 手机号码登陆
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "mobile",methods: "post")]
+    public function setLogin2Mobile(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "mobile.require"    => trans("empty.mobile"),
+                "code.require"      => trans("empty.code"),
+                "scene.require"     => trans("empty.data"),
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $code = (new Codecheck)->mobile($param['mobile'])->scene($param['scene'])->code($param['code'])->check();
+            if (!$code) return errorTrans("error.captcha");
+            $map = ["is_deleted" => 0,"mobile" => $param['mobile']];
+            [$state,$msg,$user] = $this->checkLogin($map);
+            if (!$state) return error($msg);
+            return successTrans("success.login",get_object_vars((new Auth)->guard("admin")->login($user)));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 登录验证处理
+     * @param array $map
+     * @param int $type
+     * @param array $param
+     * @return array
+     */
+    protected function checkLogin(array $map = [],int $type = 1,array $param = []): array
+    {
+        $user = (new SystemUser)->where($map)->findOrEmpty();
+        if ($user->isEmpty()) return [0,trans("error.user-empty"),[]];
+        if ($user['status'] <> 1) return [0,trans("error.user-status"),[]];
+        if ($user['type'] > 1) {
+            $typeUser = $this->getTypeUser($user['agent_id']);
+            if (empty($typeUser)) return [0,trans("empty.agent"),[]];
+            if ($typeUser['status'] <> 1) return [0,trans("error.agent"),[]];
+            if (time() > strtotime($typeUser['vip_at'])) return [0,trans("error.agent-out"),[]];
+        }
+        if ($type == 2) {
+            if (md5($param['password'].$user['salt']) <> $user['password']) return [0,trans("error.passwd"),[]];
+        }
+        $user->login_at = getDateFull();
+        $user->login_ip = request()->getRealIp();
+        $user->login_num = Db::raw("login_num+1");
+        $user->save();
+        return [1,'success',$user->toArray()];
+    }
+
+    /**
+     * 获取代理信息
+     * @param int $agentId
+     * @return array
+     */
+    protected function getTypeUser(int $agentId = 0): array
+    {
+        return (new SaasAgent)->where("agent_id",$agentId)->findOrEmpty()->toArray();
+    }
+
+    /**
+     * @return Response
+     */
+    #[Route(path: "profile",methods: "get"),Middleware(AuthMiddleware::class)]
+    public function getLoginUser(): Response
+    {
+        try {
+            $userData = (new Auth)->guard("admin")->user()->toArray();
+            if (isset($userData['password'])) unset($userData['password']);
+            $agent = (new SaasAgent)->where("agent_id",$userData['agent_id'])->findOrEmpty();
+            if (empty($agent['vip_at']))
+            {
+                $userData['vip_at'] = 0;
+            } else {
+                $userData['vip_at'] = strtotime($agent['vip_at']);
+            }
+            return successTrans("success.data",[
+                "username"  => $userData['username'],
+                "truename"  => $userData['truename'],
+                "vip_at"    => $userData['vip_at'],
+                "super"     => $userData['is_super'],
+                "type"      => $userData['type']
+            ]);
+        } catch (\Throwable $exception){
+            return error($exception->getMessage());
+        }
+    }
+
+}

+ 78 - 0
app/controller/common/Menu.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace app\controller\common;
+
+use app\extra\basic\Base;
+use app\extra\service\system\MenuService;
+use app\extra\tools\DataExtend;
+use app\middleware\AuthMiddleware;
+use app\model\system\SystemMenu;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+use Webman\Annotation\Middleware;
+
+
+#[Controller(prefix: "/api/menu"),Middleware(AuthMiddleware::class)]
+class Menu extends Base
+{
+
+    #[Inject]
+    protected MenuService $service;
+
+    /**
+     * 获取菜单
+     */
+    #[Route(path: "list",methods: "get")]
+    public function getMenuList(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "form.default"  => $request->user['type'],
+                "type.default"  => 1
+            ]);
+            $hide = 0;
+            if ($request->user['type'] == 3) { // 门店
+
+            }
+            $menu = $this->service->getMenuList($request->user['is_super'],$hide,$param['form']);
+            $permissionsData = [];
+            foreach ($menu as $val) {
+                if ($val['type'] == 'button') {
+                    $permissionsData[] = $val['name'];
+                }
+            }
+            $menu = $this->filterMenu(DataExtend::arr2tree($menu),$param['type']);
+            if($param['type'] == 1) {
+                $permissions = $permissionsData;
+                $dashboardGrid = [];
+                return successTrans("success.data",compact('menu','permissions','dashboardGrid'));
+            }
+            return successTrans("success.data",$menu);
+        } catch (\Throwable $throwable) {
+            echo $throwable->getFile()."\n";
+            echo $throwable->getLine()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 更新
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "save",methods: "post")]
+    public function saveMenuData(Request $request): Response
+    {
+        try {
+            $state = (new SystemMenu)->setAutoData($request->post());
+            if(!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+}

+ 141 - 0
app/extra/basic/Base.php

@@ -0,0 +1,141 @@
+<?php
+
+namespace app\extra\basic;
+
+use app\extra\tools\CodeExtend;
+use app\model\system\SystemUser;
+use Overtrue\EasySms\Strategies\OrderStrategy;
+use think\Validate;
+
+class Base
+{
+
+    protected function getParent(array $data)
+    {
+        if ($data['parent_id'] > 0) {
+            return $data['parent_id'];
+        }
+        return $data['user_id'];
+    }
+
+    public function getShopId()
+    {
+        return request()->header("shop",0);
+    }
+
+    public function getAccountId(array $data)
+    {
+        return $data['account_id'];
+    }
+
+
+    /**
+     * 写入新用户
+     * @param array $param
+     * @return bool
+     */
+    protected function sceneUser(array $param = [],int $type = 1,$field = "agent_id"): bool
+    {
+        if (!isset($param['id']))
+        {
+            $param['salt'] = strtoupper(CodeExtend::random(10,3));
+            $param['password'] = md5($param['password'].$param['salt']);
+            $param['create_ip'] = request()->getRealIp() ?: '127.0.0.1';
+            $param['type'] = $type;
+        }
+        return (new SystemUser)->setAutoData($param,$field);
+    }
+
+    protected function getSmsChannel(): array
+    {
+        return [
+            [
+                "name"  => "阿里云",
+                "type"  => "aliyun",
+                "url"   => "https://dysms.console.aliyun.com/dysms.htm"
+            ],
+            [
+                "name"  => "腾讯云",
+                "type"  => "qcloud",
+                "url"   => "https://console.cloud.tencent.com/smsv2"
+            ],
+            [
+                "name"  => "七牛云",
+                "type"  => "qiniu",
+                "url"   => "https://portal.qiniu.com/sms/dashboard"
+            ]
+        ];
+    }
+
+    /**
+     *  快捷输入并验证( 支持 规则 # 别名 )
+     * @param array $rules 验证规则( 验证信息数组 )
+     * @param array|string $input 输入方式 ( post. 或 get. )
+     * @param callable|null $callable 异常处理操作
+     */
+    protected function _valid(array $rules, array|string $input = '', ?callable $callable = null)
+    {
+        if (is_string($input)) {
+            $type = trim($input, '.') ?: 'get';
+            $input = request()->$type();
+        }
+        [$data, $rule, $info] = [[], [], []];
+        foreach ($rules as $name => $message) if (is_numeric($name)) {
+            [$name, $alias] = explode('#', $message . '#');
+            $data[$name] = $input[($alias ?: $name)] ?? null;
+        } elseif (!str_contains($name, '.')) {
+            $data[$name] = $message;
+        } elseif (preg_match('|^(.*?)\.(.*?)#(.*?)#?$|', $name . '#', $matches)) {
+            [, $_key, $_rule, $alias] = $matches;
+            if (in_array($_rule, ['value', 'default'])) {
+                if ($_rule === 'value') $data[$_key] = $message;
+                elseif ($_rule === 'default') $data[$_key] = $input[($alias ?: $_key)] ?? $message;
+            } else {
+                $info[explode(':', $name)[0]] = $message;
+                $data[$_key] = $data[$_key] ?? ($input[($alias ?: $_key)] ?? null);
+                $rule[$_key] = isset($rule[$_key]) ? ($rule[$_key] . '|' . $_rule) : $_rule;
+            }
+        }
+        $validate = new Validate();
+        if ($validate->rule($rule)->message($info)->check($data)) {
+            return $data;
+        } elseif (is_callable($callable)) {
+            return call_user_func($callable, $validate->getError(), $data);
+        } else {
+            return $validate->getError();
+        }
+    }
+
+
+    /**
+     * 菜单信息格式化
+     * @param array $menus
+     * @param int $type
+     * @param int $mch
+     * @return array
+     */
+    protected function filterMenu(array $menus,int $type,int $mch = 0): array
+    {
+        foreach ($menus as &$menu) {
+            $menu['meta'] = [
+                "title"     => $menu['title'],
+                "icon"      => $menu['icon']??'',
+                "type"      => $menu['type']
+            ];
+            if (!empty($menu['children'])) {
+                $menu['children'] = $this->filterMenu($menu['children'],$type,$mch);
+            }
+            if ($mch > 0) {
+                $menu['component'] = ($mch==1?'merchant/':'store/').$menu['name'];
+            } else {
+                $menu['component'] = $menu['name'];
+            }
+            $menu['isMenu'] = $menu['status'];
+            if($type == 1) unset($menu['title'],$menu['icon'],$menu['type'],$menu['pid'],$menu['id']);
+        }
+        return $menus;
+    }
+
+
+
+}

+ 30 - 0
app/extra/basic/Model.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace app\extra\basic;
+
+class Model extends \think\Model
+{
+
+
+
+    /**
+     * 数据操作
+     * @param array $data
+     * @param string $id
+     * @return bool|int
+     */
+    public function setAutoData(array $data = [],string $id = "id"): bool|int
+    {
+        try {
+            if (isset($data[$id]) && $this->where($id,$data[$id])->count()) {
+                $state = $this->where($id,$data[$id])->strict(false)->update($data);
+            } else {
+                $state = $this->strict(false)->insertGetId($data);
+            }
+        } catch (\Throwable $throwable) {
+            return false;
+        }
+        return $state;
+    }
+
+}

+ 103 - 0
app/extra/basic/Service.php

@@ -0,0 +1,103 @@
+<?php
+
+namespace app\extra\basic;
+
+use app\model\system\SystemConfig;
+use Overtrue\EasySms\Strategies\OrderStrategy;
+
+class Service
+{
+
+    /**
+     * @var null
+     */
+    protected $mode = null;
+
+
+    /**
+     * 白名单
+     * @var array|string[]
+     */
+    protected array $mobileWhite = ["18665619195","18665619196","18665619197","18665619198","18665619199","18665619191","18665619192","18665619193","18665619194"];
+
+
+    /**
+     * 短信配置
+     * @param int $type 1 系统配置 2 代理配置
+     * @return array
+     */
+    protected function smsConfig(int $type = 1): array
+    {
+        $sms = (new SystemConfig)->where("type","sms")->column("value","name");
+        return [
+            "config" => [
+                'enable' => true,
+                'timeout' => 5.0,
+                "default"   => [
+                    "strategy"  => OrderStrategy::class,
+                    "gateways"  => [$sms['sms_type']]
+                ],
+                "gateways"  => [
+                    'errorlog' => [
+                        'file' => runtime_path().'/tmp/easy-sms.log',
+                    ],
+                    'aliyun' => [
+                        'access_key_id' => $sms['AccessKeyId'],
+                        'access_key_secret' => $sms['AccessKeySecret'],
+                        'sign_name' => trim($sms['sign']),
+                    ],
+                ]
+            ],
+            "template"  => $sms['login']
+        ];
+    }
+    /**
+     * 默认排序筛选
+     * @param array $param
+     * @param string $prefix
+     * @param array $filter
+     * @return string[]
+     */
+    public function defaultSort(array $param = [],string $prefix = "",array $filter = []): array
+    {
+        if (!empty($prefix)) $prefix = $prefix.".";
+        if (isset($param['order']) && $param['order'] == 'descending'){
+            $orderBy = ["{$prefix}{$param['field']}" => "desc"];
+        } else if (isset($param['order']) && $param['order'] == 'ascending'){
+            $orderBy = ["{$prefix}{$param['field']}" => "asc"];
+        } else {
+            $orderBy = ["{$prefix}create_at" => "desc"];
+        }
+        return $orderBy;
+    }
+
+
+    /**
+     * @param array $param 参数
+     * @param array $filter 过滤器
+     * @param string $prefix 链表前缀
+     * @param string $join 链表表名
+     * @param string $field 字段
+     * @param string $forKey 链表首字母
+     * @param string $localKey 被链表首字母
+     */
+    protected function searchVal(array $param = [],array $filter = [],string $prefix = "",string $join = "",string $field = "",string $forKey = "",string $localKey = "")
+    {
+        $orderBy = $this->defaultSort($param,$prefix);
+        $commonFilter = [];
+        // 起止时间
+        if (!empty($param['create'])) {
+            $times = between_time($param['create']);
+            $start = date('Y-m-d',$times['start_time']);
+            $end = date('Y-m-d',($times['end_time'] + 86400));
+            $commonFilter[] = ['create_at', '>=', $start ];
+            $commonFilter[] = ['create_at', '<', $end ];
+        }
+        $filter = array_merge($filter,$commonFilter);
+        if (!empty($join)) {
+            $this->mode =  $this->mode->alias('a')->join("{$join} {$prefix}","a.{$forKey} = {$prefix}.{$localKey}")->field($field);
+        }
+        return $this->mode->order($orderBy)->where($filter);
+    }
+
+}

+ 27 - 0
app/extra/service/system/MenuService.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace app\extra\service\system;
+
+use app\extra\basic\Service;
+use app\model\system\SystemMenu;
+
+class MenuService extends Service
+{
+
+    /**
+     * @param int $super
+     * @param int $type
+     * @return array
+     */
+    public function getMenuList(int $super = 0,int $hide = 0,int $type = 1): array
+    {
+        $model = new SystemMenu();
+        try {
+            $data = $model->where("status",1)->where("from",$type)->order("sort","asc")->select();
+        } catch (\Throwable $throwable) {
+            return [];
+        }
+        return $data->isEmpty()?[]:$data->toArray();
+    }
+
+}

+ 183 - 0
app/extra/service/system/SystemService.php

@@ -0,0 +1,183 @@
+<?php
+
+namespace app\extra\service\system;
+
+use app\extra\basic\Service;
+use app\model\system\SystemConfig;
+use app\model\system\SystemData;
+use app\model\system\SystemOplog;
+use support\think\Cache;
+
+class SystemService extends Service
+{
+
+    /**
+     * 配置缓存数据
+     * @var array
+     */
+    private static array $config = [];
+
+
+    public static function get(string $name = '', string $default = '')
+    {
+        try {
+            $cacheConfig = Cache::get("SystemConfig");
+            if (empty($cacheConfig))
+            {
+                $config = (new SystemConfig)->select();
+                self::setConfig($config);
+                Cache::set("SystemConfig",json_encode($config));
+            } else {
+                $config = json_decode($cacheConfig,true);
+                self::setConfig($config);
+            }
+
+            [$type, $field, $outer] = static::_parse($name);
+            if (empty($name)) {
+                return static::$config;
+            } elseif (isset(static::$config[$type])) {
+                $group = static::$config[$type];
+                if ($outer !== 'raw') foreach ($group as $kk => $vo) {
+                    $group[$kk] = htmlspecialchars(strval($vo));
+                }
+                return $field ? ($group[$field] ?? $default) : $group;
+            } else {
+                return $default;
+            }
+
+        } catch (\Exception $exception)
+        {
+            return false;
+        }
+
+    }
+
+    protected static function setConfig($data)
+    {
+        foreach($data as $item)
+        {
+            static::$config[$item['type']][$item['name']] = $item['value'];
+        }
+    }
+
+    public static function set(string $name, $value = '')
+    {
+        static::$config = [];
+        [$type, $field] = static::_parse($name);
+        if (is_array($value)) {
+            $count = 0;
+            foreach ($value as $kk => $vv) {
+                $count += static::set("{$field}.{$kk}", $vv);
+            }
+            return $count;
+        } else try {
+            Cache::delete("SystemConfig");
+            $map = ['type' => $type, 'name' => $field];
+            $data = array_merge($map, ['value' => $value]);
+            $query = (new SystemConfig)->where($map);
+            return (clone $query)->count() > 0 ? $query->update($data) : $query->insert($data);
+        } catch (\Throwable $exception) {
+            return false;
+        }
+    }
+
+    /**
+     * 解析缓存名称
+     * @param string $rule 配置名称
+     * @return array
+     */
+    private static function _parse(string $rule): array
+    {
+        $type = 'base';
+        if (stripos($rule, '.') !== false) {
+            [$type, $rule] = explode('.', $rule, 2);
+        }
+        [$field, $outer] = explode('|', "{$rule}|");
+        return [$type, $field, strtolower($outer)];
+    }
+
+
+    /**
+     * 读取数据内容
+     * @param string $name
+     * @param array $default
+     * @return array|mixed
+     */
+    public static function getData(string $name, array $default = [])
+    {
+        try {
+            $value = (new SystemData)->where("name",$name)->value("content");
+            if (!empty($value)) return json_decode($value,true);
+            return $default;
+        } catch (\Throwable $exception)
+        {
+            return $default;
+        }
+    }
+
+    /**
+     * 保存数据内容
+     * @param string $name 数据名称
+     * @param mixed $value 数据内容
+     * @return bool
+     */
+    public static function setData(string $name, $value)
+    {
+        try {
+            $data = ['name' => $name, 'content' => json_encode($value, 64 | 256)];
+            return (new SystemData)->setAutoData($data,"name");
+        } catch (\Throwable $exception) {
+            echo $exception->getFile()."\n";
+            echo $exception->getLine()."\n";
+            echo $exception->getMessage()."\n";
+            return false;
+        }
+    }
+
+
+    /**
+     * 写入系统日志内容
+     * @param string $action
+     * @param string $content
+     * @param string $userName
+     * @return boolean
+     */
+    public static function setOplog(string $action, string $content, string $userName): bool
+    {
+        return (new SystemOplog)->insert(static::getOplog($action, $content,$userName)) !== false;
+    }
+
+    /**
+     * 获取系统日志内容
+     * @param string $action
+     * @param string $content
+     * @param string $userName
+     * @return array
+     */
+    public static function getUserOplog(string $action, string $content, string $userName): array
+    {
+        return [
+            'username'  => $userName,
+            'action'    => $action, 'content' => $content,
+            'geoip'     => request()->getRealIp() ?: '127.0.0.1',
+        ];
+    }
+
+    /**
+     * 获取系统日志内容
+     * @param string $action
+     * @param string $content
+     * @param string $userName
+     * @return array
+     */
+    public static function getOplog(string $action, string $content, string $userName): array
+    {
+        return [
+            'node'      => request()->uri(),
+            'action'    => $action, 'content' => $content,
+            'geoip'     => request()->getRealIp() ?: '127.0.0.1',
+            'username'  => $userName ?: '-'
+        ];
+    }
+
+}

+ 38 - 0
app/extra/service/system/UserService.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace app\extra\service\system;
+
+use app\extra\basic\Service;
+use app\model\system\SystemUser;
+
+class UserService extends Service
+{
+
+    /**
+     * 列表
+     * @param array $param
+     */
+    public function getList(array $param = [])
+    {
+        $this->mode = new SystemUser();
+        return $this->searchVal($param,$this->searchFilter($param))->paginate([
+            "list_rows" => $param['pageSize'],
+            "page"      => $param['page']
+        ]);
+    }
+
+
+
+    protected function searchFilter(array $param = []): array
+    {
+        $filter = [];
+        !empty($param['agent']) && $filter[] = ["agent_id", '=', $param['agent']];
+        !empty($param['store']) && $filter[] = ["store_id", '=', $param['store']];
+        !empty($param['factory']) && $filter[] = ["factory_id", '=', $param['factory']];
+        !empty($param['status']) && $filter[] = ["status", '=', ($param['status']-1)];
+        !empty($param['mobile']) && $filter[] = ["mobile", 'like', "%{$param['mobile']}%"];
+        !empty($param['username']) && $filter[] = ["username", 'like', "%{$param['username']}%"];
+        return $filter;
+    }
+
+}

+ 135 - 0
app/extra/tools/CodeExtend.php

@@ -0,0 +1,135 @@
+<?php
+
+namespace app\extra\tools;
+
+class CodeExtend
+{
+    /**
+     * 生成随机编码
+     * @param integer $size 编码长度
+     * @param integer $type 编码类型(1纯数字,2纯字母,3数字字母)
+     * @param string $prefix 编码前缀
+     * @return string
+     */
+    public static function random(int $size = 10, int $type = 1, string $prefix = ''): string
+    {
+        $numbs = '0123456789';
+        $chars = 'abcdefghijklmnopqrstuvwxyz';
+        if ($type === 1) $chars = $numbs;
+        if ($type === 3) $chars = "{$numbs}{$chars}";
+        $code = $prefix . $chars[rand(1, strlen($chars) - 1)];
+        while (strlen($code) < $size) $code .= $chars[rand(0, strlen($chars) - 1)];
+        return $code;
+    }
+
+    /**
+     * 生成日期编码
+     * @param integer $size 编码长度
+     * @param string $prefix 编码前缀
+     * @return string
+     */
+    public static function uniqidDate(int $size = 16, string $prefix = ''): string
+    {
+        if ($size < 14) $size = 14;
+        $code = $prefix . date('Ymd') . (date('H') + date('i')) . date('s');
+        while (strlen($code) < $size) $code .= rand(0, 9);
+        return $code;
+    }
+
+    /**
+     * 生成数字编码
+     * @param integer $size 编码长度
+     * @param string $prefix 编码前缀
+     * @return string
+     */
+    public static function uniqidNumber(int $size = 12, string $prefix = ''): string
+    {
+        $time = strval(time());
+        if ($size < 10) $size = 10;
+        $code = $prefix . (intval($time[0]) + intval($time[1])) . substr($time, 2) . rand(0, 9);
+        while (strlen($code) < $size) $code .= rand(0, 9);
+        return $code;
+    }
+
+    /**
+     * 文本转码
+     * @param string $text 文本内容
+     * @param string $target 目标编码
+     * @return string
+     */
+    public static function text2utf8(string $text, string $target = 'UTF-8'): string
+    {
+        [$first2, $first4] = [substr($text, 0, 2), substr($text, 0, 4)];
+        if ($first4 === chr(0x00) . chr(0x00) . chr(0xFE) . chr(0xFF)) $ft = 'UTF-32BE';
+        elseif ($first4 === chr(0xFF) . chr(0xFE) . chr(0x00) . chr(0x00)) $ft = 'UTF-32LE';
+        elseif ($first2 === chr(0xFE) . chr(0xFF)) $ft = 'UTF-16BE';
+        elseif ($first2 === chr(0xFF) . chr(0xFE)) $ft = 'UTF-16LE';
+        return mb_convert_encoding($text, $target, $ft ?? mb_detect_encoding($text));
+    }
+
+    /**
+     * 数据解密处理
+     * @param mixed $data 加密数据
+     * @param string $skey 安全密钥
+     * @return string
+     */
+    public static function encrypt($data, string $skey): string
+    {
+        $iv = static::random(16, 3);
+        $value = openssl_encrypt(serialize($data), 'AES-256-CBC', $skey, 0, $iv);
+        return static::enSafe64(json_encode(['iv' => $iv, 'value' => $value]));
+    }
+
+    /**
+     * 数据加密处理
+     * @param string $data 解密数据
+     * @param string $skey 安全密钥
+     * @return mixed
+     */
+    public static function decrypt(string $data, string $skey)
+    {
+        $attr = json_decode(static::deSafe64($data), true);
+        return unserialize(openssl_decrypt($attr['value'], 'AES-256-CBC', $skey, 0, $attr['iv']));
+    }
+
+    /**
+     * Base64Url 安全编码
+     * @param string $text 待加密文本
+     * @return string
+     */
+    public static function enSafe64(string $text): string
+    {
+        return rtrim(strtr(base64_encode($text), '+/', '-_'), '=');
+    }
+
+    /**
+     * Base64Url 安全解码
+     * @param string $text 待解密文本
+     * @return string
+     */
+    public static function deSafe64(string $text): string
+    {
+        return base64_decode(str_pad(strtr($text, '-_', '+/'), strlen($text) % 4, '='));
+    }
+
+    /**
+     * 压缩数据对象
+     * @param mixed $data
+     * @return string
+     */
+    public static function enzip($data): string
+    {
+        return static::enSafe64(gzcompress(serialize($data)));
+    }
+
+    /**
+     * 解压数据对象
+     * @param string $string
+     * @return mixed
+     */
+    public static function dezip(string $string)
+    {
+        return unserialize(gzuncompress(static::deSafe64($string)));
+    }
+
+}

+ 73 - 0
app/extra/tools/DataExtend.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace app\extra\tools;
+
+
+class DataExtend
+{
+
+    /**
+     * 一维数组转多维数据树
+     * @param array $its 待处理数据
+     * @param string $cid 自己的主键
+     * @param string $pid 上级的主键
+     * @param string $sub 子数组名称
+     * @return array
+     */
+    public static function arr2tree(array $its, string $cid = 'id', string $pid = 'pid', string $sub = 'children'): array
+    {
+        [$tree, $its] = [[], array_column($its, null, $cid)];
+        foreach ($its as $it) isset($its[$it[$pid]]) ? $its[$it[$pid]][$sub][] = &$its[$it[$cid]] : $tree[] = &$its[$it[$cid]];
+        return $tree;
+    }
+
+    /**
+     * 一维数组转数据树表
+     * @param array $its 待处理数据
+     * @param string $cid 自己的主键
+     * @param string $pid 上级的主键
+     * @param string $path 当前 PATH
+     * @return array
+     */
+    public static function arr2table(array $its, string $cid = 'id', string $pid = 'pid', string $path = 'path'): array
+    {
+        $call = function (array $its, callable $call, array &$data = [], string $parent = '') use ($cid, $pid, $path) {
+            foreach ($its as $it) {
+                $ts = $it['sub'] ?? [];
+                unset($it['sub']);
+                $it[$path] = "{$parent}-{$it[$cid]}";
+                $it['spc'] = count($ts);
+                $it['spt'] = substr_count($parent, '-');
+                $it['spl'] = str_repeat('ㅤ├ㅤ', $it['spt']);
+                $it['sps'] = ",{$it[$cid]},";
+                array_walk_recursive($ts, function ($val, $key) use ($cid, &$it) {
+                    if ($key === $cid) $it['sps'] .= "{$val},";
+                });
+                $it['spp'] = arr2str(str2arr(strtr($parent . $it['sps'], '-', ',')));
+                $data[] = $it;
+                if (empty($ts)) continue;
+                $call($ts, $call, $data, $it[$path]);
+            }
+            return $data;
+        };
+        return $call(static::arr2tree($its, $cid, $pid), $call);
+    }
+
+    /**
+     * 获取数据树子ID集合
+     * @param array $list 数据列表
+     * @param mixed $value 起始有效ID值
+     * @param string $ckey 当前主键ID名称
+     * @param string $pkey 上级主键ID名称
+     * @return array
+     */
+    public static function getArrSubIds(array $list, $value = 0, string $ckey = 'id', string $pkey = 'pid'): array
+    {
+        $ids = [intval($value)];
+        foreach ($list as $vo) if (intval($vo[$pkey]) > 0 && intval($vo[$pkey]) === intval($value)) {
+            $ids = array_merge($ids, static::getArrSubIds($list, intval($vo[$ckey]), $ckey, $pkey));
+        }
+        return $ids;
+    }
+
+}

+ 122 - 0
app/extra/tools/UploadExtend.php

@@ -0,0 +1,122 @@
+<?php
+
+namespace app\extra\tools;
+
+use app\extra\service\basic\UploadService;
+use Tinywan\Storage\Storage;
+
+/**
+ * @see Storage
+ * @mixin Storage
+ *
+ * @method static array uploadFile(array $config = [])  上传文件
+ * @method static array uploadBase64(string $base64, string $extension = 'png') 上传Base64文件
+ * @method static array uploadServerFile(string $file_path)  上传服务端文件
+ */
+class UploadExtend
+{
+    /**
+     * 本地对象存储.
+     */
+    public const MODE_LOCAL = 'local';
+
+    /**
+     * 阿里云对象存储.
+     */
+    public const MODE_OSS = 'oss';
+
+    /**
+     * 腾讯云对象存储.
+     */
+    public const MODE_COS = 'cos';
+
+    /**
+     * 七牛云对象存储.
+     */
+    public const MODE_QINIU = 'qiniu';
+
+    /**
+     * S3对象存储.
+     */
+    public const MODE_S3 = 's3';
+
+
+    protected array $config = [];
+
+    /**
+     * Support Storage
+     */
+    static $allowStorage = [
+        self::MODE_LOCAL,
+        self::MODE_OSS,
+        self::MODE_COS,
+        self::MODE_QINIU,
+        self::MODE_S3
+    ];
+
+    /**
+     * 存储磁盘
+     * @param string|null $name
+     * @param bool $_is_file_upload
+     * @return mixed
+     * @author Tinywan(ShaoBo Wan)
+     */
+    public static function disk(string $name = null, bool $_is_file_upload = true)
+    {
+        $storage = $name ?? self::getDefaultStorage();
+        $config = self::getConfig($storage);
+        return new $config['adapter'](array_merge(
+            $config, ['_is_file_upload' => $_is_file_upload]
+        ));
+    }
+
+    /**
+     * 默认存储
+     * @return mixed
+     * @author Tinywan(ShaoBo Wan)
+     */
+    public static function getDefaultStorage()
+    {
+        return self::getConfig('default');
+    }
+
+    /**
+     * 获取存储配置
+     * @param string|null $name 名称
+     * @return mixed
+     * @author Tinywan(ShaoBo Wan)
+     */
+    public static function getConfig(string $name = null)
+    {
+        $config = (new UploadService)->setConfigVal();
+        if (!is_null($name)) {
+            return $config['storage'][$name];
+//            return config('plugin.tinywan.storage.app.storage.' . $name, self::MODE_LOCAL);
+        }
+        return $config['storage']["default"];
+//        return config('plugin.tinywan.storage.app.storage.default');
+    }
+
+
+    /**
+     * 配置信息
+     * @param array $config
+     */
+    public function config(array $config)
+    {
+        $this->$config = $config;
+        return $this;
+    }
+
+    /**
+     * @param $name
+     * @param $arguments
+     * @return mixed
+     * @author Tinywan(ShaoBo Wan)
+     */
+    public static function __callStatic($name, $arguments)
+    {
+        return static::disk()->{$name}(...$arguments);
+    }
+
+}

+ 406 - 0
app/functions.php

@@ -0,0 +1,406 @@
+<?php
+
+
+use app\extra\service\system\SystemService;
+use support\Response;
+use Webman\Event\Event;
+
+if (!function_exists("between_time"))
+{
+    /**
+     * 格式化起止时间(为了兼容前端RangePicker组件)
+     * 2020-04-01T08:15:08.891Z => 1585670400
+     * @param array $times
+     * @param bool $isWithTime 是否包含时间
+     * @return array
+     */
+    function between_time(array $times, bool $isWithTime = false): array
+    {
+        foreach ($times as &$time) {
+            $time = trim($time, '&quot;');
+            $time = str2date($time, $isWithTime);
+        }
+        return ['start_time' => current($times), 'end_time' => next($times)];
+    }
+}
+if (!function_exists("str2date"))
+{
+    /**
+     * 日期转换时间戳
+     * 例如: 2020-04-01 08:15:08 => 1585670400
+     * @param string $date
+     * @param bool $isWithTime 是否包含时间
+     * @return int
+     */
+    function str2date(string $date, bool $isWithTime = false): int
+    {
+        if (!$isWithTime) {
+            $date = date('Y-m-d', strtotime($date));
+        }
+        return strtotime($date);
+    }
+}
+
+if(!function_exists("hide_mobile")){
+
+    /**
+     * 手机号码脱敏
+     * @param string $mobile
+     * @param int $len 4 末尾4位 6 末尾2位
+     * @return string
+     */
+    function hide_mobile(string $mobile,int $len = 6): string
+    {
+        return substr_replace($mobile, ($len==4 ? '****' : '******'), 3, $len);
+    }
+}
+
+if (!function_exists('hide_str')) {
+    /**
+     * 将一个字符串部分字符用*替代隐藏
+     * @param string $string 待转换的字符串
+     * @param int $begin 起始位置,从0开始计数,当$type=4时,表示左侧保留长度
+     * @param int $len 需要转换成*的字符个数,当$type=4时,表示右侧保留长度
+     * @param int $type 转换类型:0,从左向右隐藏;1,从右向左隐藏;2,从指定字符位置分割前由右向左隐藏;3,从指定字符位置分割后由左向右隐藏;4,保留首末指定字符串中间用***代替
+     * @param string $glue 分割符
+     * @return string   处理后的字符串
+     */
+    function hide_str(string $string, int $begin = 3, int $len = 4, int $type = 0, string $glue = "@"): string
+    {
+        $array = array();
+        if ($type == 0 || $type == 1 || $type == 4) {
+            $strlen = $length = mb_strlen($string);
+            while ($strlen) {
+                $array[] = mb_substr($string, 0, 1, "utf8");
+                $string = mb_substr($string, 1, $strlen, "utf8");
+                $strlen = mb_strlen($string);
+            }
+        }
+        if ($type == 0) {
+            for ($i = $begin; $i < ($begin + $len); $i++) {
+                if (isset($array[$i])) {
+                    $array[$i] = "*";
+                }
+            }
+            $string = implode("", $array);
+        } elseif ($type == 1) {
+            $array = array_reverse($array);
+            for ($i = $begin; $i < ($begin + $len); $i++) {
+                if (isset($array[$i])) {
+                    $array[$i] = "*";
+                }
+            }
+            $string = implode("", array_reverse($array));
+        } elseif ($type == 2) {
+            $array = explode($glue, $string);
+            if (isset($array[0])) {
+                $array[0] = hide_str($array[0], $begin, $len, 1);
+            }
+            $string = implode($glue, $array);
+        } elseif ($type == 3) {
+            $array = explode($glue, $string);
+            if (isset($array[1])) {
+                $array[1] = hide_str($array[1], $begin, $len, 0);
+            }
+            $string = implode($glue, $array);
+        } elseif ($type == 4) {
+            $left = $begin;
+            $right = $len;
+            $tem = array();
+            for ($i = 0; $i < ($length - $right); $i++) {
+                if (isset($array[$i])) {
+                    $tem[] = $i >= $left ? "" : $array[$i];
+                }
+            }
+            $tem[] = '*****';
+            $array = array_chunk(array_reverse($array), $right);
+            $array = array_reverse($array[0]);
+            for ($i = 0; $i < $right; $i++) {
+                if (isset($array[$i])) {
+                    $tem[] = $array[$i];
+                }
+            }
+            $string = implode("", $tem);
+        }
+        return $string;
+    }
+}
+
+if (!function_exists('list_sort_by')) {
+    /**
+     *----------------------------------------------------------
+     * 对查询结果集进行排序
+     *----------------------------------------------------------
+     * @access public
+     *----------------------------------------------------------
+     * @param array $list 查询结果
+     * @param string $field 排序的字段名
+     * @param string $sortBy 排序类型
+     * @switch string  asc正向排序 desc逆向排序 nat自然排序
+     *----------------------------------------------------------
+     * @return array
+     *----------------------------------------------------------
+     */
+    function list_sort_by(array $list, string $field, string $sortBy = 'asc'): array
+    {
+        if (!empty($list)) {
+            $refer = $resultSet = array();
+            foreach ($list as $i => $data)
+                $refer[$i] = &$data[$field];
+            switch ($sortBy) {
+                case 'asc': // 正向排序
+                    asort($refer);
+                    break;
+                case 'desc':// 逆向排序
+                    arsort($refer);
+                    break;
+                case 'nat': // 自然排序
+                    natcasesort($refer);
+                    break;
+            }
+            foreach ($refer as $key => $val)
+                $resultSet[] = &$list[$key];
+            return $resultSet;
+        }
+        return [];
+    }
+}
+
+
+if (!function_exists('supplement_id')) {
+    /**
+     * 用户ID风格
+     * @param string $id
+     * @return string
+     */
+    function supplement_id(string $id): string
+    {
+        $len = strlen($id);
+        $buf = '000000';
+        return $len < 6 ? substr($buf, 0, (6 - $len)) . $id : $id;
+    }
+}
+if (!function_exists('createOrderId')) {
+    /**
+     * 生成订单号
+     * @param string $letter
+     * @param int $length
+     * @return string
+     */
+    function createOrderId(string $letter = '', int $length = 3): string
+    {
+        $gradual = 0;
+        $orderId = date('YmdHis') . mt_rand(10000000, 99999999);
+        $lengths = strlen($orderId);
+
+        // 循环处理随机数
+        for ($i = 0; $i < $lengths; $i++) {
+            $gradual += (int)(substr($orderId, $i, 1));
+        }
+
+        if (empty($letter)) {
+            $letter = get_order_letter($length);
+        }
+
+        $code = (100 - $gradual % 100) % 100;
+        return $letter . $orderId . str_pad((string)$code, 2, '0', STR_PAD_LEFT);
+    }
+}
+
+if (!function_exists('get_order_letter')) {
+    /**
+     * 生成订单短ID
+     * @param int $length
+     * @return string
+     */
+    function get_order_letter(int $length = 2): string
+    {
+        $letter_all = range('A', 'Z');
+        shuffle($letter_all);
+        $letter_array = array_diff($letter_all, ['I', 'O']);
+        $letter = array_rand(array_flip($letter_array), $length);
+        return implode('', $letter);
+    }
+}
+
+if (!function_exists('object_array')) {
+    /**
+     * @param $array
+     * @return array
+     */
+    function object_array($array): array
+    {
+        if(is_object($array)) {
+            $array = (array)$array;
+        }
+        if(is_array($array)) {
+            foreach($array as $key=>$value) {
+                $array[$key] = object_array($value);
+            }
+        }
+        return $array;
+    }
+}
+
+if (!function_exists("getDateFull"))
+{
+    /**
+     * @param string $format
+     * @param string $time
+     * @return string
+     */
+    function getDateFull(string $format = "Y-m-d H:i:s",string $time = ""): string
+    {
+        if (empty($time)) $time = time();
+        return date($format,$time);
+    }
+}
+
+if (!function_exists("events"))
+{
+    /**
+     * 事件发布
+     * @param $name
+     * @param $data
+     * @return array|mixed|null
+     */
+    function events($name,$data): mixed
+    {
+        return Event::dispatch($name,$data);
+    }
+}
+
+if (!function_exists("success")) {
+    /**
+     * @param string $msg
+     * @param array $data
+     * @param int $code
+     * @return Response
+     */
+    function success(string $msg = "", array $data = [], int $code = 1): Response
+    {
+        return json(compact('code', 'data', 'msg'));
+    }
+}
+
+if (!function_exists("successTrans")) {
+    /**
+     * 消息返回
+     * @param string $message
+     * @param array $data
+     * @param int $code
+     * @return Response
+     */
+    function successTrans(string $message, array $data = [], int $code = 1): Response
+    {
+        $msg = trans($message);
+        return json(compact("msg", "code", "data"));
+    }
+}
+
+if (!function_exists("error")) {
+    /**
+     * @param $msg
+     * @param array $data
+     * @param int $code
+     * @return Response
+     */
+    function error($msg, array $data = [], int $code = 0): Response
+    {
+        return json(compact('code', 'data', 'msg'));
+    }
+}
+
+if (!function_exists("errorTrans")) {
+    /**
+     * 消息返回
+     * @param string $message
+     * @param array $data
+     * @param int $code
+     * @return Response
+     */
+    function errorTrans(string $message = "", array $data = [], int $code = 0): Response
+    {
+        $msg = trans($message);
+        return json(compact("msg", "code", "data"));
+    }
+}
+
+if (!function_exists("pageFormat")) {
+    /**
+     * @param $data
+     * @param int $size
+     * @return array {page:"",pageSize:"",rows:[],total:""}
+     */
+    function pageFormat($data, int $size = 10): array
+    {
+        if (empty($data)) return [];
+        return [
+            'total'     => $data->total(),
+            'page'      => $data->currentPage(),
+            'pageSize'  => $size,
+            'rows'      => $data->items()
+        ];
+    }
+}
+
+
+if(!function_exists('format_money')){
+    function format_money($str,$len = '2',$append = ""): string
+    {
+        if (empty($str)) {
+            return "0.00";
+        }
+        return number_format($str, $len, ".", $append);
+    }
+}
+
+
+if (!function_exists('sConf')) {
+
+    /**
+     * 获取或配置系统参数
+     * @param string $name 参数名称
+     * @param string|null $value 参数内容
+     */
+    function sConf(string $name = '', string $value = null)
+    {
+        if (is_null($value) && is_string($name)) {
+            return SystemService::get($name);
+        } else {
+            return SystemService::set($name, $value);
+        }
+    }
+}
+
+if (!function_exists('sData')) {
+
+    /**
+     * JSON 数据读取与存储
+     * @param string $name 数据名称
+     * @param mixed $value 数据内容
+     */
+    function sData(string $name,mixed $value = null)
+    {
+        if (is_null($value)) {
+            return SystemService::getData($name);
+        } else {
+            return SystemService::setData($name, $value);
+        }
+    }
+}
+
+
+if (!function_exists('sOplog')) {
+    /**
+     * 写入系统日志
+     * @param string $action 日志行为
+     * @param string $content 日志内容
+     * @param string $userName
+     * @return boolean
+     */
+    function sOplog(string $action, string $content, string $userName): bool
+    {
+        return SystemService::setOplog($action, $content,$userName);
+    }
+}

+ 45 - 0
app/middleware/AuthMiddleware.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace app\middleware;
+
+use Shopwwi\WebmanAuth\Auth;
+use Shopwwi\WebmanAuth\JWT;
+use Webman\Http\Request;
+use Webman\Http\Response;
+use Webman\MiddlewareInterface;
+
+class AuthMiddleware implements MiddlewareInterface
+{
+
+
+    public function process(Request $request, callable $handler): Response
+    {
+        try {
+            $controller = new \ReflectionClass($request->controller);
+            $noNeedLogin = $controller->getDefaultProperties()['noNeedLogin']??[];
+            if (empty($noNeedLogin) || !in_array($request->action, $noNeedLogin)) {
+                $type = $request->header('api-type','');
+                if (empty($type)) return json(['code'=> 0,'msg'=> trans("error.param")]);
+                $token =  $request->header("Authorization","");
+                if (empty($token)) return json(['code'=> 0,'msg'=> trans("error.request")]);
+                (new JWT)->guard("admin")->verify();
+                $user = (new Auth)->guard("admin")->user();
+                if (empty($user)) return json(['code'=>401,'msg'=> trans("error.login")]);
+                $request->user = $user->toArray();
+            }
+        } catch (\ReflectionException $exception) {
+            return json(['code'=> 500,'msg'=> $exception->getMessage()]);
+        }
+        $response = $request->method() == 'OPTIONS' ? response('',204) : $handler($request);
+        // 给响应添加跨域相关的http头
+        $response->withHeaders([
+            'Access-Control-Allow-Credentials' => 'true',
+            'Access-Control-Allow-Origin' => $request->header('origin', '*'),
+            'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'),
+            'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'),
+        ]);
+        return $response;
+    }
+
+
+}

+ 42 - 0
app/middleware/StaticFile.php

@@ -0,0 +1,42 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+namespace app\middleware;
+
+use Webman\MiddlewareInterface;
+use Webman\Http\Response;
+use Webman\Http\Request;
+
+/**
+ * Class StaticFile
+ * @package app\middleware
+ */
+class StaticFile implements MiddlewareInterface
+{
+    public function process(Request $request, callable $handler): Response
+    {
+        // Access to files beginning with. Is prohibited
+        if (strpos($request->path(), '/.') !== false) {
+            return response('<h1>403 forbidden</h1>', 403);
+        }
+        /** @var Response $response */
+        $response = $handler($request);
+        // Add cross domain HTTP header
+        /*$response->withHeaders([
+            'Access-Control-Allow-Origin'      => '*',
+            'Access-Control-Allow-Credentials' => 'true',
+        ]);*/
+        return $response;
+    }
+}

+ 51 - 0
app/model/saas/SaasAgent.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property integer $user_id 唯一ID
+ * @property string $name 名称
+ * @property string $contact_name 
+ * @property string $contact_mobile 
+ * @property string $code 
+ * @property string $vip_end 到期时间
+ * @property integer $status 
+ * @property string $is_deleted 
+ * @property mixed $create_at
+ */
+class SaasAgent extends Model
+{
+    /**
+     * The connection name for the model.
+     *
+     * @var string|null
+     */
+    protected $connection = 'mysql';
+    
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected string $table = "saas_agent";
+    
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected string $primaryKey = "id";
+    
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public bool $timestamps = false;
+
+
+}

+ 48 - 0
app/model/system/SystemConfig.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace app\model\system;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property string $type 配置分类
+ * @property string $name 配置名称
+ * @property string $value 配置内容
+ * @property string $desc 字段名称
+ * @property integer $status 
+ * @property mixed $update_at 更新时间
+ */
+class SystemConfig extends Model
+{
+    /**
+     * The connection name for the model.
+     *
+     * @var string|null
+     */
+    protected $connection = 'mysql';
+    
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected string $table = "system_config";
+    
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected string $primaryKey = "id";
+    
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public bool $timestamps = false;
+
+
+}

+ 45 - 0
app/model/system/SystemData.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace app\model\system;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $name 
+ * @property mixed $content 
+ * @property integer $is_default 是否使用
+ */
+class SystemData extends Model
+{
+    /**
+     * The connection name for the model.
+     *
+     * @var string|null
+     */
+    protected $connection = 'mysql';
+    
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected string $table = "system_data";
+    
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected string $primaryKey = "id";
+    
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public bool $timestamps = false;
+
+
+}

+ 49 - 0
app/model/system/SystemExport.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace app\model\system;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property integer $user_id 
+ * @property integer $uuid 
+ * @property string $type 
+ * @property string $name 
+ * @property string $down_url 下载地址
+ * @property integer $status 0进行中1已完成
+ * @property mixed $create_at
+ */
+class SystemExport extends Model
+{
+    /**
+     * The connection name for the model.
+     *
+     * @var string|null
+     */
+    protected $connection = 'mysql';
+    
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected string $table = "system_export";
+    
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected string $primaryKey = "id";
+    
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public bool $timestamps = false;
+
+
+}

+ 54 - 0
app/model/system/SystemMenu.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace app\model\system;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property integer $from 1管理后台2代理
+ * @property integer $pid 
+ * @property string $name 
+ * @property string $path 
+ * @property string $title 
+ * @property string $type 
+ * @property string $descs 
+ * @property string $icon 
+ * @property integer $status 
+ * @property integer $sort 
+ * @property integer $is_used 
+ * @property integer $is_mch 1代理2门店
+ */
+class SystemMenu extends Model
+{
+    /**
+     * The connection name for the model.
+     *
+     * @var string|null
+     */
+    protected $connection = 'mysql';
+    
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected string $table = "system_menu";
+    
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected string $primaryKey = "id";
+    
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public bool $timestamps = false;
+
+
+}

+ 48 - 0
app/model/system/SystemOplog.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace app\model\system;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property string $username 
+ * @property string $node 当前操作节点
+ * @property string $action 操作行为名称
+ * @property string $content 当前操作节点
+ * @property mixed $geoip 
+ * @property mixed $create_at
+ */
+class SystemOplog extends Model
+{
+    /**
+     * The connection name for the model.
+     *
+     * @var string|null
+     */
+    protected $connection = 'mysql';
+    
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected string $table = "system_oplog";
+    
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected string $primaryKey = "id";
+    
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public bool $timestamps = false;
+
+
+}

+ 64 - 0
app/model/system/SystemUser.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace app\model\system;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property integer $user_id 
+ * @property integer $role_id 权限ID
+ * @property string $role_path 
+ * @property integer $parent_id 代理ID
+ * @property integer $account_id 店铺id
+ * @property string $username 用户名
+ * @property string $truename 真实姓名
+ * @property string $password 密码
+ * @property mixed $salt 密钥串
+ * @property integer $status 状态
+ * @property string $contact_name 
+ * @property string $contact_mobile 
+ * @property integer $type 1管理员2代理子账号3店铺账号
+ * @property integer $is_deleted 删除状态
+ * @property integer $is_super 
+ * @property string $remark 备注
+ * @property string $login_ip 登录IP
+ * @property mixed $login_at 登录时间
+ * @property integer $login_num 
+ * @property string $create_ip 
+ * @property mixed $updated_at 更新时间
+ * @property mixed $create_at 创建时间
+ */
+class SystemUser extends Model
+{
+    /**
+     * The connection name for the model.
+     *
+     * @var string|null
+     */
+    protected $connection = 'mysql';
+    
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected string $table = "system_user";
+    
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected string $primaryKey = "id";
+    
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public bool $timestamps = false;
+
+
+}

+ 10 - 0
app/process/Http.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace app\process;
+
+use Webman\App;
+
+class Http extends App
+{
+
+}

+ 305 - 0
app/process/Monitor.php

@@ -0,0 +1,305 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+namespace app\process;
+
+use FilesystemIterator;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use SplFileInfo;
+use Workerman\Timer;
+use Workerman\Worker;
+
+/**
+ * Class FileMonitor
+ * @package process
+ */
+class Monitor
+{
+    /**
+     * @var array
+     */
+    protected array $paths = [];
+
+    /**
+     * @var array
+     */
+    protected array $extensions = [];
+
+    /**
+     * @var array
+     */
+    protected array $loadedFiles = [];
+
+    /**
+     * @var int
+     */
+    protected int $ppid = 0;
+
+    /**
+     * Pause monitor
+     * @return void
+     */
+    public static function pause(): void
+    {
+        file_put_contents(static::lockFile(), time());
+    }
+
+    /**
+     * Resume monitor
+     * @return void
+     */
+    public static function resume(): void
+    {
+        clearstatcache();
+        if (is_file(static::lockFile())) {
+            unlink(static::lockFile());
+        }
+    }
+
+    /**
+     * Whether monitor is paused
+     * @return bool
+     */
+    public static function isPaused(): bool
+    {
+        clearstatcache();
+        return file_exists(static::lockFile());
+    }
+
+    /**
+     * Lock file
+     * @return string
+     */
+    protected static function lockFile(): string
+    {
+        return runtime_path('monitor.lock');
+    }
+
+    /**
+     * FileMonitor constructor.
+     * @param $monitorDir
+     * @param $monitorExtensions
+     * @param array $options
+     */
+    public function __construct($monitorDir, $monitorExtensions, array $options = [])
+    {
+        $this->ppid = function_exists('posix_getppid') ? posix_getppid() : 0;
+        static::resume();
+        $this->paths = (array)$monitorDir;
+        $this->extensions = $monitorExtensions;
+        foreach (get_included_files() as $index => $file) {
+            $this->loadedFiles[$file] = $index;
+            if (strpos($file, 'webman-framework/src/support/App.php')) {
+                break;
+            }
+        }
+        if (!Worker::getAllWorkers()) {
+            return;
+        }
+        $disableFunctions = explode(',', ini_get('disable_functions'));
+        if (in_array('exec', $disableFunctions, true)) {
+            echo "\nMonitor file change turned off because exec() has been disabled by disable_functions setting in " . PHP_CONFIG_FILE_PATH . "/php.ini\n";
+        } else {
+            if ($options['enable_file_monitor'] ?? true) {
+                Timer::add(1, function () {
+                    $this->checkAllFilesChange();
+                });
+            }
+        }
+
+        $memoryLimit = $this->getMemoryLimit($options['memory_limit'] ?? null);
+        if ($memoryLimit && ($options['enable_memory_monitor'] ?? true)) {
+            Timer::add(60, [$this, 'checkMemory'], [$memoryLimit]);
+        }
+    }
+
+    /**
+     * @param $monitorDir
+     * @return bool
+     */
+    public function checkFilesChange($monitorDir): bool
+    {
+        static $lastMtime, $tooManyFilesCheck;
+        if (!$lastMtime) {
+            $lastMtime = time();
+        }
+        clearstatcache();
+        if (!is_dir($monitorDir)) {
+            if (!is_file($monitorDir)) {
+                return false;
+            }
+            $iterator = [new SplFileInfo($monitorDir)];
+        } else {
+            // recursive traversal directory
+            $dirIterator = new RecursiveDirectoryIterator($monitorDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS);
+            $iterator = new RecursiveIteratorIterator($dirIterator);
+        }
+        $count = 0;
+        foreach ($iterator as $file) {
+            $count ++;
+            /** var SplFileInfo $file */
+            if (is_dir($file->getRealPath())) {
+                continue;
+            }
+            // check mtime
+            if (in_array($file->getExtension(), $this->extensions, true) && $lastMtime < $file->getMTime()) {
+                $lastMtime = $file->getMTime();
+                if (DIRECTORY_SEPARATOR === '/' && isset($this->loadedFiles[$file->getRealPath()])) {
+                    echo "$file updated but cannot be reloaded because only auto-loaded files support reload.\n";
+                    continue;
+                }
+                $var = 0;
+                exec('"'.PHP_BINARY . '" -l ' . $file, $out, $var);
+                if ($var) {
+                    continue;
+                }
+                // send SIGUSR1 signal to master process for reload
+                if (DIRECTORY_SEPARATOR === '/') {
+                    if ($masterPid = $this->getMasterPid()) {
+                        echo $file . " updated and reload\n";
+                        posix_kill($masterPid, SIGUSR1);
+                    } else {
+                        echo "Master process has gone away and can not reload\n";
+                    }
+                    return true;
+                }
+                echo $file . " updated and reload\n";
+                return true;
+            }
+        }
+        if (!$tooManyFilesCheck && $count > 1000) {
+            echo "Monitor: There are too many files ($count files) in $monitorDir which makes file monitoring very slow\n";
+            $tooManyFilesCheck = 1;
+        }
+        return false;
+    }
+
+    /**
+     * @return int
+     */
+    public function getMasterPid(): int
+    {
+        if ($this->ppid === 0) {
+            return 0;
+        }
+        if (function_exists('posix_kill') && !posix_kill($this->ppid, 0)) {
+            echo "Master process has gone away\n";
+            return $this->ppid = 0;
+        }
+        if (PHP_OS_FAMILY !== 'Linux') {
+            return $this->ppid;
+        }
+        $cmdline = "/proc/$this->ppid/cmdline";
+        if (!is_readable($cmdline) || !($content = file_get_contents($cmdline)) || (!str_contains($content, 'WorkerMan') && !str_contains($content, 'php'))) {
+            // Process not exist
+            $this->ppid = 0;
+        }
+        return $this->ppid;
+    }
+
+    /**
+     * @return bool
+     */
+    public function checkAllFilesChange(): bool
+    {
+        if (static::isPaused()) {
+            return false;
+        }
+        foreach ($this->paths as $path) {
+            if ($this->checkFilesChange($path)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @param $memoryLimit
+     * @return void
+     */
+    public function checkMemory($memoryLimit): void
+    {
+        if (static::isPaused() || $memoryLimit <= 0) {
+            return;
+        }
+        $masterPid = $this->getMasterPid();
+        if ($masterPid <= 0) {
+            echo "Master process has gone away\n";
+            return;
+        }
+
+        $childrenFile = "/proc/$masterPid/task/$masterPid/children";
+        if (!is_file($childrenFile) || !($children = file_get_contents($childrenFile))) {
+            return;
+        }
+        foreach (explode(' ', $children) as $pid) {
+            $pid = (int)$pid;
+            $statusFile = "/proc/$pid/status";
+            if (!is_file($statusFile) || !($status = file_get_contents($statusFile))) {
+                continue;
+            }
+            $mem = 0;
+            if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) {
+                $mem = $match[1];
+            }
+            $mem = (int)($mem / 1024);
+            if ($mem >= $memoryLimit) {
+                posix_kill($pid, SIGINT);
+            }
+        }
+    }
+
+    /**
+     * Get memory limit
+     * @param $memoryLimit
+     * @return int
+     */
+    protected function getMemoryLimit($memoryLimit): int
+    {
+        if ($memoryLimit === 0) {
+            return 0;
+        }
+        $usePhpIni = false;
+        if (!$memoryLimit) {
+            $memoryLimit = ini_get('memory_limit');
+            $usePhpIni = true;
+        }
+
+        if ($memoryLimit == -1) {
+            return 0;
+        }
+        $unit = strtolower($memoryLimit[strlen($memoryLimit) - 1]);
+        $memoryLimit = (int)$memoryLimit;
+        if ($unit === 'g') {
+            $memoryLimit = 1024 * $memoryLimit;
+        } else if ($unit === 'k') {
+            $memoryLimit = ($memoryLimit / 1024);
+        } else if ($unit === 'm') {
+            $memoryLimit = (int)($memoryLimit);
+        } else if ($unit === 't') {
+            $memoryLimit = (1024 * 1024 * $memoryLimit);
+        } else {
+            $memoryLimit = ($memoryLimit / (1024 * 1024));
+        }
+        if ($memoryLimit < 50) {
+            $memoryLimit = 50;
+        }
+        if ($usePhpIni) {
+            $memoryLimit = (0.8 * $memoryLimit);
+        }
+        return (int)$memoryLimit;
+    }
+
+}

+ 14 - 0
app/view/index/view.html

@@ -0,0 +1,14 @@
+<!doctype html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <link rel="shortcut icon" href="/favicon.ico"/>
+    <title>webman</title>
+
+</head>
+<body>
+hello <?=htmlspecialchars($name)?>
+</body>
+</html>

+ 82 - 0
composer.json

@@ -0,0 +1,82 @@
+{
+  "name": "workerman/webman",
+  "type": "project",
+  "keywords": [
+    "high performance",
+    "http service"
+  ],
+  "homepage": "https://www.workerman.net",
+  "license": "MIT",
+  "description": "High performance HTTP Service Framework.",
+  "authors": [
+    {
+      "name": "walkor",
+      "email": "walkor@workerman.net",
+      "homepage": "https://www.workerman.net",
+      "role": "Developer"
+    }
+  ],
+  "support": {
+    "email": "walkor@workerman.net",
+    "issues": "https://github.com/walkor/webman/issues",
+    "forum": "https://wenda.workerman.net/",
+    "wiki": "https://workerman.net/doc/webman",
+    "source": "https://github.com/walkor/webman"
+  },
+  "require": {
+    "php": ">=8.1",
+    "workerman/webman-framework": "^2.1",
+    "monolog/monolog": "^2.0",
+    "topthink/think-template": "^3.0",
+    "webman/think-orm": "^2.1",
+    "webman/think-cache": "^2.1",
+    "webman/redis": "^2.1",
+    "illuminate/events": "^12.38",
+    "webman/redis-queue": "^2.1",
+    "webman/rate-limiter": "^1.1",
+    "symfony/translation": "^7.3",
+    "intervention/image": "^3.11",
+    "webman/event": "^1.0",
+    "vlucas/phpdotenv": "^5.6",
+    "workerman/crontab": "^1.0",
+    "phpoffice/phpspreadsheet": "^5.2",
+    "webman/console": "^2.1",
+    "yzh52521/easyhttp": "^1.1",
+    "xiaosongshu/elasticsearch": "^2.0",
+    "kkokk/poster": "^3.0",
+    "linfly/annotation": "1.x",
+    "hhink/webman-sms": "^1.0",
+    "tinywan/captcha": "^0.0.4",
+    "shopwwi/webman-auth": "^2.0",
+    "tinywan/storage": "^1.1",
+    "aliyuncs/oss-sdk-php": "^2.7",
+    "qcloud/cos-sdk-v5": "^2.6",
+    "topthink/think-validate": "^3.0",
+    "hzdad/codecheck": "^1.0",
+    "php-di/php-di": "7.0"
+  },
+  "suggest": {
+    "ext-event": "For better performance. "
+  },
+  "autoload": {
+    "psr-4": {
+      "": "./",
+      "app\\": "./app",
+      "App\\": "./app",
+      "app\\View\\Components\\": "./app/view/components"
+    }
+  },
+  "scripts": {
+    "post-package-install": [
+      "support\\Plugin::install"
+    ],
+    "post-package-update": [
+      "support\\Plugin::install"
+    ],
+    "pre-package-uninstall": [
+      "support\\Plugin::uninstall"
+    ]
+  },
+  "minimum-stability": "dev",
+  "prefer-stable": true
+}

+ 26 - 0
config/app.php

@@ -0,0 +1,26 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+use support\Request;
+
+return [
+    'debug' => true,
+    'error_reporting' => E_ALL,
+    'default_timezone' => 'Asia/Shanghai',
+    'request_class' => Request::class,
+    'public_path' => base_path() . DIRECTORY_SEPARATOR . 'public',
+    'runtime_path' => base_path(false) . DIRECTORY_SEPARATOR . 'runtime',
+    'controller_suffix' => 'Controller',
+    'controller_reuse' => false,
+];

+ 21 - 0
config/autoload.php

@@ -0,0 +1,21 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+return [
+    'files' => [
+        base_path() . '/app/functions.php',
+        base_path() . '/support/Request.php',
+        base_path() . '/support/Response.php',
+    ]
+];

+ 18 - 0
config/bootstrap.php

@@ -0,0 +1,18 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+return [
+    support\bootstrap\Session::class,
+    Webman\ThinkOrm\ThinkOrm::class,
+];

+ 6 - 0
config/container.php

@@ -0,0 +1,6 @@
+<?php
+$builder = new \DI\ContainerBuilder();
+$builder->addDefinitions(config('dependence', []));
+$builder->useAutowiring(true);
+$builder->useAttributes(true);
+return $builder->build();

+ 15 - 0
config/dependence.php

@@ -0,0 +1,15 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+return [];

+ 5 - 0
config/event.php

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

+ 17 - 0
config/exception.php

@@ -0,0 +1,17 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+return [
+    '' => support\exception\Handler::class,
+];

+ 32 - 0
config/log.php

@@ -0,0 +1,32 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+return [
+    'default' => [
+        'handlers' => [
+            [
+                'class' => Monolog\Handler\RotatingFileHandler::class,
+                'constructor' => [
+                    runtime_path() . '/logs/webman.log',
+                    7, //$maxFiles
+                    Monolog\Logger::DEBUG,
+                ],
+                'formatter' => [
+                    'class' => Monolog\Formatter\LineFormatter::class,
+                    'constructor' => [null, 'Y-m-d H:i:s', true],
+                ],
+            ]
+        ],
+    ],
+];

+ 15 - 0
config/middleware.php

@@ -0,0 +1,15 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+return [];

+ 32 - 0
config/plugin/hhink/webman-sms/app.php

@@ -0,0 +1,32 @@
+<?php
+
+use Overtrue\EasySms\Strategies\OrderStrategy;
+
+return [
+    'enable' => true,
+
+    // HTTP 请求的超时时间(秒)
+    'timeout' => 5.0,
+
+    // 默认发送配置
+    'default' => [
+        // 网关调用策略,默认:顺序调用
+        'strategy' => OrderStrategy::class,
+
+        // 默认可用的发送网关
+        'gateways' => [
+            'aliyun',
+        ],
+    ],
+    // 可用的网关配置
+    'gateways' => [
+        'errorlog' => [
+            'file' => '/tmp/easy-sms.log',
+        ],
+        'aliyun' => [
+            'access_key_id' => '************',
+            'access_key_secret' => '**********************',
+            'sign_name' => '签名',
+        ],
+    ],
+];

+ 8 - 0
config/plugin/hzdad/codecheck/app.php

@@ -0,0 +1,8 @@
+<?php
+return [
+    'enable' => true,
+    'expire' => 300,
+    'length' => 6,
+    'chcktimes' => 3,//最多可以尝试次数
+    'delafterok' => true,//验证后删除
+];

+ 35 - 0
config/plugin/linfly/annotation/annotation.php

@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * Created by PhpStorm.
+ * User: LinFei
+ * Created time 2022/10/10 10:57:15
+ * E-mail: fly@eyabc.cn
+ */
+declare (strict_types=1);
+
+return [
+    // 注解扫描路径
+    'include_paths' => [
+        // 应用目录 支持通配符: * , 例如: app/*, app/*.php
+        'app',
+    ],
+    // 扫描排除的路径 支持通配符: *
+    'exclude_paths' => [
+        'app/model',
+    ],
+    // 路由设置
+    'route' => [
+        // 如果注解路由 @Route() 未传参则默认使用方法名作为path
+        'use_default_method' => true,
+    ],
+    // 验证器注解
+    'validate' => [
+        // 验证器验证处理类 (该功能需要自行安装对应的验证器扩展包),目前只支持 think-validate
+        'handle' => LinFly\Annotation\Validate\Handle\ThinkValidate::class,
+        // 验证失败处理方法
+        'fail_handle' => function (Webman\Http\Request $request, string $message) {
+            return json(['code' => 500, 'msg' => $message]);
+        }
+    ],
+];

+ 4 - 0
config/plugin/linfly/annotation/app.php

@@ -0,0 +1,4 @@
+<?php
+return [
+    'enable' => true,
+];

+ 19 - 0
config/plugin/linfly/annotation/bootstrap.php

@@ -0,0 +1,19 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+namespace LinFly\Annotation\Bootstrap;
+
+return [
+    AnnotationBootstrap::class
+];

+ 17 - 0
config/plugin/linfly/annotation/route.php

@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * Created by PhpStorm.
+ * User: LinFei
+ * Created time 2022/10/10 10:52:22
+ * E-mail: fly@eyabc.cn
+ */
+declare (strict_types=1);
+
+namespace LinFly\Annotation\Handle;
+
+use LinFly\Annotation\Bootstrap\AnnotationBootstrap;
+
+if (!AnnotationBootstrap::isIgnoreProcess()) {
+    RouteAnnotationHandle::createRoute();
+}

+ 71 - 0
config/plugin/shopwwi/auth/app.php

@@ -0,0 +1,71 @@
+<?php
+
+ return [
+     'enable' => true,
+     'app_key' => 'base64:N721v3Gt2I58HH7oiU7a70PQ+i8ekPWRqwI+JSnM1wo=',
+     'guard' => [
+         'admin' => [
+             'key' => 'id',
+             'field' => ['id','username'], //设置允许写入扩展中的字段
+             'num' => 0, //-1为不限制终端数量 0为只支持一个终端在线 大于0为同一账号同终端支持数量 建议设置为1 则同一账号同终端在线1个
+             'model'=> [\app\model\system\SystemUser::class,'thinkphp'] // 当为数组时 [app\model\Test::class,'thinkphp'] 来说明模型归属
+         ]
+     ],
+     'jwt' => [
+         'redis' => false,
+         // redis前缀
+         'redis_prefix' => '',
+         // 算法类型 ES256、HS256、HS384、HS512、RS256、RS384、RS512
+         'algorithms' => 'HS256',
+         // access令牌秘钥
+         'access_secret_key' => 'w5LgNx5luRRjmamZFSqz3cPHOp9KuQPExlvgi18DN4SdnSI9obcVEhiZVE0NIIC7',
+         // access令牌过期时间,单位秒。默认 2 小时
+         'access_exp' => 36000,
+         // refresh令牌秘钥
+         'refresh_secret_key' => 'w5LgNx5luRRjmamZFSqz3cPHOp9KuQPExlvgi18DN4SdnSI9obcVEhiZVE0NIIC7',
+         // refresh令牌过期时间,单位秒。默认 7 天
+         'refresh_exp' => 72000,
+         // 令牌签发者
+         'iss' => 'webman',
+         // 令牌签发时间
+         'iat' => time(),
+
+         /**
+          * access令牌 RS256 私钥
+          * 生成RSA私钥(Linux系统):openssl genrsa -out access_private_key.key 1024 (2048)
+          */
+         'access_private_key' => <<<EOD
+-----BEGIN RSA PRIVATE KEY-----
+...
+-----END RSA PRIVATE KEY-----
+EOD,
+         /**
+          * access令牌 RS256 公钥
+          * 生成RSA公钥(Linux系统):openssl rsa -in access_private_key.key -pubout -out access_public_key.key
+          */
+         'access_public_key' => <<<EOD
+-----BEGIN PUBLIC KEY-----
+...
+-----END PUBLIC KEY-----
+EOD,
+
+         /**
+          * refresh令牌 RS256 私钥
+          * 生成RSA私钥(Linux系统):openssl genrsa -out refresh_private_key.key 1024 (2048)
+          */
+         'refresh_private_key' => <<<EOD
+-----BEGIN RSA PRIVATE KEY-----
+...
+-----END RSA PRIVATE KEY-----
+EOD,
+         /**
+          * refresh令牌 RS256 公钥
+          * 生成RSA公钥(Linux系统):openssl rsa -in refresh_private_key.key -pubout -out refresh_public_key.key
+          */
+         'refresh_public_key' => <<<EOD
+-----BEGIN PUBLIC KEY-----
+...
+-----END PUBLIC KEY-----
+EOD,
+     ],
+ ];

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 12 - 0
config/plugin/tinywan/captcha/app.php


+ 76 - 0
config/plugin/tinywan/storage/app.php

@@ -0,0 +1,76 @@
+<?php
+/**
+ * @desc app.php 描述信息
+ *
+ * @author Tinywan(ShaoBo Wan)
+ * @date 2022/3/10 19:46
+ */
+
+return [
+    'enable' => true,
+    'storage' => [
+        'default' => 'local', // local:本地 oss:阿里云 cos:腾讯云 qos:七牛云
+        'single_limit' => 1024 * 1024 * 200, // 单个文件的大小限制,默认200M 1024 * 1024 * 200
+        'total_limit' => 1024 * 1024 * 200, // 所有文件的大小限制,默认200M 1024 * 1024 * 200
+        'nums' => 10, // 文件数量限制,默认10
+        'include' => [], // 被允许的文件类型列表
+        'exclude' => [], // 不被允许的文件类型列表
+        // 本地对象存储
+        'local' => [
+            'adapter' => \Tinywan\Storage\Adapter\LocalAdapter::class,
+            'root' => runtime_path().'/storage',
+            'dirname' => function () {
+                return date('Ymd');
+            },
+            'domain' => 'http://127.0.0.1:8787',
+            'uri' => '/runtime', // 如果 domain + uri 不在 public 目录下,请做好软链接,否则生成的url无法访问
+            'algo' => 'sha1',
+        ],
+        // 阿里云对象存储
+        'oss' => [
+            'adapter' => \Tinywan\Storage\Adapter\OssAdapter::class,
+            'accessKeyId' => 'xxxxxxxxxxxx',
+            'accessKeySecret' => 'xxxxxxxxxxxx',
+            'bucket' => 'resty-webman',
+            'dirname' => function () {
+                return 'storage';
+            },
+            'domain' => 'http://webman.oss.tinywan.com',
+            'endpoint' => 'oss-cn-hangzhou.aliyuncs.com',
+            'algo' => 'sha1',
+        ],
+        // 腾讯云对象存储
+        'cos' => [
+            'adapter' => \Tinywan\Storage\Adapter\CosAdapter::class,
+            'secretId' => 'xxxxxxxxxxxxx',
+            'secretKey' => 'xxxxxxxxxxxx',
+            'bucket' => 'resty-webman-xxxxxxxxx',
+            'dirname' => 'storage',
+            'domain' => 'http://webman.oss.tinywan.com',
+            'region' => 'ap-shanghai',
+        ],
+        // 七牛云对象存储
+        'qiniu' => [
+            'adapter' => \Tinywan\Storage\Adapter\QiniuAdapter::class,
+            'accessKey' => 'xxxxxxxxxxxxx',
+            'secretKey' => 'xxxxxxxxxxxxx',
+            'bucket' => 'resty-webman',
+            'dirname' => 'storage',
+            'domain' => 'http://webman.oss.tinywan.com',
+        ],
+        // aws
+        's3' => [
+            'adapter' => \Tinywan\Storage\Adapter\S3Adapter::class,
+            'key' => 'xxxxxxxxxxxxx',
+            'secret' => 'xxxxxxxxxxxxx',
+            'bucket' => 'resty-webman',
+            'dirname' => 'storage',
+            'domain' => 'http://webman.oss.tinywan.com',
+            'region' => 'S3_REGION',
+            'version' => 'latest',
+            'use_path_style_endpoint' => true,
+            'endpoint' => 'S3_ENDPOINT',
+            'acl' => 'public-read',
+        ],
+    ],
+];

+ 24 - 0
config/plugin/webman/console/app.php

@@ -0,0 +1,24 @@
+<?php
+return [
+    'enable' => true,
+
+    'build_dir'  => BASE_PATH . DIRECTORY_SEPARATOR . 'build',
+
+    'phar_filename' => 'webman.phar',
+
+    'bin_filename' => 'webman.bin',
+
+    'signature_algorithm'=> Phar::SHA256, //set the signature algorithm for a phar and apply it. The signature algorithm must be one of Phar::MD5, Phar::SHA1, Phar::SHA256, Phar::SHA512, or Phar::OPENSSL.
+
+    'private_key_file'  => '', // The file path for certificate or OpenSSL private key file.
+
+    'exclude_pattern'   => '#^(?!.*(composer.json|/.github/|/.idea/|/.git/|/.setting/|/runtime/|/vendor-bin/|/build/|/vendor/webman/admin/))(.*)$#',
+
+    'exclude_files'     => [
+        '.env', 'LICENSE', 'composer.json', 'composer.lock', 'start.php', 'webman.phar', 'webman.bin'
+    ],
+
+    'custom_ini' => '
+memory_limit = 256M
+    ',
+];

+ 4 - 0
config/plugin/webman/event/app.php

@@ -0,0 +1,4 @@
+<?php
+return [
+    'enable' => true,
+];

+ 17 - 0
config/plugin/webman/event/bootstrap.php

@@ -0,0 +1,17 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+return [
+    Webman\Event\BootStrap::class,
+];

+ 7 - 0
config/plugin/webman/event/command.php

@@ -0,0 +1,7 @@
+<?php
+
+use Webman\Event\EventListCommand;
+
+return [
+    EventListCommand::class
+];

+ 14 - 0
config/plugin/webman/rate-limiter/app.php

@@ -0,0 +1,14 @@
+<?php
+return [
+    'enable' => true,
+    'driver' => 'auto', // auto, apcu, memory, redis
+    'stores' => [
+        'redis' => [
+            'connection' => 'default',
+        ]
+    ],
+    // 这些ip的请求不做频率限制
+    'ip_whitelist' => [
+        '127.0.0.1',
+    ],
+];

+ 17 - 0
config/plugin/webman/rate-limiter/bootstrap.php

@@ -0,0 +1,17 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+return [
+    Webman\RateLimiter\Bootstrap::class
+];

+ 8 - 0
config/plugin/webman/rate-limiter/middleware.php

@@ -0,0 +1,8 @@
+<?php
+use Webman\RateLimiter\Limiter;
+
+return [
+    '@' => [
+        Limiter::class
+    ],
+];

+ 4 - 0
config/plugin/webman/redis-queue/app.php

@@ -0,0 +1,4 @@
+<?php
+return [
+    'enable' => true,
+];

+ 7 - 0
config/plugin/webman/redis-queue/command.php

@@ -0,0 +1,7 @@
+<?php
+
+use Webman\RedisQueue\Command\MakeConsumerCommand;
+
+return [
+    MakeConsumerCommand::class
+];

+ 32 - 0
config/plugin/webman/redis-queue/log.php

@@ -0,0 +1,32 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+return [
+    'default' => [
+        'handlers' => [
+            [
+                'class' => Monolog\Handler\RotatingFileHandler::class,
+                'constructor' => [
+                    runtime_path() . '/logs/redis-queue/queue.log',
+                    7, //$maxFiles
+                    Monolog\Logger::DEBUG,
+                ],
+                'formatter' => [
+                    'class' => Monolog\Formatter\LineFormatter::class,
+                    'constructor' => [null, 'Y-m-d H:i:s', true],
+                ],
+            ]
+        ],
+    ]
+];

+ 11 - 0
config/plugin/webman/redis-queue/process.php

@@ -0,0 +1,11 @@
+<?php
+return [
+    'consumer'  => [
+        'handler'     => Webman\RedisQueue\Process\Consumer::class,
+        'count'       => 8, // 可以设置多进程同时消费
+        'constructor' => [
+            // 消费者类目录
+            'consumer_dir' => app_path() . '/queue/redis'
+        ]
+    ]
+];

+ 21 - 0
config/plugin/webman/redis-queue/redis.php

@@ -0,0 +1,21 @@
+<?php
+return [
+    'default' => [
+        'host' => 'redis://127.0.0.1:6379',
+        'options' => [
+            'auth' => null,
+            'db' => 0,
+            'prefix' => '',
+            'max_attempts'  => 5,
+            'retry_seconds' => 5,
+        ],
+        // Connection pool, supports only Swoole or Swow drivers.
+        'pool' => [
+            'max_connections' => 5,
+            'min_connections' => 1,
+            'wait_timeout' => 3,
+            'idle_timeout' => 60,
+            'heartbeat_interval' => 50,
+        ]
+    ],
+];

+ 49 - 0
config/process.php

@@ -0,0 +1,49 @@
+<?php
+use support\Log;
+use support\Request;
+use app\process\Http;
+
+global $argv;
+
+return [
+    'wash' => [
+        'handler' => Http::class,
+        'listen' => 'http://0.0.0.0:8190',
+        'count' => cpu_count() * 4,
+        'user' => '',
+        'group' => '',
+        'reusePort' => false,
+        'eventLoop' => '',
+        'context' => [],
+        'constructor' => [
+            'requestClass' => Request::class,
+            'logger' => Log::channel('default'),
+            'appPath' => app_path(),
+            'publicPath' => public_path()
+        ]
+    ],
+    // File update detection and automatic reload
+    'monitor' => [
+        'handler' => app\process\Monitor::class,
+        'reloadable' => false,
+        'constructor' => [
+            // Monitor these directories
+            'monitorDir' => array_merge([
+                app_path(),
+                config_path(),
+                base_path() . '/process',
+                base_path() . '/support',
+                base_path() . '/resource',
+                base_path() . '/.env',
+            ], glob(base_path() . '/plugin/*/app'), glob(base_path() . '/plugin/*/config'), glob(base_path() . '/plugin/*/api')),
+            // Files with these suffixes will be monitored
+            'monitorExtensions' => [
+                'php', 'html', 'htm', 'env'
+            ],
+            'options' => [
+                'enable_file_monitor' => !in_array('-d', $argv) && DIRECTORY_SEPARATOR === '/',
+                'enable_memory_monitor' => DIRECTORY_SEPARATOR === '/',
+            ]
+        ]
+    ]
+];

+ 29 - 0
config/redis.php

@@ -0,0 +1,29 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+return [
+    'default' => [
+        'password' => '',
+        'host' => '127.0.0.1',
+        'port' => 6379,
+        'database' => 0,
+        'pool' => [
+            'max_connections' => 5,
+            'min_connections' => 1,
+            'wait_timeout' => 3,
+            'idle_timeout' => 60,
+            'heartbeat_interval' => 50,
+        ],
+    ]
+];

+ 21 - 0
config/route.php

@@ -0,0 +1,21 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+use Webman\Route;
+
+
+
+
+
+

+ 10 - 0
config/server.php

@@ -0,0 +1,10 @@
+<?php
+return [
+    'event_loop' => '',
+    'stop_timeout' => 2,
+    'pid_file' => runtime_path() . '/wash.pid',
+    'status_file' => runtime_path() . '/wash.status',
+    'stdout_file' => runtime_path() . '/logs/stdout.log',
+    'log_file' => runtime_path() . '/logs/workerman.log',
+    'max_package_size' => 10 * 1024 * 1024
+];

+ 65 - 0
config/session.php

@@ -0,0 +1,65 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+use Webman\Session\FileSessionHandler;
+use Webman\Session\RedisSessionHandler;
+use Webman\Session\RedisClusterSessionHandler;
+
+return [
+
+    'type' => 'file', // or redis or redis_cluster
+
+    'handler' => FileSessionHandler::class,
+
+    'config' => [
+        'file' => [
+            'save_path' => runtime_path() . '/sessions',
+        ],
+        'redis' => [
+            'host' => '127.0.0.1',
+            'port' => 6379,
+            'auth' => '',
+            'timeout' => 2,
+            'database' => '',
+            'prefix' => 'redis_session_',
+        ],
+        'redis_cluster' => [
+            'host' => ['127.0.0.1:7000', '127.0.0.1:7001', '127.0.0.1:7001'],
+            'timeout' => 2,
+            'auth' => '',
+            'prefix' => 'redis_session_',
+        ]
+    ],
+
+    'session_name' => 'PHPSID',
+    
+    'auto_update_timestamp' => false,
+
+    'lifetime' => 7*24*60*60,
+
+    'cookie_lifetime' => 365*24*60*60,
+
+    'cookie_path' => '/',
+
+    'domain' => '',
+    
+    'http_only' => true,
+
+    'secure' => false,
+    
+    'same_site' => '',
+
+    'gc_probability' => [1, 1000],
+
+];

+ 23 - 0
config/static.php

@@ -0,0 +1,23 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+/**
+ * Static file settings
+ */
+return [
+    'enable' => true,
+    'middleware' => [     // Static file Middleware
+        //app\middleware\StaticFile::class,
+    ],
+];

+ 38 - 0
config/think-cache.php

@@ -0,0 +1,38 @@
+<?php
+return [
+    // 默认缓存驱动
+    'default' => 'redis',
+    // 缓存连接方式配置
+    'stores'  => [
+        // redis缓存
+        'redis' => [
+            // 驱动方式
+            'type' => 'redis',
+            // 服务器地址
+            'host' => '127.0.0.1',
+            // 缓存前缀
+            'prefix' => 'cache:',
+            // 默认缓存有效期 0表示永久缓存
+            'expire'     => 0,
+            // Thinkphp官方没有这个参数,由于生成的tag键默认不过期,如果tag键数量很大,避免长时间占用内存,可以设置一个超过其他缓存的过期时间,0为不设置
+            'tag_expire' => 86400 * 30,
+            // 缓存标签前缀
+            'tag_prefix' => 'tag:',
+            // 连接池配置
+            'pool' => [
+                'max_connections' => 5, // 最大连接数
+                'min_connections' => 1, // 最小连接数
+                'wait_timeout' => 3,    // 从连接池获取连接等待超时时间
+                'idle_timeout' => 60,   // 连接最大空闲时间,超过该时间会被回收
+                'heartbeat_interval' => 50, // 心跳检测间隔,需要小于60秒
+            ],
+        ],
+        // 文件缓存
+        'file' => [
+            // 驱动方式
+            'type' => 'file',
+            // 设置不同的缓存保存目录
+            'path' => runtime_path() . '/file/',
+        ],
+    ],
+];

+ 42 - 0
config/think-orm.php

@@ -0,0 +1,42 @@
+<?php
+
+return [
+    'default' => getenv('DB_DEFAULT'),
+    'connections' => [
+        'mysql' => [
+            // 数据库类型
+            'type' => 'mysql',
+            // 服务器地址
+            'hostname' => getenv('DB_HOST'),
+            // 数据库名
+            'database' => getenv('DB_NAME'),
+            // 数据库用户名
+            'username' => getenv('DB_USER'),
+            // 数据库密码
+            'password' => getenv('DB_PASSWORD'),
+            // 数据库连接端口
+            'hostport' => getenv('DB_PORT'),
+            // 数据库连接参数
+            'params' => [
+                // 连接超时3秒
+                \PDO::ATTR_TIMEOUT => 3,
+            ],
+            // 数据库编码默认采用utf8
+            'charset' => 'utf8',
+            // 数据库表前缀
+            'prefix' => '',
+            // 断线重连
+            'break_reconnect' => true,
+            // 连接池配置
+            'pool' => [
+                'max_connections' => 5, // 最大连接数
+                'min_connections' => 1, // 最小连接数
+                'wait_timeout' => 3,    // 从连接池获取连接等待超时时间
+                'idle_timeout' => 60,   // 连接最大空闲时间,超过该时间会被回收
+                'heartbeat_interval' => 50, // 心跳检测间隔,需要小于60秒
+            ],
+        ],
+    ],
+    // 自定义分页类
+    'paginator' =>  '',
+];

+ 13 - 0
config/translation.php

@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * Multilingual configuration
+ */
+return [
+    // Default language
+    'locale' => 'zh_CN',
+    // Fallback language
+    'fallback_locale' => ['zh_CN', 'en'],
+    // Folder where language files are stored
+    'path' => base_path() . '/resource/translations',
+];

+ 22 - 0
config/view.php

@@ -0,0 +1,22 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+use support\view\Raw;
+use support\view\Twig;
+use support\view\Blade;
+use support\view\ThinkPHP;
+
+return [
+    'handler' => Raw::class
+];

BIN
public/favicon.ico


+ 3 - 0
resource/translations/en/messages.php

@@ -0,0 +1,3 @@
+<?php
+
+return [];

+ 40 - 0
resource/translations/zh_CN/messages.php

@@ -0,0 +1,40 @@
+<?php
+
+
+return [
+    "empty" => [
+        "mobile"    => "请输入手机号码",
+        "user"      => "请输入用户名",
+        "passwd"    => "请输入登陆密码",
+        "code"      => "请输入验证码",
+        "data"      => "数据不存在",
+        "require"   => "参数不能为空",
+        "agent"     => "未绑定代理信息"
+    ],
+    "error"     => [
+        "data"          => "操作失败",
+        "captcha"       => "验证码输入错误",
+        "mobile"        => "手机号码格式错误",
+        "sms-repeat"    => "请勿重复获取",
+        "sms"           => "获取验证码失败,请联系管理员",
+        "mobile-empty"  => "手机号未注册,请开通后再重试",
+        "mobile-exist"  => "手机号已被注册",
+        "user-empty"    => "登录账号不存在",
+        "user-exist"    => "登录账号已存在,请更换",
+        "user-status"   => "该账号已被冻结,请联系管理员",
+        "passwd"        => "登录密码错误",
+        "param"         => "不合法的参数",
+        "request"       => "不合法的请求格式",
+        "login"         => "登录已过期,请重新登录",
+        "sms-err"       => "短信验证码错误",
+        "agent"         => "代理状态异常,请联系管理员",
+        "agent-out"     => "权限已到期,请联系管理员",
+        "agent-no-exist"   => "代理不存在",
+        "store-no-exist"   => "门店不存在",
+    ],
+    "success"   => [
+        "data"  => "操作成功",
+        "sms"   => "验证码已成功发送至%mobile%,请注意查收",
+        "login" => "登录成功"
+    ],
+];

+ 5 - 0
start.php

@@ -0,0 +1,5 @@
+#!/usr/bin/env php
+<?php
+chdir(__DIR__);
+require_once __DIR__ . '/vendor/autoload.php';
+support\App::run();

+ 25 - 0
support/Request.php

@@ -0,0 +1,25 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+namespace support;
+
+/**
+ * Class Request
+ * @package support
+ */
+class Request extends \Webman\Http\Request
+{
+
+    public mixed $user;
+}

+ 24 - 0
support/Response.php

@@ -0,0 +1,24 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+namespace support;
+
+/**
+ * Class Response
+ * @package support
+ */
+class Response extends \Webman\Http\Response
+{
+
+}

+ 139 - 0
support/bootstrap.php

@@ -0,0 +1,139 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+use Dotenv\Dotenv;
+use support\Log;
+use Webman\Bootstrap;
+use Webman\Config;
+use Webman\Middleware;
+use Webman\Route;
+use Webman\Util;
+use Workerman\Events\Select;
+use Workerman\Worker;
+
+$worker = $worker ?? null;
+
+if (empty(Worker::$eventLoopClass)) {
+    Worker::$eventLoopClass = Select::class;
+}
+
+set_error_handler(function ($level, $message, $file = '', $line = 0) {
+    if (error_reporting() & $level) {
+        throw new ErrorException($message, 0, $level, $file, $line);
+    }
+});
+
+if ($worker) {
+    register_shutdown_function(function ($startTime) {
+        if (time() - $startTime <= 0.1) {
+            sleep(1);
+        }
+    }, time());
+}
+
+if (class_exists('Dotenv\Dotenv') && file_exists(base_path(false) . '/.env')) {
+    if (method_exists('Dotenv\Dotenv', 'createUnsafeMutable')) {
+        Dotenv::createUnsafeMutable(base_path(false))->load();
+    } else {
+        Dotenv::createMutable(base_path(false))->load();
+    }
+}
+
+Config::clear();
+support\App::loadAllConfig(['route']);
+if ($timezone = config('app.default_timezone')) {
+    date_default_timezone_set($timezone);
+}
+
+foreach (config('autoload.files', []) as $file) {
+    include_once $file;
+}
+foreach (config('plugin', []) as $firm => $projects) {
+    foreach ($projects as $name => $project) {
+        if (!is_array($project)) {
+            continue;
+        }
+        foreach ($project['autoload']['files'] ?? [] as $file) {
+            include_once $file;
+        }
+    }
+    foreach ($projects['autoload']['files'] ?? [] as $file) {
+        include_once $file;
+    }
+}
+
+Middleware::load(config('middleware', []));
+foreach (config('plugin', []) as $firm => $projects) {
+    foreach ($projects as $name => $project) {
+        if (!is_array($project) || $name === 'static') {
+            continue;
+        }
+        Middleware::load($project['middleware'] ?? []);
+    }
+    Middleware::load($projects['middleware'] ?? [], $firm);
+    if ($staticMiddlewares = config("plugin.$firm.static.middleware")) {
+        Middleware::load(['__static__' => $staticMiddlewares], $firm);
+    }
+}
+Middleware::load(['__static__' => config('static.middleware', [])]);
+
+foreach (config('bootstrap', []) as $className) {
+    if (!class_exists($className)) {
+        $log = "Warning: Class $className setting in config/bootstrap.php not found\r\n";
+        echo $log;
+        Log::error($log);
+        continue;
+    }
+    /** @var Bootstrap $className */
+    $className::start($worker);
+}
+
+foreach (config('plugin', []) as $firm => $projects) {
+    foreach ($projects as $name => $project) {
+        if (!is_array($project)) {
+            continue;
+        }
+        foreach ($project['bootstrap'] ?? [] as $className) {
+            if (!class_exists($className)) {
+                $log = "Warning: Class $className setting in config/plugin/$firm/$name/bootstrap.php not found\r\n";
+                echo $log;
+                Log::error($log);
+                continue;
+            }
+            /** @var Bootstrap $className */
+            $className::start($worker);
+        }
+    }
+    foreach ($projects['bootstrap'] ?? [] as $className) {
+        /** @var string $className */
+        if (!class_exists($className)) {
+            $log = "Warning: Class $className setting in plugin/$firm/config/bootstrap.php not found\r\n";
+            echo $log;
+            Log::error($log);
+            continue;
+        }
+        /** @var Bootstrap $className */
+        $className::start($worker);
+    }
+}
+
+$directory = base_path() . '/plugin';
+$paths = [config_path()];
+foreach (Util::scanDir($directory) as $path) {
+    if (is_dir($path = "$path/config")) {
+        $paths[] = $path;
+    }
+}
+Route::load($paths);
+

+ 71 - 0
webman

@@ -0,0 +1,71 @@
+#!/usr/bin/env php
+<?php
+
+use Webman\Config;
+use Webman\Console\Command;
+use Webman\Console\Util;
+use support\Container;
+use Dotenv\Dotenv;
+
+if (!Phar::running()) {
+    chdir(__DIR__);
+}
+require_once __DIR__ . '/vendor/autoload.php';
+
+if (!$appConfigFile = config_path('app.php')) {
+    throw new RuntimeException('Config file not found: app.php');
+}
+
+if (class_exists(Dotenv::class) && file_exists(run_path('.env'))) {
+    if (method_exists(Dotenv::class, 'createUnsafeImmutable')) {
+        Dotenv::createUnsafeImmutable(run_path())->load();
+    } else {
+        Dotenv::createMutable(run_path())->load();
+    }
+}
+
+$appConfig = require $appConfigFile;
+if ($timezone = $appConfig['default_timezone'] ?? '') {
+    date_default_timezone_set($timezone);
+}
+
+if ($errorReporting = $appConfig['error_reporting'] ?? '') {
+    error_reporting($errorReporting);
+}
+
+if (!in_array($argv[1] ?? '', ['start', 'restart', 'stop', 'status', 'reload', 'connections'])) {
+    require_once __DIR__ . '/support/bootstrap.php';
+} else {
+    if (class_exists('Support\App')) {
+        Support\App::loadAllConfig(['route']);
+    } else {
+        Config::reload(config_path(), ['route', 'container']);
+    }
+}
+
+$cli = new Command();
+$cli->setName('webman cli');
+$cli->installInternalCommands();
+if (is_dir($command_path = Util::guessPath(app_path(), '/command', true))) {
+    $cli->installCommands($command_path);
+}
+
+foreach (config('plugin', []) as $firm => $projects) {
+    if (isset($projects['app'])) {
+        foreach (['', '/app'] as $app) {
+            if ($command_str = Util::guessPath(base_path() . "/plugin/$firm{$app}", 'command')) {
+                $command_path = base_path() . "/plugin/$firm{$app}/$command_str";
+                $cli->installCommands($command_path, "plugin\\$firm" . str_replace('/', '\\', $app) . "\\$command_str");
+            }
+        }
+    }
+    foreach ($projects as $name => $project) {
+        if (!is_array($project)) {
+            continue;
+        }
+        $project['command'] ??= [];
+        array_walk($project['command'], [$cli, 'createCommandInstance']);
+    }
+}
+
+$cli->run();

+ 3 - 0
windows.bat

@@ -0,0 +1,3 @@
+CHCP 65001
+php windows.php
+pause

+ 136 - 0
windows.php

@@ -0,0 +1,136 @@
+<?php
+/**
+ * Start file for windows
+ */
+chdir(__DIR__);
+require_once __DIR__ . '/vendor/autoload.php';
+
+use Dotenv\Dotenv;
+use support\App;
+use Workerman\Worker;
+
+ini_set('display_errors', 'on');
+error_reporting(E_ALL);
+
+if (class_exists('Dotenv\Dotenv') && file_exists(base_path() . '/.env')) {
+    if (method_exists('Dotenv\Dotenv', 'createUnsafeImmutable')) {
+        Dotenv::createUnsafeImmutable(base_path())->load();
+    } else {
+        Dotenv::createMutable(base_path())->load();
+    }
+}
+
+App::loadAllConfig(['route']);
+
+$errorReporting = config('app.error_reporting');
+if (isset($errorReporting)) {
+    error_reporting($errorReporting);
+}
+
+$runtimeProcessPath = runtime_path() . DIRECTORY_SEPARATOR . '/windows';
+$paths = [
+    $runtimeProcessPath,
+    runtime_path('logs'),
+    runtime_path('views')
+];
+foreach ($paths as $path) {
+    if (!is_dir($path)) {
+        mkdir($path, 0777, true);
+    }
+}
+
+$processFiles = [];
+if (config('server.listen')) {
+    $processFiles[] = __DIR__ . DIRECTORY_SEPARATOR . 'start.php';
+}
+foreach (config('process', []) as $processName => $config) {
+    $processFiles[] = write_process_file($runtimeProcessPath, $processName, '');
+}
+
+foreach (config('plugin', []) as $firm => $projects) {
+    foreach ($projects as $name => $project) {
+        if (!is_array($project)) {
+            continue;
+        }
+        foreach ($project['process'] ?? [] as $processName => $config) {
+            $processFiles[] = write_process_file($runtimeProcessPath, $processName, "$firm.$name");
+        }
+    }
+    foreach ($projects['process'] ?? [] as $processName => $config) {
+        $processFiles[] = write_process_file($runtimeProcessPath, $processName, $firm);
+    }
+}
+
+function write_process_file($runtimeProcessPath, $processName, $firm): string
+{
+    $processParam = $firm ? "plugin.$firm.$processName" : $processName;
+    $configParam = $firm ? "config('plugin.$firm.process')['$processName']" : "config('process')['$processName']";
+    $fileContent = <<<EOF
+<?php
+require_once __DIR__ . '/../../vendor/autoload.php';
+
+use Workerman\Worker;
+use Workerman\Connection\TcpConnection;
+use Webman\Config;
+use support\App;
+
+ini_set('display_errors', 'on');
+error_reporting(E_ALL);
+
+if (is_callable('opcache_reset')) {
+    opcache_reset();
+}
+
+if (!\$appConfigFile = config_path('app.php')) {
+    throw new RuntimeException('Config file not found: app.php');
+}
+\$appConfig = require \$appConfigFile;
+if (\$timezone = \$appConfig['default_timezone'] ?? '') {
+    date_default_timezone_set(\$timezone);
+}
+
+App::loadAllConfig(['route']);
+
+worker_start('$processParam', $configParam);
+
+if (DIRECTORY_SEPARATOR != "/") {
+    Worker::\$logFile = config('server')['log_file'] ?? Worker::\$logFile;
+    TcpConnection::\$defaultMaxPackageSize = config('server')['max_package_size'] ?? 10*1024*1024;
+}
+
+Worker::runAll();
+
+EOF;
+    $processFile = $runtimeProcessPath . DIRECTORY_SEPARATOR . "start_$processParam.php";
+    file_put_contents($processFile, $fileContent);
+    return $processFile;
+}
+
+if ($monitorConfig = config('process.monitor.constructor')) {
+    $monitorHandler = config('process.monitor.handler');
+    $monitor = new $monitorHandler(...array_values($monitorConfig));
+}
+
+function popen_processes($processFiles)
+{
+    $cmd = '"' . PHP_BINARY . '" ' . implode(' ', $processFiles);
+    $descriptorspec = [STDIN, STDOUT, STDOUT];
+    $resource = proc_open($cmd, $descriptorspec, $pipes, null, null, ['bypass_shell' => true]);
+    if (!$resource) {
+        exit("Can not execute $cmd\r\n");
+    }
+    return $resource;
+}
+
+$resource = popen_processes($processFiles);
+echo "\r\n";
+while (1) {
+    sleep(1);
+    if (!empty($monitor) && $monitor->checkAllFilesChange()) {
+        $status = proc_get_status($resource);
+        $pid = $status['pid'];
+        shell_exec("taskkill /F /T /PID $pid");
+        proc_close($resource);
+        $resource = popen_processes($processFiles);
+    }
+}

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác