Browse Source

'2235-01'

zory 2 days ago
parent
commit
609826cf6c
99 changed files with 5160 additions and 1 deletions
  1. 20 0
      .gitignore
  2. 21 0
      LICENSE
  3. 20 1
      README.md
  4. 133 0
      app/command/Model.php
  5. 146 0
      app/controller/Dashboard.php
  6. 66 0
      app/controller/admin/Config.php
  7. 29 0
      app/controller/admin/Dashboard.php
  8. 158 0
      app/controller/admin/Shop.php
  9. 65 0
      app/controller/admin/Upload.php
  10. 34 0
      app/controller/common/Common.php
  11. 145 0
      app/controller/common/Login.php
  12. 75 0
      app/controller/common/Menu.php
  13. 89 0
      app/controller/notify/SfExpress.php
  14. 206 0
      app/extra/basic/Base.php
  15. 30 0
      app/extra/basic/Model.php
  16. 103 0
      app/extra/basic/Service.php
  17. 98 0
      app/extra/service/basic/UploadService.php
  18. 37 0
      app/extra/service/saas/ShopService.php
  19. 28 0
      app/extra/service/system/MenuService.php
  20. 183 0
      app/extra/service/system/SystemService.php
  21. 42 0
      app/extra/service/system/UserService.php
  22. 135 0
      app/extra/tools/CodeExtend.php
  23. 73 0
      app/extra/tools/DataExtend.php
  24. 122 0
      app/extra/tools/UploadExtend.php
  25. 463 0
      app/functions.php
  26. 45 0
      app/middleware/AuthMiddleware.php
  27. 44 0
      app/middleware/DyMiddleware.php
  28. 42 0
      app/middleware/StaticFile.php
  29. 62 0
      app/model/saas/SaasAgent.php
  30. 48 0
      app/model/system/SystemConfig.php
  31. 45 0
      app/model/system/SystemData.php
  32. 47 0
      app/model/system/SystemExport.php
  33. 54 0
      app/model/system/SystemMenu.php
  34. 48 0
      app/model/system/SystemOplog.php
  35. 58 0
      app/model/system/SystemUser.php
  36. 10 0
      app/process/Http.php
  37. 305 0
      app/process/Monitor.php
  38. 31 0
      app/queue/redis/AutoExpress.php
  39. 32 0
      app/queue/redis/AutoPrint.php
  40. 80 0
      app/queue/redis/ExportOrder.php
  41. 36 0
      app/queue/redis/PrintState.php
  42. 78 0
      app/queue/redis/SyncOrder.php
  43. 32 0
      app/validate/saas/ShopValidate.php
  44. 27 0
      app/validate/saas/UserValidate.php
  45. 14 0
      app/view/index/view.html
  46. 82 0
      composer.json
  47. 26 0
      config/app.php
  48. 21 0
      config/autoload.php
  49. 18 0
      config/bootstrap.php
  50. 6 0
      config/container.php
  51. 15 0
      config/dependence.php
  52. 5 0
      config/event.php
  53. 17 0
      config/exception.php
  54. 32 0
      config/log.php
  55. 15 0
      config/middleware.php
  56. 32 0
      config/plugin/hhink/webman-sms/app.php
  57. 8 0
      config/plugin/hzdad/codecheck/app.php
  58. 35 0
      config/plugin/linfly/annotation/annotation.php
  59. 4 0
      config/plugin/linfly/annotation/app.php
  60. 19 0
      config/plugin/linfly/annotation/bootstrap.php
  61. 22 0
      config/plugin/linfly/annotation/route.php
  62. 77 0
      config/plugin/shopwwi/auth/app.php
  63. 12 0
      config/plugin/tinywan/captcha/app.php
  64. 76 0
      config/plugin/tinywan/storage/app.php
  65. 24 0
      config/plugin/webman/console/app.php
  66. 4 0
      config/plugin/webman/event/app.php
  67. 17 0
      config/plugin/webman/event/bootstrap.php
  68. 7 0
      config/plugin/webman/event/command.php
  69. 14 0
      config/plugin/webman/rate-limiter/app.php
  70. 17 0
      config/plugin/webman/rate-limiter/bootstrap.php
  71. 8 0
      config/plugin/webman/rate-limiter/middleware.php
  72. 4 0
      config/plugin/webman/redis-queue/app.php
  73. 7 0
      config/plugin/webman/redis-queue/command.php
  74. 32 0
      config/plugin/webman/redis-queue/log.php
  75. 11 0
      config/plugin/webman/redis-queue/process.php
  76. 21 0
      config/plugin/webman/redis-queue/redis.php
  77. 49 0
      config/process.php
  78. 29 0
      config/redis.php
  79. 21 0
      config/route.php
  80. 10 0
      config/server.php
  81. 65 0
      config/session.php
  82. 23 0
      config/static.php
  83. 38 0
      config/think-cache.php
  84. 42 0
      config/think-orm.php
  85. 13 0
      config/translation.php
  86. 22 0
      config/view.php
  87. BIN
      public/favicon.ico
  88. BIN
      public/logs/已预约订单_1765168684493.xlsx
  89. 25 0
      public/uploads/storage/20251210/64762e3876273e5f65d3cd11797de2779603bd85.pem
  90. 28 0
      public/uploads/storage/20251210/bd1a4e35f31b7d8deb0f03ac4aa6f99d29db9314.pem
  91. 3 0
      resource/translations/en/messages.php
  92. 42 0
      resource/translations/zh_CN/messages.php
  93. 5 0
      start.php
  94. 25 0
      support/Request.php
  95. 24 0
      support/Response.php
  96. 139 0
      support/bootstrap.php
  97. 71 0
      webman
  98. 3 0
      windows.bat
  99. 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.

+ 20 - 1
README.md

@@ -1,2 +1,21 @@
-# print-api
+# hx-mini-api
 
+### 跨域
+
+```
+文件
+config/plugin/linfly/annotation/route.php
+新增内容
+use Webman\Route;
+// 匹配所有options路由
+Route::options('[{path:.+}]', function (){
+    return response('',204);
+});
+```
+
+### 合并
+
+````
+git fetch origin
+git reset --hard origin/master
+````

+ 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';
+        }
+    }
+
+}

+ 146 - 0
app/controller/Dashboard.php

@@ -0,0 +1,146 @@
+<?php
+
+namespace app\controller;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasOrderLife;
+use app\model\saas\SaasStore;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/api/dashboard"),Middleware(AuthMiddleware::class)]
+class Dashboard extends Base
+{
+
+
+    /**
+     *
+     * @return Response
+     */
+    #[Route(path: "manage",methods: "get")]
+    public function getManageData(): Response
+    {
+        try {
+            $total = (new SaasOrderLife)->where("status","in",[1,2,3,6])->whereDay("create_at")->field("ROUND(sum(pay_amount)/100,2) as money,count(id) as num")->find();
+            $today = [
+                "order_money"   => $total['money']??0, // 营业额
+                "order_num"     => $total['num']??0, // 订单数
+                "order_done"    => "", // 已核销
+                "order_send"    => "",  // 待发货
+                "store_num"     => (new SaasStore)->count(), // 总店铺数量
+            ];
+            $moneys = $orders = [];
+            $fields = ['ROUND(sum(pay_amount)/100,2)' => 'total','substr(create_at,1,10)' => 'mday'];
+            $orderMoney = (new SaasOrderLife)->field($fields)->where("status","in",[1,2,3,6])->whereTime('create_at', '-15 days')->group('mday')->select()->column(null, 'mday');
+            for ($i = 15; $i >= 0; $i--) {
+                $date = date('Y-m-d', strtotime("-{$i}days"));
+                $moneys[] = [
+                    '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                    '订单金额' => ($orderMoney[$date] ?? [])['total'] ?? 0,
+                ];
+            }
+            $field = ['count(1)' => 'count', 'substr(create_at,1,10)' => 'mday'];
+            $orderNum = (new SaasOrderLife)->field($field)->where("status","in",[1,2,3,6])->whereTime('create_at', '-15 days')->group('mday')->select()->column(null, 'mday');
+            for ($i = 15; $i >= 0; $i--) {
+                $date = date('Y-m-d', strtotime("-{$i}days"));
+                $orders[] = [
+                    '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                    '订单数量' => ($orderNum[$date] ?? [])['count'] ?? 0,
+                ];
+            }
+            return success("ok",compact("today","moneys","orders"));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "store",methods: "get")]
+    public function getStoreData(Request $request): Response
+    {
+        try {
+            $map = [ "agent_id" => $request->user['agent_id'] ];
+            $total = (new SaasOrderLife)->where($map)->where("status","in",[1,2,3,6])->whereDay("create_at")->field("ROUND(sum(pay_amount)/100,2) as money,count(id) as num")->find();
+            $today = [
+                "order_money"   => $total['money']??0, // 营业额
+                "order_num"     => $total['num']??0, // 订单数
+                "order_done"    => "", // 已核销
+                "order_send"    => "",  // 待发货
+                "store_num"     => (new SaasStore)->where($map)->count(), // 总店铺数量
+            ];
+            $moneys = $orders = [];
+            $fields = ['ROUND(sum(pay_amount)/100,2)' => 'total','substr(create_at,1,10)' => 'mday'];
+            $orderMoney = (new SaasOrderLife)->field($fields)->where($map)->where("status","in",[1,3,4,5])->whereTime('create_at', '-15 days')->group('mday')->select()->column(null, 'mday');
+            for ($i = 15; $i >= 0; $i--) {
+                $date = date('Y-m-d', strtotime("-{$i}days"));
+                $moneys[] = [
+                    '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                    '订单金额' => ($orderMoney[$date] ?? [])['total'] ?? 0,
+                ];
+            }
+            $field = ['count(1)' => 'count', 'substr(create_at,1,10)' => 'mday'];
+            $orderNum = (new SaasOrderLife)->field($field)->where($map)->where("status","in",[1,2,3,6])->whereTime('create_at', '-15 days')->group('mday')->select()->column(null, 'mday');
+            for ($i = 15; $i >= 0; $i--) {
+                $date = date('Y-m-d', strtotime("-{$i}days"));
+                $orders[] = [
+                    '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                    '订单数量' => ($orderNum[$date] ?? [])['count'] ?? 0,
+                ];
+            }
+            return success("ok",compact("today","moneys","orders"));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "shop",methods: "get")]
+    public function getShopData(Request $request): Response
+    {
+        try {
+            $map = [ "store_id" => $request->user['store_id'],"agent_id" => $request->user['agent_id'] ];
+            $total = (new SaasOrderLife)->where($map)->where("status","in",[1,3,4,5])->whereDay("create_at")->field("ROUND(sum(pay_amount)/100,2) as money,count(id) as num")->find();
+            $today = [
+                "order_money"   => $total['money']??0, // 营业额
+                "order_num"     => $total['num']??0, // 订单数
+                "order_done"    => "", // 已核销
+                "order_send"    => "",  // 待发货
+            ];
+            $moneys = $orders = [];
+            $fields = ['ROUND(sum(pay_amount)/100,2)' => 'total','substr(create_at,1,10)' => 'mday'];
+            $orderMoney = (new SaasOrderLife)->field($fields)->where($map)->where("status","in",[1,3,4,5])->whereTime('create_at', '-15 days')->group('mday')->select()->column(null, 'mday');
+            for ($i = 15; $i >= 0; $i--) {
+                $date = date('Y-m-d', strtotime("-{$i}days"));
+                $moneys[] = [
+                    '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                    '订单金额' => ($orderMoney[$date] ?? [])['total'] ?? 0,
+                ];
+            }
+            $field = ['count(1)' => 'count', 'substr(create_at,1,10)' => 'mday'];
+            $orderNum = (new SaasOrderLife)->field($field)->where($map)->where("status","in",[1,3,4,5])->whereTime('create_at', '-15 days')->group('mday')->select()->column(null, 'mday');
+            for ($i = 15; $i >= 0; $i--) {
+                $date = date('Y-m-d', strtotime("-{$i}days"));
+                $orders[] = [
+                    '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                    '订单数量' => ($orderNum[$date] ?? [])['count'] ?? 0,
+                ];
+            }
+            return success("ok",compact("today","moneys","orders"));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 66 - 0
app/controller/admin/Config.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\system\SystemConfig;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+use Webman\Annotation\Middleware;
+
+
+#[Controller(prefix: "/api/config"),Middleware(AuthMiddleware::class)]
+class Config extends Base
+{
+
+
+    /**
+     * 获取配置信息
+     */
+    #[Route(path: "list",methods: "get")]
+    public function getConfigList(Request $request): Response
+    {
+        try {
+            $type = $request->get("type","service");
+            $data = (new SystemConfig)->where("type",$type)->where("status",1)->field("name,value")->select()->toArray();
+            $result = [];
+            foreach ($data as $item) {
+                $result[$item['name']] = $item['value'];
+            }
+            if ($type == "sms") {
+                $result['channel'] = $this->getSmsChannel();
+                $result['login_sms'] = '您的验证码为:${code},请勿泄露于他人!';
+            }
+            return successTrans("success.data",$result);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 保存通用配置
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "save",methods: "post")]
+    public function setConfigData(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            if (isset($param['data']['channel'])) unset($param['data']['channel']);
+            foreach ($param['data'] as $k => $v){
+                if(is_array($v)) $v = implode(",",$v);
+                sConf($param['type'].'.'.$k, $v);
+            }
+            return successTrans("success.data",[]);
+        } catch (\Throwable $exception){
+            return error($exception->getMessage());
+        }
+
+    }
+
+}

+ 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());
+        }
+    }
+
+}

+ 158 - 0
app/controller/admin/Shop.php

@@ -0,0 +1,158 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\extra\service\saas\ShopService;
+use app\extra\tools\CodeExtend;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasAgent;
+use app\model\saas\SaasStore;
+use app\model\saas\SaasStoreShop;
+use app\model\system\SystemUser;
+use app\validate\saas\ShopValidate;
+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/shop"),Middleware(AuthMiddleware::class)]
+class Shop extends Base
+{
+
+    #[Inject]
+    protected ShopValidate $validate;
+
+    #[Inject]
+    protected ShopService $service;
+
+    #[Inject]
+    protected SaasAgent $model;
+
+    #[Route(path: "list",methods: "get")]
+    public function getStoreList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 新增/编辑代理
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "save",methods: "post")]
+    public function save(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            if (!isset($param['id'])) {
+                $param['agent_id'] = CodeExtend::random(16,1,date("md"));
+                if (!empty($param['username'])) {
+                    $userName = (new SystemUser)->where("username",$param['username'])->findOrEmpty();
+                    if (!$userName->isEmpty()) return errorTrans("error.user-exist");
+                }
+            }
+            if (!$this->validate->check($param)) return error($this->validate->getError());
+            $state = $this->model->setAutoData($param);
+            if (!$state) return errorTrans("error.data");
+            $this->sceneUser($param,2,"id");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            echo $throwable->getMessage()."\n";
+            echo $throwable->getFile()."\n";
+            echo $throwable->getLine()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 新增/编辑代理
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "edit",methods: "post")]
+    public function edit(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            if (!$this->validate->check($request->post())) return error($this->validate->getError());
+            $state = $this->model->setAutoData($param);
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            echo $throwable->getMessage()."\n";
+            echo $throwable->getFile()."\n";
+            echo $throwable->getLine()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "batch",methods: "post")]
+    public function setBatchData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"        => trans("empty.require"),
+                "value.require"     => trans("empty.require"),
+                "field.require"     => trans("empty.require"),
+                "type.require"      => trans("empty.require"),
+            ],"post");
+            if (!is_array($param)) return error($param);
+            if ($param['type'] == "batch") {
+                $state = $this->model->where("id","in",$param['id'])->save([$param['field'] => $param['value']]);
+            } else {
+                $state = $this->model->where("id",$param['id'])->save([$param['field'] => $param['value']]);
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 删除
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "del",methods: "post")]
+    public function delUser(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"    => trans("empty.require"),
+                "type.default"  => "one",
+            ],"post");
+            if (!is_array($param)) return error($param);
+            if ($param['type'] == "batch") {
+                $state = $this->model->where("id","in",$param['id'])->delete();
+            } else {
+                $data = $this->model->where("id",$param['id'])->findOrEmpty();
+                if ($data->isEmpty()) return errorTrans("empty.data");
+                // 删除其他相关数据
+                $state = $data->delete();
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+}

+ 65 - 0
app/controller/admin/Upload.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\extra\tools\UploadExtend;
+use app\middleware\AuthMiddleware;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+use Tinywan\Storage\Storage;
+
+
+#[Controller(prefix: "/api/upload"),Middleware(AuthMiddleware::class)]
+class Upload extends Base
+{
+
+
+    /**
+     * 上传文件
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "data",methods: "post")]
+    public function upload2image(Request $request): Response
+    {
+        try {
+            $resp = UploadExtend::uploadFile();
+            if (!isset($resp[0]['url'])) return errorTrans(40010);
+            return successTrans("success.data",[
+                "fileName"  => $resp[0]['origin_name'],
+                "src"       => $resp[0]['url'],
+            ],200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+
+    /**
+     * 上传文件
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "file",methods: "post")]
+    public function upload2file(Request $request): Response
+    {
+        try {
+            $resp = UploadExtend::disk(UploadExtend::MODE_LOCAL)->uploadFile();
+            if (!isset($resp[0]['url'])) return errorTrans(40010);
+            return successTrans("success.data",[
+                "fileName"  => $resp[0]['origin_name'],
+                "src"       => $resp[0]['url'],
+            ],200);
+        } catch (\Throwable $throwable) {
+            echo $throwable->getFile()."\n";
+            echo $throwable->getLine()."\n";
+            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());
+        }
+    }
+
+}

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

@@ -0,0 +1,145 @@
+<?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_end'])) 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_end']))
+            {
+                $userData['vip_end'] = 0;
+            } else {
+                $userData['vip_end'] = strtotime($agent['vip_end']);
+            }
+            return successTrans("success.data",[
+                "username"  => $userData['username'],
+                "truename"  => $userData['truename'],
+                "vip_at"    => $userData['vip_end'],
+                "agent_id"  => $userData['agent_id'],
+                "super"     => $userData['is_super'],
+                "type"      => $userData['type']
+            ]);
+        } catch (\Throwable $exception){
+            return error($exception->getMessage());
+        }
+    }
+
+}

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

@@ -0,0 +1,75 @@
+<?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;
+            $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());
+        }
+    }
+}

+ 89 - 0
app/controller/notify/SfExpress.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace app\controller\notify;
+
+use app\extra\basic\Base;
+use app\model\saas\SaasOrder;
+use app\model\saas\SaasOrderExpress;
+use app\model\saas\SaasOrderLog;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/sf")]
+class SfExpress extends Base
+{
+
+    protected array $opCode = [
+        50 => "已揽件",
+        30 => "运输中",
+        31 => "运输中",
+        44 => "运输中",
+        80 => "已签收"
+    ];
+
+
+    /**
+     *
+     * @param Request $request
+     * @param $id
+     * @return Response
+     */
+    #[Route(path: "hook/{id}",methods: ['get','post'])]
+    public function getHook(Request $request,$id): Response
+    {
+        try {
+            if ($request->method() == "GET") {
+                $data = $request->get();
+            } else {
+                $data = $request->post();
+            }
+            if (!empty($data['orderState'])){
+                foreach ($data['orderState'] as $val) {
+                    $order = (new SaasOrder)->where("express_id",$val['waybillNo'])->findOrEmpty();
+                    if ($order->isEmpty()) return json(['code' => 0,"success" => true,"msg" => ""]);
+                    if ($val['empCode'] == 500963) {
+                        (new SaasOrderExpress)->insertGetId(['order_id' => $order['order_sn'],'express_id' => $val['waybillNo'],'content' => "顺丰快递",'title' => "系统已接收"]);
+                        (new SaasOrderLog)->insertGetId(['order_id' => $order['order_sn'],'remark' => "顺丰快递",'title' => "系统已接收"]);
+                    }
+
+                }
+            }
+            if (!empty($data['Body']['WaybillRoute'])){
+                foreach ($data['Body']['WaybillRoute'] as $val) {
+                    $order = (new SaasOrder)->where("express_id",$val['mailno'])->with(['store' => function($query){
+                        $query->field('store_id,store_name,appid,secret,express_time,order_end');
+                    }])->findOrEmpty();
+                    if ($order->isEmpty()) return json(['code' => 0,"success" => true,"msg" => ""]);
+                    if ($order['status'] >= 2) return json(['code' => 0,"success" => true,"msg" => ""]);
+                    (new SaasOrderExpress)->insertGetId(['order_id' => $order['order_sn'],'express_id' => $val['mailno'],'content' => $val['remark'],'title' => $this->opCode[$val['opCode']]??'']);
+                    (new SaasOrderLog)->insertGetId(['order_id' => $order['order_sn'],'remark' => $val['remark'],'title' => $this->opCode[$val['opCode']]??'' ]);
+                    if ($val['opCode'] == 50 && $order['store']['order_end'] == "2") { // 已揽收,发起核销
+                        // $val['acceptAddress'] 发货地址
+                        $this->orderDone($order->toArray(),1);
+                        $order->status = 2;
+                        $order->save();
+                    }
+                    if ($val['opCode'] == 80 && $order['store']['order_end'] == "3") { // 已签收
+                        // $val['acceptAddress'] 发货地址
+                        $this->orderDone($order->toArray(),2);
+                        $order->status = 2;
+                        $order->save();
+                    }
+                }
+            }
+            return json(['code' => 0,"success" => true,"msg" => ""]);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    protected function orderDone(array $data = [],int $type = 1)
+    {
+
+    }
+
+}

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

@@ -0,0 +1,206 @@
+<?php
+
+namespace app\extra\basic;
+
+use app\extra\douyin\Client;
+use app\extra\tools\CodeExtend;
+use app\model\saas\SaasOrderLife;
+use app\model\saas\SaasOrderLog;
+use app\model\saas\SaasStore;
+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'];
+    }
+
+    protected function getDyConfig(): array
+    {
+        return ["appid" => sConf('dy.appid'),'secret' => sConf('dy.secret')];
+    }
+
+
+    /**
+     * 同步订单-实时
+     * @param string $account
+     * @param string $openId
+     */
+    protected function asyncDyOrder(string $account = "",string $openId = "",string $orderId = "")
+    {
+        // 同步已经下单的订单
+        if (!empty($account)) {
+            if (!empty($orderId)) {
+                $order = (new SaasOrderLife)->where("order_id",$orderId)->findOrEmpty();
+                if (!$order->isEmpty()) return [];
+            }
+            $resp = (new Client)->config($this->getDyConfig())->token()->queryOrder($account,$openId);
+            $store = (new SaasStore)->where("store_id",$account)->findOrEmpty();
+            if (empty($resp['data']['orders'])) return [];
+            $lifeOrder = $resp['data']['orders'][0];
+            $orderData = $orderLog = [];
+            foreach ($lifeOrder['certificates'] as $key=>$val) {
+                $orderEx = (new SaasOrderLife)->where("order_id",$lifeOrder['order_id'])->where("certificate_id",$val['certificate_id'])->findOrEmpty();
+                if ($orderEx->isEmpty()) {
+                    $orderData[$key] = [
+                        "open_id"   => $openId,
+                        "agent_id"  => $store['agent_id']??'',
+                        "store_id"  => $store['store_id']??'',
+                        "order_id"  => $lifeOrder['order_id'],
+                        "pay_amount"    => $val['amount']['pay_amount'],
+                        "order_amount"  => $val['amount']['original_amount'],
+                        "expire_at"     => date("Y-m-d H:i:s",$val['expire_time']),
+                        "out_id"        => $val['sku_info']['sku_id'],
+                        "product_name"  => $val['sku_info']['title'],
+                        "groupon_type"  => $val['sku_info']['groupon_type'],
+                        "certificate_id"  => $val['certificate_id']??'',
+                        "status"        => 1,
+                        "pay_at"    => getDateFull(),
+                        "start_time"    => date("Y-m-d H:i:s",$val['start_time']),
+                    ];
+                } else {
+                    $orderEx->save([
+                        "open_id"       => $openId,
+                        "expire_at"     => date("Y-m-d H:i:s",$val['expire_time']),
+                        "out_id"        => $val['sku_info']['sku_id'],
+                        "product_name"  => $val['sku_info']['title'],
+                        "groupon_type"  => $val['sku_info']['groupon_type'],
+                        "certificate_id"  => $val['certificate_id']??'',
+                        "start_time"    => date("Y-m-d H:i:s",$val['start_time']),
+                    ]);
+                }
+            }
+            if (!empty($orderData)) {
+                (new SaasOrderLife)->insertAll(array_values($orderData));
+            }
+            return [];
+        }
+    }
+
+    /**
+     * 写入新用户
+     * @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);
+    }
+
+}

+ 98 - 0
app/extra/service/basic/UploadService.php

@@ -0,0 +1,98 @@
+<?php
+
+namespace app\extra\service\basic;
+
+use app\model\system\SystemConfig;
+use Tinywan\Storage\Adapter\CosAdapter;
+use Tinywan\Storage\Adapter\LocalAdapter;
+use Tinywan\Storage\Adapter\QiniuAdapter;
+
+class UploadService
+{
+    /**
+     * 配置
+     * @return array
+     */
+    public function setConfigVal(string $type = ""): array
+    {
+        $data = (new SystemConfig)->where("type","storage")->column("value","name");
+        $config = [];
+        $config['storage']['default'] = $data['type'];
+        $config['storage']['include'] = !empty($data['allow_exts']) ? explode(",",$data['allow_exts']) : [];
+        $config['storage']['exclude'] = !empty($data['noallow_exts']) ? explode(",",$data['noallow_exts']) : [];
+        $config['storage']['nums'] = 10;
+        $config['storage']['single_limit'] = 1024 * 1024 * 200;
+        $config['storage']['total_limit'] = 1024 * 1024 * 200;
+        if (empty($type)) $type = $data['type'];
+        switch ($type)
+        {
+            case "local":
+                $config['storage']['local'] = [
+                    'adapter' => LocalAdapter::class,
+                    'root' => public_path().'/uploads/storage/',
+                    'dirname' => function () {
+                        return date('Ymd');
+                    },
+                    'domain' => "",
+                    'uri' => '/uploads/storage/', // 如果 domain + uri 不在 public 目录下,请做好软链接,否则生成的url无法访问
+                    'algo' => 'sha1',
+                ];
+                break;
+            case "qiniu":
+                $config['storage']['qiniu'] = [
+                    'adapter' => QiniuAdapter::class,
+                    'accessKey' => $data['qiniu_access_key'],
+                    'secretKey' => $data['qiniu_secret_key'],
+                    'bucket' => $data['qiniu_bucket'],
+                    'dirname' => 'storage/'.date("Ymd"),
+                    'domain' => $this->setDomain($data['oss_http_protocol'],$data['qiniu_http_domain']),
+                ];
+                break;
+            case "oss":
+                $config['storage']['oss'] = [
+                    'adapter' => \Tinywan\Storage\Adapter\OssAdapter::class,
+                    'accessKeyId' => $data['oss_access_key'],
+                    'accessKeySecret' => $data['oss_secret_key'],
+                    'bucket' => $data['oss_bucket'],
+                    'dirname' => function () {
+                        return 'storage/'.date("Ymd");
+                    },
+                    'domain' => $this->setDomain($data['oss_http_protocol'],$data['oss_http_domain']),
+                    'endpoint' => $data['oss_region'],
+                    'algo' => 'sha1',
+
+                ];
+                break;
+            case "cos":
+                $config['storage']['cos'] = [
+                    'adapter' => CosAdapter::class,
+                    'secretId' => $data['cos_access_key'],
+                    'secretKey' => $data['cos_secret_key'],
+                    'bucket' => $data['cos_bucket'],
+                    'dirname' => 'storage/'.date("Ymd"),
+                    'domain' => $this->setDomain($data['oss_http_protocol'],$data['cos_http_domain']),
+                    'region' => $data['cos_region'],
+                ];
+                break;
+            default:
+                break;
+        }
+        return $config;
+    }
+
+    /**
+     * 协议域名
+     * @param string $type
+     * @param string $domain
+     * @return string
+     */
+    protected function setDomain(string $type,string $domain): string
+    {
+        if ($type == "auto") {
+            return "//".$domain;
+        }
+        return $type."://".$domain;
+    }
+
+
+}

+ 37 - 0
app/extra/service/saas/ShopService.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace app\extra\service\saas;
+
+use app\extra\basic\Service;
+use app\model\saas\SaasAgent;
+
+class ShopService extends Service
+{
+
+    /**
+     * 列表
+     * @param array $param
+     */
+    public function getList(array $param = [])
+    {
+        $this->mode = new SaasAgent();
+        return $this->searchVal($param,$this->searchFilter($param))->where("is_deleted",0)->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['status']) && $filter[] = ["status", '=', ($param['status']-1)];
+        !empty($param['type']) && $filter[] = ["store_type", '=', $param['type']];
+        !empty($param['name']) && $filter[] = ["shop_name", 'like', "%{$param['name']}%"];
+        !empty($param['poi']) && $filter[] = ["poi_id", 'like', "%{$param['poi']}%"];
+        return $filter;
+    }
+
+}

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

@@ -0,0 +1,28 @@
+<?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 $hide
+     * @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 ?: '-'
+        ];
+    }
+
+}

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

@@ -0,0 +1,42 @@
+<?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))->with(['account' => function ($query) {
+            $query->field("store_id,store_name");
+        }])->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']}%"];
+        !empty($param['name']) && $filter[] = ["username", 'like', "%{$param['name']}%"];
+        !empty($param['type']) && $filter[] = ["type", '=', $param['type']];
+        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($name);
+        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);
+    }
+
+}

+ 463 - 0
app/functions.php

@@ -0,0 +1,463 @@
+<?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);
+    }
+}
+
+/**
+ * 过滤emoji表情
+ * @param string $str
+ * @return string
+ */
+if (!function_exists('removeEmoji')) {
+    function removeEmoji(string $str = '') : string
+    {
+        $str = preg_replace('/[\x{1F600}-\x{1F64F}]/u', '', $str);
+        $str = preg_replace('/[\x{1F300}-\x{1F5FF}]/u', '', $str);
+        $str = preg_replace('/[\x{1F680}-\x{1F6FF}]/u', '', $str);
+        $str = preg_replace('/[\x{2600}-\x{26FF}]/u', '', $str);
+        return preg_replace('/[\x{2700}-\x{27BF}]/u', '', $str);
+    }
+}
+
+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??'Y-m-d H:i:s',$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);
+    }
+}
+
+
+
+if(!function_exists('store_region')){
+    /**
+     * 返回附近司机数据
+     * @param $latitude
+     * @param $longitude
+     * @param $type 1 返回查询影响记录数;2 返回数据数组
+     */
+    function store_region($latitude,$longitude,$type = 1,$where = 'status = 1',$field="*"){
+        $sql = "select ".$field." from(
+SELECT id,poi_id,poi_name,status,poi_address,longitude,latitude,
+ROUND(6378.138*2*ASIN(SQRT(POW(SIN(({$latitude}*PI()/180-latitude*PI()/180)/2),2)+COS({$latitude}*PI()/180)*COS(latitude*PI()/180)*POW(SIN(({$longitude}*PI()/180-longitude*PI()/180)/2),2)))*1000) AS juli
+FROM saas_store_shop where ".$where.") as tmp_table_name order by juli asc";
+        if($type == 1){
+            return \think\facade\Db::execute($sql);
+        }
+        return \think\facade\Db::query($sql);
+    }
+}
+
+if(!function_exists('getHourlyTimeSlots')){
+    function getHourlyTimeSlots($startHour = 9, $endHour = 22, $format = 'H:i'): array
+    {
+        $slots = [];
+
+        for ($hour = $startHour; $hour < $endHour; $hour++) {
+            $startTime = sprintf('%02d:00', $hour);
+            $endTime = sprintf('%02d:00', $hour + 1);
+
+            $slots[] = [
+                'start' => $startTime,
+                'end' => $endTime,
+                'display' => $startTime . ' - ' . $endTime
+            ];
+        }
+
+        return $slots;
+    }
+}

+ 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;
+    }
+
+
+}

+ 44 - 0
app/middleware/DyMiddleware.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace app\middleware;
+
+use Shopwwi\WebmanAuth\Auth;
+use Shopwwi\WebmanAuth\JWT;
+use Webman\Http\Request;
+use Webman\Http\Response;
+use Webman\MiddlewareInterface;
+
+class DyMiddleware 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('platform','');
+                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("member")->verify();
+                $user = (new Auth)->guard("member")->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;
+    }
+}

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

@@ -0,0 +1,62 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property integer $agent_id 
+ * @property string $shop_name 
+ * @property string $start_at 
+ * @property string $end_at 营业时间-结束
+ * @property string $shop_address 地址
+ * @property mixed $rule 收费规则
+ * @property mixed $vip_end VIP到期时间
+ * @property string $shop_notice 公告
+ * @property integer $is_deleted 
+ * @property integer $shop_status 1营业2休息
+ * @property string $shop_contact 联系人
+ * @property string $shop_mobile 联系电话
+ * @property integer $user_card 充值套餐0默认1自定义
+ * @property mixed $user_card_price 会员卡充值套餐自定义金额
+ * @property integer $balance 
+ * @property integer $total_balance 
+ * @property float $cash_rate 提现费率
+ * @property integer $status 0正常1到期2冻结
+ * @property mixed $line_time 最后在线时间
+ * @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;
+
+
+}

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

@@ -0,0 +1,47 @@
+<?php
+
+namespace app\model\system;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property integer $uuid 
+ * @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;
+
+
+}

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

@@ -0,0 +1,58 @@
+<?php
+
+namespace app\model\system;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property integer $agent_id 代理ID
+ * @property string $username 用户名
+ * @property string $truename 真实姓名
+ * @property string $password 密码
+ * @property mixed $salt 密钥串
+ * @property integer $status 状态
+ * @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;
+    }
+
+}

+ 31 - 0
app/queue/redis/AutoExpress.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace app\queue\redis;
+
+use Webman\RedisQueue\Consumer;
+
+/**
+ * 自动呼叫快递
+ */
+class AutoExpress implements Consumer
+{
+
+
+    public $queue = "auto-express";
+
+    public $connection = "default";
+
+
+    /**
+     * @param $data
+     * @return bool
+     */
+    public function consume($data): bool
+    {
+        try {
+
+        } catch (\Throwable $throwable) {
+            return true;
+        }
+    }
+}

+ 32 - 0
app/queue/redis/AutoPrint.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace app\queue\redis;
+
+use Webman\RedisQueue\Consumer;
+
+/**
+ * 打印面单
+ */
+class AutoPrint implements Consumer
+{
+
+
+    public $queue = "auto-print";
+
+    public $connection = "default";
+
+
+    /**
+     * @param $data
+     * @return bool
+     */
+    public function consume($data): bool
+    {
+        try {
+
+        } catch (\Throwable $throwable) {
+            return true;
+        }
+    }
+
+}

+ 80 - 0
app/queue/redis/ExportOrder.php

@@ -0,0 +1,80 @@
+<?php
+
+namespace app\queue\redis;
+
+use app\extra\service\saas\OrderService;
+use app\model\system\SystemExport;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use Webman\RedisQueue\Consumer;
+
+class ExportOrder implements Consumer
+{
+
+    public $queue = "export-order";
+
+    public $connection = "default";
+
+
+    /**
+     * @param $data
+     * @return bool
+     */
+    public function consume($data): bool
+    {
+        try {
+            echo getDateFull()."===导出订单\n";
+            $order = (new OrderService)->getListAll($data['param']);
+            if (empty($order))
+            {
+                (new SystemExport)->where("id",$data['logId'])->save(['status' => 2]);
+                return true;
+            }
+            $title = ["订单编号","所属门店","订单类型","商品ID","商品名称","规格","订单金额","收件人","收件电话","收件地址","券码"];
+            $titCol = 'A';
+            $spreadsheet = new Spreadsheet();
+            $sheet = $spreadsheet->getActiveSheet();
+            foreach($title as $k=>$v){
+                $sheet->setCellValue($titCol."1",$v);
+                $titCol ++;
+            }
+            $row = 2;
+            foreach ($order as $item){
+                $dataCol = "A";
+                $newData = [
+                    "order_id"      => (string) $item['order_id'], // 余额
+                    "store"         => $item['store']['store_name']??'-',
+                    "type"          => $item['service_type']==1?'预约':'快递',
+                    "out_id"        => $item['product_id']??'-',
+                    "product_name"  => $item['product_name']??'-',
+                    "sku"           => $item['sku']??'-',
+                    "order_amount"  => "¥".$item['parent']['order_amount']??'-',
+                    "username"      => $item['username']??'-',
+                    "mobile"        => $item['mobile']??'-',
+                    "address"       => $item['region'].$item['address'],
+                    "certificate"   => $item['certificate_id']??'-'
+                ];
+                foreach ($newData as $value) {
+                    $sheet->setCellValue($dataCol . $row, ' '.$value);
+                    $dataCol++;
+                }
+                $row++;
+            }
+            $write = new Xlsx($spreadsheet);
+            $pathName = public_path().DIRECTORY_SEPARATOR."logs".DIRECTORY_SEPARATOR.$data['param']['fileName'];
+            (new SystemExport)->where("id",$data['logId'])->update([
+                "down_url"  => "https://file.jsshuita.com.cn/logs/".$data['param']['fileName'],
+                "status"    => 1
+            ]);
+            $write->save($pathName);
+            return true;
+        } catch (\Throwable $throwable) {
+            echo getDateFull()."导出文件报错\n";
+            echo $throwable->getMessage()."\n";
+            echo $throwable->getFile()."\n";
+            echo $throwable->getLine()."\n";
+            return true;
+        }
+    }
+
+}

+ 36 - 0
app/queue/redis/PrintState.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace app\queue\redis;
+
+use app\extra\tools\PrintService;
+use app\model\saas\SaasPrint;
+use Webman\RedisQueue\Consumer;
+
+class PrintState implements Consumer
+{
+
+    public $queue = "print-state";
+
+    public $connection = "default";
+
+    public function consume($data): bool
+    {
+        echo getDateFull()."===批量查询打印件状态\n";
+        $print = (new SaasPrint)->select();
+        $sn = [];
+        if ($print->isEmpty()) return true;
+        foreach ($print as $key => $value) {
+            $sn[$key] = $value['sn'];
+        }
+        $resp = (new PrintService)->config(['appid' => sConf('dy.ex_appid'),'secret' => sConf('dy.ex_secret')])->deviceState($sn);
+        if (!$resp['status']) return true;
+        if (empty($resp['data'])) return true;
+        foreach ($resp['data'] as $key => $val) {
+            echo getDateFull()."===更新序列号=={$val['sn']}=={$val['status']}=={$val['deviceStatus']}\n";
+            $state = (new SaasPrint)->where("sn",$val['sn'])->update(["online" => $val['status'],"device_state" => $val['deviceStatus']]);
+            echo "更新状态==={$state}\n";
+        }
+        return true;
+    }
+
+}

+ 78 - 0
app/queue/redis/SyncOrder.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace app\queue\redis;
+
+use app\extra\douyin\Client;
+use app\extra\life\OrderLife;
+use app\model\saas\SaasOrderLife;
+use app\model\saas\SaasOrderLog;
+use app\model\saas\SaasStore;
+use Webman\RedisQueue\Consumer;
+use Webman\RedisQueue\Redis;
+
+class SyncOrder implements Consumer
+{
+
+
+    public $queue = "sync-order";
+
+    public $connection = "default";
+
+
+    public function consume($data): bool
+    {
+        if (empty($data['order'])) return true;
+        if (empty($data['openid'])) return true;
+        $order = (new SaasOrderLife)->where("order_id",$data['order'])->findOrEmpty();
+        if (!$order->isEmpty()) return true;
+        $resp = (new Client)->config($this->getDyConfig())->token()->queryOrderStore($data['order']);
+        if (empty($resp['data']['certificates'])) return true;
+        echo getDateFull()."同步订单\n";
+        print_r($data);
+        echo json_encode($resp);
+        $lifeOrder = $resp['data'];
+        $orderData = [];
+        foreach ($lifeOrder['certificates'] as $key=>$val) {
+            $orderEx = (new SaasOrderLife)->where("order_id",$lifeOrder['order_id'])->where("certificate_id",$val['certificate_id'])->findOrEmpty();
+            if ($orderEx->isEmpty()) {
+                $store = (new SaasStore)->where("store_id",$val['sku_info']['account_id'])->findOrEmpty();
+                $orderData[$key] = [
+                    "open_id"   => $data['openid'],
+                    "agent_id"  => $store['agent_id']??'',
+                    "store_id"  => $store['store_id']??'',
+                    "order_id"  => $lifeOrder['order_id'],
+                    "pay_amount"    => $val['amount']['pay_amount'],
+                    "order_amount"  => $val['amount']['original_amount'],
+                    "expire_at"     => date("Y-m-d H:i:s",$val['expire_time']),
+                    "out_id"        => $val['sku_info']['sku_id'],
+                    "product_name"  => $val['sku_info']['title'],
+                    "groupon_type"  => $val['sku_info']['groupon_type'],
+                    "certificate_id"  => $val['certificate_id']??'',
+                    "status"        => 1,
+                    "pay_at"        => date("Y-m-d H:i:s",$data['pay_time']),
+                    "start_time"    => date("Y-m-d H:i:s",$val['start_time']),
+                ];
+            } else {
+                $orderEx->save([
+                    "expire_at"     => date("Y-m-d H:i:s",$val['expire_time']),
+                    "out_id"        => $val['sku_info']['sku_id'],
+                    "product_name"  => $val['sku_info']['title'],
+                    "groupon_type"  => $val['sku_info']['groupon_type'],
+                    "certificate_id"  => $val['certificate_id']??'',
+                    "start_time"    => date("Y-m-d H:i:s",$val['start_time']),
+                ]);
+            }
+        }
+        if (!empty($orderData)) {
+            (new SaasOrderLife)->insertAll(array_values($orderData));
+        }
+        return true;
+    }
+
+
+    protected function getDyConfig(): array
+    {
+        return ["appid" => sConf('dy.appid'),'secret' => sConf('dy.secret')];
+    }
+
+}

+ 32 - 0
app/validate/saas/ShopValidate.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace app\validate\saas;
+
+use think\Validate;
+
+class ShopValidate extends Validate
+{
+
+
+    protected $rule = [
+        "agent_id"      => "require",
+        "shop_name"     => "require",
+        "shop_contact"  => "require",
+        "shop_mobile"   => "require",
+        "start_at"      => "require",
+        "end_at"        => "require",
+        "cash_rate"     => "require",
+    ];
+
+
+    protected $message = [
+        "agent_id.require"      => "账户ID不能为空",
+        "shop_name.require"     => "请输入店铺名称",
+        "shop_contact.require"  => "请输入联系人",
+        "shop_mobile.require"   => "请输入联系电话",
+        "start_at.require"      => "请输入营业时间",
+        "end_at.require"        => "请输入营业时间",
+        "cash_rate.require"     => "请输入提现手续费",
+    ];
+
+}

+ 27 - 0
app/validate/saas/UserValidate.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace app\validate\saas;
+
+use think\Validate;
+
+class UserValidate extends Validate
+{
+
+    protected $rule = [
+        "agent_id"      => "require",
+        "password"      => "require",
+        "salt"          => "require",
+    ];
+
+
+    protected $message = [
+        "agent_id.require"      => "请选择代理",
+        "password.require"      => "请输入密码",
+        "salt.require"          => "数据加密失败",
+    ];
+
+    protected $scene = [
+
+    ];
+
+}

+ 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
+];

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

@@ -0,0 +1,22 @@
+<?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;
+use Webman\Route;
+// 匹配所有options路由
+Route::options('[{path:.+}]', function (){
+    return response('',204);
+});
+
+if (!AnnotationBootstrap::isIgnoreProcess()) {
+    RouteAnnotationHandle::createRoute();
+}

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

@@ -0,0 +1,77 @@
+<?php
+
+ return [
+     'enable' => true,
+     'app_key' => 'base64:N721v3Gt2I58HH7oiU7a70PQ+i8ekPWRqwI+JSnM1wo=',
+     'guard' => [
+         'admin' => [
+             'key' => 'id',
+             'field' => ['id','username'], //设置允许写入扩展中的字段
+             'num' => 2, //-1为不限制终端数量 0为只支持一个终端在线 大于0为同一账号同终端支持数量 建议设置为1 则同一账号同终端在线1个
+             'model'=> [\app\model\system\SystemUser::class,'thinkphp'] // 当为数组时 [app\model\Test::class,'thinkphp'] 来说明模型归属
+         ],
+         'member' => [
+             'key' => 'id',
+             'field' => ['id','open_id'], //设置允许写入扩展中的字段
+             'num' => 0, //-1为不限制终端数量 0为只支持一个终端在线 大于0为同一账号同终端支持数量 建议设置为1 则同一账号同终端在线1个
+             'model'=> [\app\model\saas\SaasMember::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,
+     ],
+ ];

File diff suppressed because it is too large
+ 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:8890',
+        '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' => 'file',
+    // 缓存连接方式配置
+    '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


BIN
public/logs/已预约订单_1765168684493.xlsx


+ 25 - 0
public/uploads/storage/20251210/64762e3876273e5f65d3cd11797de2779603bd85.pem

@@ -0,0 +1,25 @@
+-----BEGIN CERTIFICATE-----
+MIIEJDCCAwygAwIBAgIUZWfxTKFU1r9pyNGyCf1lbtnL0mowDQYJKoZIhvcNAQEL
+BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
+FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
+Q0EwHhcNMjUxMDEwMDgzMDA2WhcNMzAxMDA5MDgzMDA2WjB+MRMwEQYDVQQDDAox
+NzI5NDg5NDcwMRswGQYDVQQKDBLlvq7kv6HllYbmiLfns7vnu58xKjAoBgNVBAsM
+Ieays+WNl+aZuuaDoOWNsOenkeaKgOaciemZkOWFrOWPuDELMAkGA1UEBhMCQ04x
+ETAPBgNVBAcMCFNoZW5aaGVuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEArQY2xAi7bot3xSX509jysm/19KHvtR9e9uyeDL8CoK0fMcjxwa4yVWe6+Jm4
+lqyvNWHf5Y3QiwXcdv42rTl/FT7vHd+kKYqSyZSG1AL6Z+OG1v0DMxNzKQRjblQo
+9vbCtEGaJ06iHXlM9KP/NppL3jjqAG+tEZ7mQt4F6JYZJEzOoWKpZnp26fEg23GC
+R+Cbs7JIjsUNKv1ShIWRX4DCSwGhnBr9a8Cc7LSTiRcr7K5ps7RSuqCeTmJ/XbEM
+AjSxnpl9hEqazZg/0vSJp6ZyIU10lZfUMXvMSek6Pq71npN3cH1JF+RbtBsTwYA1
+Wma6p91ZLccHtn3F2+yDTuaUmwIDAQABo4G5MIG2MAkGA1UdEwQCMAAwCwYDVR0P
+BAQDAgP4MIGbBgNVHR8EgZMwgZAwgY2ggYqggYeGgYRodHRwOi8vZXZjYS5pdHJ1
+cy5jb20uY24vcHVibGljL2l0cnVzY3JsP0NBPTFCRDQyMjBFNTBEQkMwNEIwNkFE
+Mzk3NTQ5ODQ2QzAxQzNFOEVCRDImc2c9SEFDQzQ3MUI2NTQyMkUxMkIyN0E5RDMz
+QTg3QUQxQ0RGNTkyNkUxNDAzNzEwDQYJKoZIhvcNAQELBQADggEBABiypr2vO/YW
+XJACZzcWhDQwCLUD+7hO5ksj28nGyuYU0CIdexlzY4iM9OEiEoZj2qkJHQQF6jLt
+xOcdc9PfjbLiLw+i8F05Xt8ZJTOpxt7BnkzqZfLdaxM+Y70iokAClbTN6pSQnpu7
+7EMCDXrrDzgVMJ7dHn1IiPaJajb2GxLYh9ANXWXSCBXT2dSxt3YI5azq2DYwgV+Y
+KIqvfEFx4VDQKa62HrHyvBKPa1ZcznCUpPMFxk5X/OlhXhyR/ysu5oiDxP1gZRV3
+9rjCTciWkLATD2ekwIdx7WT5Dy+jMt1b5Dg1FDrNzPR4sqW3brjAlV8hq6ksnFhc
+lICPcAwhVFM=
+-----END CERTIFICATE-----

+ 28 - 0
public/uploads/storage/20251210/bd1a4e35f31b7d8deb0f03ac4aa6f99d29db9314.pem

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCtBjbECLtui3fF
+JfnT2PKyb/X0oe+1H1727J4MvwKgrR8xyPHBrjJVZ7r4mbiWrK81Yd/ljdCLBdx2
+/jatOX8VPu8d36QpipLJlIbUAvpn44bW/QMzE3MpBGNuVCj29sK0QZonTqIdeUz0
+o/82mkveOOoAb60RnuZC3gXolhkkTM6hYqlmenbp8SDbcYJH4JuzskiOxQ0q/VKE
+hZFfgMJLAaGcGv1rwJzstJOJFyvsrmmztFK6oJ5OYn9dsQwCNLGemX2ESprNmD/S
+9ImnpnIhTXSVl9Qxe8xJ6To+rvWek3dwfUkX5Fu0GxPBgDVaZrqn3Vktxwe2fcXb
+7INO5pSbAgMBAAECggEAOzb+NTb71ohatBkcGBmObe0NUuy82dLjsEQojxor7wtw
+upwXyP4x8bKvdXc+iiqNSAq4EprnPT0DR6IW2k/sIzxHn4SzbRgkxmJThmmCg12F
+/zvWBXvplKcQ1GhvIJ4w4nn1JPCvYMDrymn55RkHUAVQaFfP9lDYYxgTE5eTAS3M
+FlzbnehFBkF+Jc5Dfc+Dr1eDWAnTzYPcLHPSdy9L3vdTvtkaI9zdzcpnVnYbNWkJ
+Z+rjL2KXRUeqdK7Ai9Kp7Ip77LcMBXBpPHjZkvo2A0MuWqw8Ko12zAZxPqpAVp/a
+tc5cCLsMu/hp39+orAk/FuVWfh4B/wSiA/NgX4ozgQKBgQDT7k5+v5ivjBQ5G1ym
+R85gHe798/8HnEWHNEf4xheFFFy+VsMm9U3gtL2C4jZNAMHrZTBI4MqBnVh7jcrb
+3qda6nKpUrVNYC5VzOFlwQErgYJqm0eK/USL+tzf2kbdavEGe8k9rtb0mYqMVyZU
+nUwnwFBhy/Y2dD6ZZoGkXcILWwKBgQDRAMvB+ojyLH4KZjFykflD34FP1lQo1WhM
++Hf0rJyflgELybAT8v7U+qLrzFsyOZeFEG7L1i+/n/zVwOM9nKxC9Jy0inxPKwyK
+psHW2TDyU/I9ArIvFIkMxRkwxrsVjhwHUExXNVXi7oo1HmLdoE7Hc7UJJe7nI4N/
+Cs7i4XQfwQKBgQDJ7cpE4nGs8h9iujtxBAITevH4br+UlMV6qcnZs4U9e8VSZoDT
+Ye+uJwha6QcsH4ilrWhwSB8rmKxyLQwYqvFyouhVhUTSUM5VWj15Iojm1yNYSFPG
+jsL9TS2e7O+QkRDOSKvaZfjSXmmwhmkzPh0N0yPDyv7xq5jpuM3Vuq/k+QKBgAZf
+/tcQ3EJ+xu1sRo/XEgJW++vCftisAb1vSsFkznYzrh587Wj+XXWDm5qTpih3Bz8g
+zDBdfSFcMOsYMhY8BCkaqvj6zGXhy0UjZwA1qb+KffYMcgDroG6KSIVrWmOC7Snv
+8hQq5U7Bted+4Mcfz6dXySrZRFs6gxVJ8BuNguxBAoGBAKAVT5d00K+nVATgEnqM
+71KIsgQtcCtyj9qVSyys0SS5w9M8TjuPOLMgIh6rSr3wDDP25IGI/0jSBMoUQz+T
+/pPttOss/am1LJHyIP17denlPxFJGXAHFK+X9qHdop56HMrDbs2tkZPegYcCgjD+
+CGgu2iJPyQJak6+UOAasuw9r
+-----END PRIVATE KEY-----

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

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

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

@@ -0,0 +1,42 @@
+<?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" => "登录成功",
+        "print" => "已成功发起打印测试,请留意打印机是否正常工作",
+        "print-state" => "已成功发起状态查询任务,请等待1-3分钟再刷新数据"
+    ],
+];

+ 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);
+    }
+}

Some files were not shown because too many files changed in this diff