zory 2 days ago
parent
commit
ab1e7afd40
76 changed files with 4535 additions and 1 deletions
  1. 20 0
      .gitignore
  2. 18 0
      Dockerfile
  3. 21 0
      LICENSE
  4. 22 1
      README.md
  5. 133 0
      app/command/Model.php
  6. 50 0
      app/command/ShopwwiAuthCommand.php
  7. 42 0
      app/controller/IndexController.php
  8. 495 0
      app/functions.php
  9. 42 0
      app/middleware/StaticFile.php
  10. 29 0
      app/model/Test.php
  11. 10 0
      app/process/Http.php
  12. 305 0
      app/process/Monitor.php
  13. 14 0
      app/view/index/view.html
  14. 88 0
      composer.json
  15. 26 0
      config/app.php
  16. 21 0
      config/autoload.php
  17. 18 0
      config/bootstrap.php
  18. 31 0
      config/cache.php
  19. 15 0
      config/container.php
  20. 29 0
      config/database.php
  21. 15 0
      config/dependence.php
  22. 5 0
      config/event.php
  23. 17 0
      config/exception.php
  24. 32 0
      config/log.php
  25. 15 0
      config/middleware.php
  26. 32 0
      config/plugin/hhink/webman-sms/app.php
  27. 8 0
      config/plugin/hzdad/codecheck/app.php
  28. 26 0
      config/plugin/isszz/webman-hashids/app.php
  29. 48 0
      config/plugin/linfly/annotation/annotation.php
  30. 4 0
      config/plugin/linfly/annotation/app.php
  31. 19 0
      config/plugin/linfly/annotation/bootstrap.php
  32. 19 0
      config/plugin/linfly/annotation/middleware.php
  33. 17 0
      config/plugin/linfly/annotation/route.php
  34. 5 0
      config/plugin/luckycmc/webman-province-city-area/app.php
  35. 9 0
      config/plugin/luckycmc/webman-province-city-area/command.php
  36. 71 0
      config/plugin/shopwwi/auth/app.php
  37. 76 0
      config/plugin/tinywan/storage/app.php
  38. 28 0
      config/plugin/webman/console/app.php
  39. 4 0
      config/plugin/webman/event/app.php
  40. 17 0
      config/plugin/webman/event/bootstrap.php
  41. 7 0
      config/plugin/webman/event/command.php
  42. 4 0
      config/plugin/webman/redis-queue/app.php
  43. 7 0
      config/plugin/webman/redis-queue/command.php
  44. 32 0
      config/plugin/webman/redis-queue/log.php
  45. 11 0
      config/plugin/webman/redis-queue/process.php
  46. 21 0
      config/plugin/webman/redis-queue/redis.php
  47. 8 0
      config/plugin/webman/validation/app.php
  48. 7 0
      config/plugin/webman/validation/command.php
  49. 9 0
      config/plugin/webman/validation/middleware.php
  50. 5 0
      config/plugin/x2nx/webman-migrate/app.php
  51. 44 0
      config/plugin/x2nx/webman-migrate/command.php
  52. 24 0
      config/plugin/x2nx/webman-migrate/phinx.php
  53. 62 0
      config/process.php
  54. 29 0
      config/redis.php
  55. 21 0
      config/route.php
  56. 23 0
      config/server.php
  57. 65 0
      config/session.php
  58. 23 0
      config/static.php
  59. 38 0
      config/think-cache.php
  60. 42 0
      config/think-orm.php
  61. 25 0
      config/translation.php
  62. 22 0
      config/view.php
  63. 37 0
      database/factories/UsersFactory.php
  64. 33 0
      database/migrations/2025_01_07_213244_users.php
  65. 29 0
      database/migrations/2026_01_14_120641_create_province_city_area_table.php
  66. 40 0
      database/seeders/DatabaseSeeder.php
  67. 11 0
      docker-compose.yml
  68. BIN
      public/favicon.ico
  69. 5 0
      start.php
  70. 24 0
      support/Request.php
  71. 24 0
      support/Response.php
  72. 1558 0
      support/Setup.php
  73. 139 0
      support/bootstrap.php
  74. 71 0
      webman
  75. 3 0
      windows.bat
  76. 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

+ 18 - 0
Dockerfile

@@ -0,0 +1,18 @@
+FROM php:8.3.22-cli-alpine
+
+RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
+
+RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
+  && apk update --no-cache \
+  && docker-php-source extract
+
+# install extensions
+RUN docker-php-ext-install pdo pdo_mysql -j$(nproc) pcntl
+
+# enable opcache and pcntl
+RUN docker-php-ext-enable opcache pcntl
+RUN docker-php-source delete \
+    rm -rf /var/cache/apk/*
+
+RUN mkdir -p /app
+WORKDIR /app

+ 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.

+ 22 - 1
README.md

@@ -1,2 +1,23 @@
-# jiaoyi-api
+# 交易小程序
 
+composer require yzh52521/easyhttp
+composer require "linqiao/php-addtran:^0.1"
+composer require webman/event
+composer require phpoffice/phpspreadsheet
+composer require workerman/crontab
+composer require vlucas/phpdotenv
+composer require webman/console
+composer require webman/redis-queue
+composer require -W webman/think-cache
+composer require kkokk/poster
+composer require isszz/webman-hashids
+composer require linfly/annotation
+composer require hhink/webman-sms
+composer require hzdad/codecheck
+composer require php-di/php-di
+composer require shopwwi/webman-auth
+composer require tinywan/storage
+composer require aliyuncs/oss-sdk-php
+composer require qcloud/cos-sdk-v5
+composer require qiniu/php-sdk
+composer require luckycmc/webman-province-city-area

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

+ 50 - 0
app/command/ShopwwiAuthCommand.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace app\command;
+
+use Shopwwi\WebmanAuth\Facade\Str;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+
+class ShopwwiAuthCommand extends Command
+{
+    protected static $defaultName = 'shopwwi:auth';
+    protected static $defaultDescription = 'shopwwi auth';
+
+    /**
+     * @return void
+     */
+    protected function configure()
+    {
+        $this->addArgument('name', InputArgument::OPTIONAL, 'Name description');
+    }
+
+    /**
+     * @param InputInterface $input
+     * @param OutputInterface $output
+     * @return int
+     */
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $name = $input->getArgument('name');
+        $output->writeln('生成jwtKey 开始');
+        $key = Str::random(64);
+        file_put_contents(base_path()."/config/plugin/shopwwi/auth/app.php", str_replace(
+            "'access_secret_key' => '".config('plugin.shopwwi.auth.app.jwt.access_secret_key')."'",
+            "'access_secret_key' => '".$key."'",
+            file_get_contents(base_path()."/config/plugin/shopwwi/auth/app.php")
+        ));
+        file_put_contents(base_path()."/config/plugin/shopwwi/auth/app.php", str_replace(
+            "'refresh_secret_key' => '".config('plugin.shopwwi.auth.app.jwt.refresh_secret_key')."'",
+            "'refresh_secret_key' => '".$key."'",
+            file_get_contents(base_path()."/config/plugin/shopwwi/auth/app.php")
+        ));
+        $output->writeln('生成jwtKey 结束'.$key);
+        return self::SUCCESS;
+    }
+
+}

+ 42 - 0
app/controller/IndexController.php

@@ -0,0 +1,42 @@
+<?php
+
+namespace app\controller;
+
+use support\Request;
+
+class IndexController
+{
+    public function index(Request $request)
+    {
+        return <<<EOF
+<style>
+  * {
+    padding: 0;
+    margin: 0;
+  }
+  iframe {
+    border: none;
+    overflow: scroll;
+  }
+</style>
+<iframe
+  src="https://www.workerman.net/wellcome"
+  width="100%"
+  height="100%"
+  allow="clipboard-write"
+  sandbox="allow-scripts allow-same-origin allow-popups allow-downloads"
+></iframe>
+EOF;
+    }
+
+    public function view(Request $request)
+    {
+        return view('index/view', ['name' => 'webman']);
+    }
+
+    public function json(Request $request)
+    {
+        return json(['code' => 0, 'msg' => 'ok']);
+    }
+
+}

+ 495 - 0
app/functions.php

@@ -0,0 +1,495 @@
+<?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;
+    }
+}
+
+if (!function_exists("strToUniqueNumberV4"))
+{
+    /**
+     * 名称加密
+     * @param string $str
+     * @param string $salt
+     * @return string
+     */
+    function strToUniqueNumberV4(string $str = "", string $salt = ''): string
+    {
+        // 添加盐值增加唯一性
+        $str = $str . $salt;
+        // 使用多种哈希组合
+        $crc = abs(crc32($str));
+        $md5 = hexdec(substr(md5($str), 0, 8));
+        $sha1 = hexdec(substr(sha1($str), 0, 8));
+        // 组合并取模
+        $number = ($crc + $md5 + $sha1) % 1000000000000;
+        // 使用时间戳微调确保唯一性(针对同一字符串)
+        static $lastStr = '';
+        static $counter = 0;
+        if ($str === $lastStr) {
+            $counter++;
+            $number = ($number + $counter) % 1000000000000;
+        } else {
+            $lastStr = $str;
+            $counter = 0;
+        }
+        return str_pad($number, 12, '0', STR_PAD_LEFT);
+    }
+}

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

+ 29 - 0
app/model/Test.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace app\model;
+
+use support\Model;
+
+class Test extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected $table = 'test';
+
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected $primaryKey = 'id';
+
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public $timestamps = false;
+}

+ 10 - 0
app/process/Http.php

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

+ 305 - 0
app/process/Monitor.php

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

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

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

+ 88 - 0
composer.json

@@ -0,0 +1,88 @@
+{
+  "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",
+    "webman/console": "^2.2",
+    "webman/database": "^2.1",
+    "webman/think-orm": "^2.1",
+    "illuminate/pagination": "^12.55",
+    "illuminate/events": "^12.55",
+    "symfony/var-dumper": "^7.4",
+    "webman/redis": "^2.1",
+    "webman/validation": "^2.2",
+    "yzh52521/easyhttp": "^1.1",
+    "webman/event": "^1.0",
+    "phpoffice/phpspreadsheet": "^5.5",
+    "workerman/crontab": "^1.0",
+    "vlucas/phpdotenv": "^5.6",
+    "webman/redis-queue": "^2.1",
+    "webman/think-cache": "^2.1",
+    "kkokk/poster": "^3.0",
+    "isszz/webman-hashids": "^0.0.2",
+    "linfly/annotation": "^2.1",
+    "hhink/webman-sms": "^1.0",
+    "hzdad/codecheck": "^1.0",
+    "php-di/php-di": "^7.1",
+    "shopwwi/webman-auth": "^2.0",
+    "tinywan/storage": "^1.1",
+    "aliyuncs/oss-sdk-php": "^2.7",
+    "qcloud/cos-sdk-v5": "^2.6",
+    "qiniu/php-sdk": "^7.14",
+    "luckycmc/webman-province-city-area": "^1.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"
+    ],
+    "post-create-project-cmd": [
+      "support\\Setup::run"
+    ],
+    "setup-webman": [
+      "support\\Setup::run"
+    ]
+  },
+  "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,
+];

+ 31 - 0
config/cache.php

@@ -0,0 +1,31 @@
+<?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' => 'file',
+    'stores' => [
+        'file' => [
+            'driver' => 'file',
+            'path' => runtime_path('cache')
+        ],
+        'redis' => [
+            'driver' => 'redis',
+            'connection' => 'default'
+        ],
+        'array' => [
+            'driver' => 'array'
+        ]
+    ]
+];

+ 15 - 0
config/container.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 new Webman\Container;

+ 29 - 0
config/database.php

@@ -0,0 +1,29 @@
+<?php
+return  [
+    'default' => 'mysql',
+    'connections' => [
+        'mysql' => [
+            'driver'      => 'mysql',
+            'host'        => '127.0.0.1',
+            'port'        => '3306',
+            'database'    => 'your_database',
+            'username'    => 'your_username',
+            'password'    => 'your_password',
+            'charset'     => 'utf8mb4',
+            'collation'   => 'utf8mb4_general_ci',
+            'prefix'      => '',
+            'strict'      => true,
+            'engine'      => null,
+            'options'   => [
+                PDO::ATTR_EMULATE_PREPARES => false, // Must be false for Swoole and Swow drivers.
+            ],
+            'pool' => [
+                'max_connections' => 5,
+                'min_connections' => 1,
+                'wait_timeout' => 3,
+                'idle_timeout' => 60,
+                'heartbeat_interval' => 50,
+            ],
+        ],
+    ],
+];

+ 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,//验证后删除
+];

+ 26 - 0
config/plugin/isszz/webman-hashids/app.php

@@ -0,0 +1,26 @@
+<?php
+
+return [
+    'enable'  => true,
+    
+    // 默认连接名称
+    'default' => 'main', // 支持bilibili的BV模式
+
+    // Hashids modes
+    'modes' => [
+        'main' => [
+            'salt' => '',
+            'length' => 0,
+        ],
+        'other' => [
+            'salt' => 'salt',
+            'length' => 0,
+            'alphabet' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
+        ],
+        'bilibili' => [
+            // 此模式无需添加其他的配置
+            // 前缀超过2位英文字母忽略
+            'prefix' => '', // B站BV模式前缀类似: BV1fx411v7eo = 12345678
+        ],
+    ],
+];

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

@@ -0,0 +1,48 @@
+<?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',
+    ],
+    // 忽略的进程名称
+    'ignore_process' => [
+        'monitor'
+    ],
+    // 路由设置
+    '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]);
+        },
+
+        // 注解验证器 @Validate() 未填写验证器类名时则通过命名空间拼接规则获取验证器类名
+        'auto_validate' => true,
+        // 验证器类名后缀
+        'auto_validate_suffix' => 'Validate',
+        // 自动验证器验证处理
+        // 'auto_validate_handle' => function (array $item): string {
+        //     return str_replace('\\controller\\', '\\validate\\', $item['class']);
+        // }
+    ],
+];

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

+ 19 - 0
config/plugin/linfly/annotation/middleware.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
+ */
+
+return [
+    '' => [
+        LinFly\Annotation\Validate\ValidateMiddleware::class
+    ]
+];

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

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

+ 5 - 0
config/plugin/luckycmc/webman-province-city-area/app.php

@@ -0,0 +1,5 @@
+<?php
+return [
+    'enable' => true,
+    'dataFileName' => 'province-city-area.json',
+];

+ 9 - 0
config/plugin/luckycmc/webman-province-city-area/command.php

@@ -0,0 +1,9 @@
+<?php
+
+use Luckycmc\WebmanProvinceCityArea\Commands\ClearData;
+use Luckycmc\WebmanProvinceCityArea\Commands\RefreshData;
+
+return [
+    ClearData::class,
+    RefreshData::class
+];

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

@@ -0,0 +1,71 @@
+<?php
+
+ return [
+     'enable' => true,
+     'app_key' => 'base64:N721v3Gt2I58HH7oiU7a70PQ+i8ekPWRqwI+JSnM1wo=',
+     'guard' => [
+         'user' => [
+             'key' => 'id',
+             'field' => ['id','name','email','mobile'], //设置允许写入扩展中的字段
+             'num' => 0, //-1为不限制终端数量 0为只支持一个终端在线 大于0为同一账号同终端支持数量 建议设置为1 则同一账号同终端在线1个
+             'model'=> app\model\Test::class // 当为数组时 [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,
+     ],
+ ];

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

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

@@ -0,0 +1,28 @@
+<?php
+return [
+    'enable' => true,
+
+    'build_dir'  => BASE_PATH . DIRECTORY_SEPARATOR . 'build',
+
+    'phar_filename' => 'webman.phar',
+
+    'phar_format' => Phar::PHAR, // Phar archive format: Phar::PHAR, Phar::TAR, Phar::ZIP
+
+    'phar_compression' => Phar::NONE, // Compression method for Phar archive: Phar::NONE, Phar::GZ, Phar::BZ2
+
+    '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
+];

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

+ 8 - 0
config/plugin/webman/validation/app.php

@@ -0,0 +1,8 @@
+<?php
+
+use support\validation\ValidationException;
+
+return [
+    'enable' => true,
+    'exception' => ValidationException::class,
+];

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

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

+ 9 - 0
config/plugin/webman/validation/middleware.php

@@ -0,0 +1,9 @@
+<?php
+
+use Webman\Validation\Middleware;
+
+return [
+    '@' => [
+        Middleware::class,
+    ],
+];

+ 5 - 0
config/plugin/x2nx/webman-migrate/app.php

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

+ 44 - 0
config/plugin/x2nx/webman-migrate/command.php

@@ -0,0 +1,44 @@
+<?php
+
+$commands = [];
+
+if (config('plugin.x2nx.webman-migrate.app.driver') === 'phinx') {
+    $commands = array_merge($commands, [
+        X2nx\WebmanMigrate\Commands\Phinx\Breakpoint::class,
+        X2nx\WebmanMigrate\Commands\Phinx\Create::class,
+        X2nx\WebmanMigrate\Commands\Phinx\Init::class,
+        X2nx\WebmanMigrate\Commands\Phinx\ListAliases::class,
+        X2nx\WebmanMigrate\Commands\Phinx\Migrate::class,
+        X2nx\WebmanMigrate\Commands\Phinx\Rollback::class,
+        X2nx\WebmanMigrate\Commands\Phinx\SeedCreate::class,
+        X2nx\WebmanMigrate\Commands\Phinx\SeedRun::class,
+        X2nx\WebmanMigrate\Commands\Phinx\Status::class,
+        X2nx\WebmanMigrate\Commands\Phinx\Test::class,
+    ]);
+}
+
+if (config('plugin.x2nx.webman-migrate.app.driver') === 'migrate') {
+    $commands = array_merge($commands, [
+        X2nx\WebmanMigrate\Commands\Migrate\FactoryMakeCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\MigrateFreshCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\MigrateInstallCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\MigrateCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\MigrateMakeCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\MigrateRefreshCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\MigrateResetCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\MigrateRollbackCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\MigrateStatusCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\DbSeedCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\DbSeedMakeCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\DbCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\SchemaDumpCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\DbMonitorCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\ModelPruneCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\DbShowCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\ModelShowCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\DbTableCommand::class,
+        X2nx\WebmanMigrate\Commands\Migrate\DbWipeCommand::class,
+    ]);
+}
+
+return $commands;

+ 24 - 0
config/plugin/x2nx/webman-migrate/phinx.php

@@ -0,0 +1,24 @@
+<?php
+
+return [
+    "paths" => [
+        "migrations"    => "database/migrations",
+        "seeds"         => "database/seeders"
+    ],
+    "environments" => [
+        "default_migration_table" => "migrations",
+        "default_environment"     => "development",
+        "development" => [
+            "adapter" => config('database.default'),
+            "host"    => config(sprintf('database.connections.%s.host', config('database.default'))),
+            "name"    => config(sprintf('database.connections.%s.database', config('database.default'))),
+            "user"    => config(sprintf('database.connections.%s.username', config('database.default'))),
+            "pass"    => config(sprintf('database.connections.%s.password', config('database.default'))),
+            "port"    => config(sprintf('database.connections.%s.port', config('database.default'))),
+            "charset" => config(sprintf('database.connections.%s.charset', config('database.default'))),
+            "table_prefix" => config(sprintf('database.connections.%s.prefix', config('database.default'))),
+            "table_suffix" => config(sprintf('database.connections.%s.suffix', config('database.default'))),
+        ],
+        'version_order' => 'creation'
+    ]
+];

+ 62 - 0
config/process.php

@@ -0,0 +1,62 @@
+<?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\Log;
+use support\Request;
+use app\process\Http;
+
+global $argv;
+
+return [
+    'jiaoyi' => [
+        'handler' => Http::class,
+        'listen' => 'http://0.0.0.0:9881',
+        '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;
+
+
+
+
+
+

+ 23 - 0
config/server.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
+ */
+
+return [
+    'event_loop' => '',
+    'stop_timeout' => 2,
+    'pid_file' => runtime_path() . '/webman.pid',
+    'status_file' => runtime_path() . '/webman.status',
+    'stdout_file' => runtime_path() . '/logs/stdout.log',
+    'log_file' => runtime_path() . '/logs/workerman.log',
+    'max_package_size' => 10 * 1024 * 1024
+];

+ 65 - 0
config/session.php

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

+ 23 - 0
config/static.php

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

+ 38 - 0
config/think-cache.php

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

+ 42 - 0
config/think-orm.php

@@ -0,0 +1,42 @@
+<?php
+
+return [
+    'default' => 'mysql',
+    'connections' => [
+        'mysql' => [
+            // 数据库类型
+            'type' => 'mysql',
+            // 服务器地址
+            'hostname' => '127.0.0.1',
+            // 数据库名
+            'database' => 'test',
+            // 数据库用户名
+            'username' => 'root',
+            // 数据库密码
+            'password' => '123456',
+            // 数据库连接端口
+            'hostport' => '3306',
+            // 数据库连接参数
+            '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' =>  '',
+];

+ 25 - 0
config/translation.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
+ */
+
+/**
+ * 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
+];

+ 37 - 0
database/factories/UsersFactory.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace Database\Factories;
+
+use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;
+
+/**
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\app\model\Users>
+ */
+class UsersFactory extends Factory
+{
+    /**
+     * Define the model's default state.
+     *
+     * @return array<string, mixed>
+     */
+    public function definition(): array
+    {
+        return [
+            'name' => uniqid() . mt_rand(1,100),
+            'email' => uniqid() . mt_rand(1,100) . '@example.com',
+            'email_verified_at' => time(),
+            'password' => uniqid() . mt_rand(1,100),
+            'remember_token' => Str::random(10),
+        ];
+    }
+    /**
+     * Indicate that the model's email address should be unverified.
+     */
+    public function unverified(): static
+    {
+        return $this->state(fn (array $attributes) => [
+            'email_verified_at' => null,
+        ]);
+    }
+}

+ 33 - 0
database/migrations/2025_01_07_213244_users.php

@@ -0,0 +1,33 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use support\Db;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Db::schema()->create('users', function (Blueprint $table) {
+            $table->id();
+            $table->string('name');
+            $table->string('email')->unique();
+            $table->timestamp('email_verified_at')->nullable();
+            $table->string('password');
+            $table->rememberToken();
+            $table->timestamps();
+            $table->softDeletes();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Db::schema()->dropIfExists('users');
+    }
+};

+ 29 - 0
database/migrations/2026_01_14_120641_create_province_city_area_table.php

@@ -0,0 +1,29 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use support\Db;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Db::schema()->create('province_city_area', function (Blueprint $table) {
+            $table->increments('id');
+            $table->string('name')->default('')->comment('省市县名称');
+            $table->string('parent_id')->default(0)->comment('父级id');
+            $table->enum('type', ['province', 'city', 'area', 'street'])->default('province')->comment('类型');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Db::schema()->dropIfExists('province_city_area');
+    }
+};

+ 40 - 0
database/seeders/DatabaseSeeder.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace Database\Seeders;
+
+use http\Client\Curl\User;
+use Illuminate\Database\Eloquent\Factories\Factory;
+
+use app\model\Users;
+// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
+use Illuminate\Database\Seeder;
+use Illuminate\Support\Str;
+use support\Db;
+
+class DatabaseSeeder extends Seeder
+{
+    public function __construct()
+    {
+        Factory::guessFactoryNamesUsing(function (string $modelName) {
+            return 'Database\\Factories\\' . class_basename($modelName) . 'Factory';
+        });
+        Factory::guessModelNamesUsing(function (Factory $factory) {
+            return 'app\\model\\' . str_replace('Factory', '', class_basename($factory));
+        });
+    }
+
+    /**
+     * Seed the application's database.
+     */
+    public function run(): void
+    {
+        /*
+         * [
+            'name' => 'admin',
+            'email' => Str::random(10).'@qq.com',
+            'password' => ('<PASSWORD>')
+        ]
+         */
+        Users::factory(10)->create();
+    }
+}

+ 11 - 0
docker-compose.yml

@@ -0,0 +1,11 @@
+version: "3"
+services:
+  webman:
+    build: .
+    container_name: docker-webman
+    restart: unless-stopped
+    volumes:
+      - "./:/app"
+    ports:
+      - "8787:8787"
+    command: ["php", "start.php", "start" ]

BIN
public/favicon.ico


+ 5 - 0
start.php

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

+ 24 - 0
support/Request.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 Request
+ * @package support
+ */
+class Request extends \Webman\Http\Request
+{
+
+}

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

+ 1558 - 0
support/Setup.php

@@ -0,0 +1,1558 @@
+<?php
+
+declare(strict_types=1);
+
+namespace support;
+
+use Composer\Console\Application as ComposerApplication;
+use Composer\IO\IOInterface;
+use Composer\Script\Event;
+use Symfony\Component\Console\Cursor;
+use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Output\ConsoleOutput;
+use Symfony\Component\Console\Terminal;
+
+/**
+ * create-project setup wizard: interactive locale, timezone and optional components selection, then runs composer require.
+ */
+class Setup
+{
+    // --- Optional component package names ---
+
+    private const PACKAGE_CONSOLE           = 'webman/console';
+    private const PACKAGE_DATABASE          = 'webman/database';
+    private const PACKAGE_THINK_ORM         = 'webman/think-orm';
+    private const PACKAGE_REDIS             = 'webman/redis';
+    private const PACKAGE_ILLUMINATE_EVENTS = 'illuminate/events';
+    private const PACKAGE_ILLUMINATE_PAGINATION = 'illuminate/pagination';
+    private const PACKAGE_SYMFONY_VAR_DUMPER     = 'symfony/var-dumper';
+    private const PACKAGE_VALIDATION             = 'webman/validation';
+    private const PACKAGE_BLADE                  = 'webman/blade';
+    private const PACKAGE_TWIG                   = 'twig/twig';
+    private const PACKAGE_THINK_TEMPLATE         = 'topthink/think-template';
+
+    private const SETUP_TITLE = 'Webman Setup';
+
+    // --- Timezone regions ---
+
+    private const TIMEZONE_REGIONS = [
+        'Asia'       => \DateTimeZone::ASIA,
+        'Europe'     => \DateTimeZone::EUROPE,
+        'America'    => \DateTimeZone::AMERICA,
+        'Africa'     => \DateTimeZone::AFRICA,
+        'Australia'  => \DateTimeZone::AUSTRALIA,
+        'Pacific'    => \DateTimeZone::PACIFIC,
+        'Atlantic'   => \DateTimeZone::ATLANTIC,
+        'Indian'     => \DateTimeZone::INDIAN,
+        'Antarctica' => \DateTimeZone::ANTARCTICA,
+        'Arctic'     => \DateTimeZone::ARCTIC,
+        'UTC'        => \DateTimeZone::UTC,
+    ];
+
+    // --- Locale => default timezone ---
+
+    private const LOCALE_DEFAULT_TIMEZONES = [
+        'zh_CN' => 'Asia/Shanghai',
+        'zh_TW' => 'Asia/Taipei',
+        'en'    => 'UTC',
+        'ja'    => 'Asia/Tokyo',
+        'ko'    => 'Asia/Seoul',
+        'fr'    => 'Europe/Paris',
+        'de'    => 'Europe/Berlin',
+        'es'    => 'Europe/Madrid',
+        'pt_BR' => 'America/Sao_Paulo',
+        'ru'    => 'Europe/Moscow',
+        'vi'    => 'Asia/Ho_Chi_Minh',
+        'tr'    => 'Europe/Istanbul',
+        'id'    => 'Asia/Jakarta',
+        'th'    => 'Asia/Bangkok',
+    ];
+
+    // --- Locale options (localized display names) ---
+
+    private const LOCALE_LABELS = [
+        'zh_CN' => '简体中文',
+        'zh_TW' => '繁體中文',
+        'en'    => 'English',
+        'ja'    => '日本語',
+        'ko'    => '한국어',
+        'fr'    => 'Français',
+        'de'    => 'Deutsch',
+        'es'    => 'Español',
+        'pt_BR' => 'Português (Brasil)',
+        'ru'    => 'Русский',
+        'vi'    => 'Tiếng Việt',
+        'tr'    => 'Türkçe',
+        'id'    => 'Bahasa Indonesia',
+        'th'    => 'ไทย',
+    ];
+
+    // --- Multilingual messages (%s = placeholder) ---
+
+    private const MESSAGES = [
+        'zh_CN' => [
+            'remove_package_question' => '发现以下已安装组件本次未选择,是否将其卸载 ?%s',
+            'removing_package'   => '- 准备移除组件 %s',
+            'removing'           => '卸载:',
+            'error_remove'       => '卸载组件出错,请手动执行:composer remove %s',
+            'done_remove'        => '已卸载组件。',
+                        'skip'             => '非交互模式,跳过安装向导。',
+            'default_choice'   => ' (默认 %s)',
+            'timezone_prompt'  => '时区 (默认 %s,输入可联想补全): ',
+            'timezone_title'   => '时区设置 (默认 %s)',
+            'timezone_help'    => '输入关键字Tab自动补全,可↑↓下选择:',
+            'timezone_region'  => '选择时区区域',
+            'timezone_city'    => '选择时区',
+            'timezone_invalid' => '无效的时区,已使用默认值 %s',
+            'timezone_input_prompt'  => '输入时区或关键字:',
+            'timezone_pick_prompt'   => '请输入数字编号或关键字:',
+            'timezone_no_match'      => '未找到匹配的时区,请重试。',
+            'timezone_invalid_index' => '无效的编号,请重新输入。',
+            'yes'              => '是',
+            'no'               => '否',
+            'adding_package'   => '- 添加依赖 %s',
+            'console_question' => '安装命令行组件 webman/console',
+            'db_question'      => '数据库组件',
+            'db_none'          => '不安装',
+            'db_invalid'       => '请输入有效选项',
+            'redis_question'       => '安装 Redis 组件 webman/redis',
+            'events_note'          => '  (Redis 依赖 illuminate/events,已自动包含)',
+            'validation_question'  => '安装验证器组件 webman/validation',
+            'template_question'    => '模板引擎',
+            'template_none'        => '不安装',
+            'no_components'        => '未选择额外组件。',
+            'installing'           => '即将安装:',
+            'running'              => '执行:',
+            'error_install'        => '安装可选组件时出错,请手动执行:composer require %s',
+            'done'                 => '可选组件安装完成。',
+            'summary_locale'       => '语言:%s',
+            'summary_timezone'     => '时区:%s',
+        ],
+        'zh_TW' => [
+            'skip'             => '非交互模式,跳過安裝嚮導。',
+            'default_choice'   => ' (預設 %s)',
+            'timezone_prompt'  => '時區 (預設 %s,輸入可聯想補全): ',
+            'timezone_title'   => '時區設定 (預設 %s)',
+            'timezone_help'    => '輸入關鍵字Tab自動補全,可↑↓上下選擇:',
+            'timezone_region'  => '選擇時區區域',
+            'timezone_city'    => '選擇時區',
+            'timezone_invalid' => '無效的時區,已使用預設值 %s',
+            'timezone_input_prompt'  => '輸入時區或關鍵字:',
+            'timezone_pick_prompt'   => '請輸入數字編號或關鍵字:',
+            'timezone_no_match'      => '未找到匹配的時區,請重試。',
+            'timezone_invalid_index' => '無效的編號,請重新輸入。',
+            'yes'              => '是',
+            'no'               => '否',
+            'adding_package'   => '- 新增依賴 %s',
+            'console_question' => '安裝命令列組件 webman/console',
+            'db_question'      => '資料庫組件',
+            'db_none'          => '不安裝',
+            'db_invalid'       => '請輸入有效選項',
+            'redis_question'       => '安裝 Redis 組件 webman/redis',
+            'events_note'          => '  (Redis 依賴 illuminate/events,已自動包含)',
+            'validation_question'  => '安裝驗證器組件 webman/validation',
+            'template_question'    => '模板引擎',
+            'template_none'        => '不安裝',
+            'no_components'        => '未選擇額外組件。',
+            'installing'           => '即將安裝:',
+            'running'              => '執行:',
+            'error_install'        => '安裝可選組件時出錯,請手動執行:composer require %s',
+            'done'                 => '可選組件安裝完成。',
+            'summary_locale'       => '語言:%s',
+            'summary_timezone'     => '時區:%s',
+        ],
+        'en' => [
+            'skip'             => 'Non-interactive mode, skipping setup wizard.',
+            'default_choice'   => ' (default %s)',
+            'timezone_prompt'  => 'Timezone (default=%s, type to autocomplete): ',
+            'timezone_title'   => 'Timezone (default=%s)',
+            'timezone_help'    => 'Type keyword then press Tab to autocomplete, use ↑↓ to choose:',
+            'timezone_region'  => 'Select timezone region',
+            'timezone_city'    => 'Select timezone',
+            'timezone_invalid' => 'Invalid timezone, using default %s',
+            'timezone_input_prompt'  => 'Enter timezone or keyword:',
+            'timezone_pick_prompt'   => 'Enter number or keyword:',
+            'timezone_no_match'      => 'No matching timezone found, please try again.',
+            'timezone_invalid_index' => 'Invalid number, please try again.',
+            'yes'              => 'yes',
+            'no'               => 'no',
+            'adding_package'   => '- Adding package %s',
+            'console_question' => 'Install console component webman/console',
+            'db_question'      => 'Database component',
+            'db_none'          => 'None',
+            'db_invalid'       => 'Please enter a valid option',
+            'redis_question'       => 'Install Redis component webman/redis',
+            'events_note'          => '  (Redis requires illuminate/events, automatically included)',
+            'validation_question'  => 'Install validator component webman/validation',
+            'template_question'    => 'Template engine',
+            'template_none'        => 'None',
+            'no_components'        => 'No optional components selected.',
+            'installing'           => 'Installing:',
+            'running'              => 'Running:',
+            'error_install'        => 'Failed to install. Try manually: composer require %s',
+            'done'                 => 'Optional components installed.',
+            'summary_locale'       => 'Language: %s',
+            'summary_timezone'     => 'Timezone: %s',
+        ],
+        'ja' => [
+            'skip'             => '非対話モードのため、セットアップウィザードをスキップします。',
+            'default_choice'   => ' (デフォルト %s)',
+            'timezone_prompt'  => 'タイムゾーン (デフォルト=%s、入力で補完): ',
+            'timezone_title'   => 'タイムゾーン (デフォルト=%s)',
+            'timezone_help'    => 'キーワード入力→Tabで補完、↑↓で選択:',
+            'timezone_region'  => 'タイムゾーンの地域を選択',
+            'timezone_city'    => 'タイムゾーンを選択',
+            'timezone_invalid' => '無効なタイムゾーンです。デフォルト %s を使用します',
+            'timezone_input_prompt'  => 'タイムゾーンまたはキーワードを入力:',
+            'timezone_pick_prompt'   => '番号またはキーワードを入力:',
+            'timezone_no_match'      => '一致するタイムゾーンが見つかりません。再試行してください。',
+            'timezone_invalid_index' => '無効な番号です。もう一度入力してください。',
+            'yes'              => 'はい',
+            'no'               => 'いいえ',
+            'adding_package'   => '- パッケージを追加 %s',
+            'console_question' => 'コンソールコンポーネント webman/console をインストール',
+            'db_question'      => 'データベースコンポーネント',
+            'db_none'          => 'インストールしない',
+            'db_invalid'       => '有効なオプションを入力してください',
+            'redis_question'       => 'Redis コンポーネント webman/redis をインストール',
+            'events_note'          => '  (Redis は illuminate/events が必要です。自動的に含まれます)',
+            'validation_question'  => 'バリデーションコンポーネント webman/validation をインストール',
+            'template_question'    => 'テンプレートエンジン',
+            'template_none'        => 'インストールしない',
+            'no_components'        => 'オプションコンポーネントが選択されていません。',
+            'installing'           => 'インストール中:',
+            'running'              => '実行中:',
+            'error_install'        => 'インストールに失敗しました。手動で実行してください:composer require %s',
+            'done'                 => 'オプションコンポーネントのインストールが完了しました。',
+            'summary_locale'       => '言語:%s',
+            'summary_timezone'     => 'タイムゾーン:%s',
+        ],
+        'ko' => [
+            'skip'             => '비대화형 모드입니다. 설치 마법사를 건너뜁니다.',
+            'default_choice'   => ' (기본값 %s)',
+            'timezone_prompt'  => '시간대 (기본값=%s, 입력하여 자동완성): ',
+            'timezone_title'   => '시간대 (기본값=%s)',
+            'timezone_help'    => '키워드 입력 후 Tab 자동완성, ↑↓로 선택:',
+            'timezone_region'  => '시간대 지역 선택',
+            'timezone_city'    => '시간대 선택',
+            'timezone_invalid' => '잘못된 시간대입니다. 기본값 %s 을(를) 사용합니다',
+            'timezone_input_prompt'  => '시간대 또는 키워드 입력:',
+            'timezone_pick_prompt'   => '번호 또는 키워드 입력:',
+            'timezone_no_match'      => '일치하는 시간대를 찾을 수 없습니다. 다시 시도하세요.',
+            'timezone_invalid_index' => '잘못된 번호입니다. 다시 입력하세요.',
+            'yes'              => '예',
+            'no'               => '아니오',
+            'adding_package'   => '- 패키지 추가 %s',
+            'console_question' => '콘솔 컴포넌트 webman/console 설치',
+            'db_question'      => '데이터베이스 컴포넌트',
+            'db_none'          => '설치 안 함',
+            'db_invalid'       => '유효한 옵션을 입력하세요',
+            'redis_question'       => 'Redis 컴포넌트 webman/redis 설치',
+            'events_note'          => '  (Redis는 illuminate/events가 필요합니다. 자동으로 포함됩니다)',
+            'validation_question'  => '검증 컴포넌트 webman/validation 설치',
+            'template_question'    => '템플릿 엔진',
+            'template_none'        => '설치 안 함',
+            'no_components'        => '선택된 추가 컴포넌트가 없습니다.',
+            'installing'           => '설치 예정:',
+            'running'              => '실행 중:',
+            'error_install'        => '설치에 실패했습니다. 수동으로 실행하세요: composer require %s',
+            'done'                 => '선택 컴포넌트 설치가 완료되었습니다.',
+            'summary_locale'       => '언어: %s',
+            'summary_timezone'     => '시간대: %s',
+        ],
+        'fr' => [
+            'skip'             => 'Mode non interactif, assistant d\'installation ignoré.',
+            'default_choice'   => ' (défaut %s)',
+            'timezone_prompt'  => 'Fuseau horaire (défaut=%s, tapez pour compléter) : ',
+            'timezone_title'   => 'Fuseau horaire (défaut=%s)',
+            'timezone_help'    => 'Tapez un mot-clé, Tab pour compléter, ↑↓ pour choisir :',
+            'timezone_region'  => 'Sélectionnez la région du fuseau horaire',
+            'timezone_city'    => 'Sélectionnez le fuseau horaire',
+            'timezone_invalid' => 'Fuseau horaire invalide, utilisation de %s par défaut',
+            'timezone_input_prompt'  => 'Entrez un fuseau horaire ou un mot-clé :',
+            'timezone_pick_prompt'   => 'Entrez un numéro ou un mot-clé :',
+            'timezone_no_match'      => 'Aucun fuseau horaire correspondant, veuillez réessayer.',
+            'timezone_invalid_index' => 'Numéro invalide, veuillez réessayer.',
+            'yes'              => 'oui',
+            'no'               => 'non',
+            'adding_package'   => '- Ajout du paquet %s',
+            'console_question' => 'Installer le composant console webman/console',
+            'db_question'      => 'Composant base de données',
+            'db_none'          => 'Aucun',
+            'db_invalid'       => 'Veuillez entrer une option valide',
+            'redis_question'       => 'Installer le composant Redis webman/redis',
+            'events_note'          => '  (Redis nécessite illuminate/events, inclus automatiquement)',
+            'validation_question'  => 'Installer le composant de validation webman/validation',
+            'template_question'    => 'Moteur de templates',
+            'template_none'        => 'Aucun',
+            'no_components'        => 'Aucun composant optionnel sélectionné.',
+            'installing'           => 'Installation en cours :',
+            'running'              => 'Exécution :',
+            'error_install'        => 'Échec de l\'installation. Essayez manuellement : composer require %s',
+            'done'                 => 'Composants optionnels installés.',
+            'summary_locale'       => 'Langue : %s',
+            'summary_timezone'     => 'Fuseau horaire : %s',
+        ],
+        'de' => [
+            'skip'             => 'Nicht-interaktiver Modus, Einrichtungsassistent übersprungen.',
+            'default_choice'   => ' (Standard %s)',
+            'timezone_prompt'  => 'Zeitzone (Standard=%s, Eingabe zur Vervollständigung): ',
+            'timezone_title'   => 'Zeitzone (Standard=%s)',
+            'timezone_help'    => 'Stichwort tippen, Tab ergänzt, ↑↓ auswählen:',
+            'timezone_region'  => 'Zeitzone Region auswählen',
+            'timezone_city'    => 'Zeitzone auswählen',
+            'timezone_invalid' => 'Ungültige Zeitzone, Standardwert %s wird verwendet',
+            'timezone_input_prompt'  => 'Zeitzone oder Stichwort eingeben:',
+            'timezone_pick_prompt'   => 'Nummer oder Stichwort eingeben:',
+            'timezone_no_match'      => 'Keine passende Zeitzone gefunden, bitte erneut versuchen.',
+            'timezone_invalid_index' => 'Ungültige Nummer, bitte erneut eingeben.',
+            'yes'              => 'ja',
+            'no'               => 'nein',
+            'adding_package'   => '- Paket hinzufügen %s',
+            'console_question' => 'Konsolen-Komponente webman/console installieren',
+            'db_question'      => 'Datenbank-Komponente',
+            'db_none'          => 'Keine',
+            'db_invalid'       => 'Bitte geben Sie eine gültige Option ein',
+            'redis_question'       => 'Redis-Komponente webman/redis installieren',
+            'events_note'          => '  (Redis benötigt illuminate/events, automatisch eingeschlossen)',
+            'validation_question'  => 'Validierungs-Komponente webman/validation installieren',
+            'template_question'    => 'Template-Engine',
+            'template_none'        => 'Keine',
+            'no_components'        => 'Keine optionalen Komponenten ausgewählt.',
+            'installing'           => 'Installation:',
+            'running'              => 'Ausführung:',
+            'error_install'        => 'Installation fehlgeschlagen. Manuell ausführen: composer require %s',
+            'done'                 => 'Optionale Komponenten installiert.',
+            'summary_locale'       => 'Sprache: %s',
+            'summary_timezone'     => 'Zeitzone: %s',
+        ],
+        'es' => [
+            'skip'             => 'Modo no interactivo, asistente de instalación omitido.',
+            'default_choice'   => ' (predeterminado %s)',
+            'timezone_prompt'  => 'Zona horaria (predeterminado=%s, escriba para autocompletar): ',
+            'timezone_title'   => 'Zona horaria (predeterminado=%s)',
+            'timezone_help'    => 'Escriba una palabra clave, Tab autocompleta, use ↑↓ para elegir:',
+            'timezone_region'  => 'Seleccione la región de zona horaria',
+            'timezone_city'    => 'Seleccione la zona horaria',
+            'timezone_invalid' => 'Zona horaria inválida, usando valor predeterminado %s',
+            'timezone_input_prompt'  => 'Ingrese zona horaria o palabra clave:',
+            'timezone_pick_prompt'   => 'Ingrese número o palabra clave:',
+            'timezone_no_match'      => 'No se encontró zona horaria coincidente, intente de nuevo.',
+            'timezone_invalid_index' => 'Número inválido, intente de nuevo.',
+            'yes'              => 'sí',
+            'no'               => 'no',
+            'adding_package'   => '- Agregando paquete %s',
+            'console_question' => 'Instalar componente de consola webman/console',
+            'db_question'      => 'Componente de base de datos',
+            'db_none'          => 'Ninguno',
+            'db_invalid'       => 'Por favor ingrese una opción válida',
+            'redis_question'       => 'Instalar componente Redis webman/redis',
+            'events_note'          => '  (Redis requiere illuminate/events, incluido automáticamente)',
+            'validation_question'  => 'Instalar componente de validación webman/validation',
+            'template_question'    => 'Motor de plantillas',
+            'template_none'        => 'Ninguno',
+            'no_components'        => 'No se seleccionaron componentes opcionales.',
+            'installing'           => 'Instalando:',
+            'running'              => 'Ejecutando:',
+            'error_install'        => 'Error en la instalación. Intente manualmente: composer require %s',
+            'done'                 => 'Componentes opcionales instalados.',
+            'summary_locale'       => 'Idioma: %s',
+            'summary_timezone'     => 'Zona horaria: %s',
+        ],
+        'pt_BR' => [
+            'skip'             => 'Modo não interativo, assistente de instalação ignorado.',
+            'default_choice'   => ' (padrão %s)',
+            'timezone_prompt'  => 'Fuso horário (padrão=%s, digite para autocompletar): ',
+            'timezone_title'   => 'Fuso horário (padrão=%s)',
+            'timezone_help'    => 'Digite uma palavra-chave, Tab autocompleta, use ↑↓ para escolher:',
+            'timezone_region'  => 'Selecione a região do fuso horário',
+            'timezone_city'    => 'Selecione o fuso horário',
+            'timezone_invalid' => 'Fuso horário inválido, usando padrão %s',
+            'timezone_input_prompt'  => 'Digite fuso horário ou palavra-chave:',
+            'timezone_pick_prompt'   => 'Digite número ou palavra-chave:',
+            'timezone_no_match'      => 'Nenhum fuso horário encontrado, tente novamente.',
+            'timezone_invalid_index' => 'Número inválido, tente novamente.',
+            'yes'              => 'sim',
+            'no'               => 'não',
+            'adding_package'   => '- Adicionando pacote %s',
+            'console_question' => 'Instalar componente de console webman/console',
+            'db_question'      => 'Componente de banco de dados',
+            'db_none'          => 'Nenhum',
+            'db_invalid'       => 'Por favor, digite uma opção válida',
+            'redis_question'       => 'Instalar componente Redis webman/redis',
+            'events_note'          => '  (Redis requer illuminate/events, incluído automaticamente)',
+            'validation_question'  => 'Instalar componente de validação webman/validation',
+            'template_question'    => 'Motor de templates',
+            'template_none'        => 'Nenhum',
+            'no_components'        => 'Nenhum componente opcional selecionado.',
+            'installing'           => 'Instalando:',
+            'running'              => 'Executando:',
+            'error_install'        => 'Falha na instalação. Tente manualmente: composer require %s',
+            'done'                 => 'Componentes opcionais instalados.',
+            'summary_locale'       => 'Idioma: %s',
+            'summary_timezone'     => 'Fuso horário: %s',
+        ],
+        'ru' => [
+            'skip'             => 'Неинтерактивный режим, мастер установки пропущен.',
+            'default_choice'   => ' (по умолчанию %s)',
+            'timezone_prompt'  => 'Часовой пояс (по умолчанию=%s, введите для автодополнения): ',
+            'timezone_title'   => 'Часовой пояс (по умолчанию=%s)',
+            'timezone_help'    => 'Введите ключевое слово, Tab для автодополнения, ↑↓ для выбора:',
+            'timezone_region'  => 'Выберите регион часового пояса',
+            'timezone_city'    => 'Выберите часовой пояс',
+            'timezone_invalid' => 'Неверный часовой пояс, используется значение по умолчанию %s',
+            'timezone_input_prompt'  => 'Введите часовой пояс или ключевое слово:',
+            'timezone_pick_prompt'   => 'Введите номер или ключевое слово:',
+            'timezone_no_match'      => 'Совпадающий часовой пояс не найден, попробуйте снова.',
+            'timezone_invalid_index' => 'Неверный номер, попробуйте снова.',
+            'yes'              => 'да',
+            'no'               => 'нет',
+            'adding_package'   => '- Добавление пакета %s',
+            'console_question' => 'Установить консольный компонент webman/console',
+            'db_question'      => 'Компонент базы данных',
+            'db_none'          => 'Не устанавливать',
+            'db_invalid'       => 'Пожалуйста, введите допустимый вариант',
+            'redis_question'       => 'Установить компонент Redis webman/redis',
+            'events_note'          => '  (Redis требует illuminate/events, автоматически включён)',
+            'validation_question'  => 'Установить компонент валидации webman/validation',
+            'template_question'    => 'Шаблонизатор',
+            'template_none'        => 'Не устанавливать',
+            'no_components'        => 'Дополнительные компоненты не выбраны.',
+            'installing'           => 'Установка:',
+            'running'              => 'Выполнение:',
+            'error_install'        => 'Ошибка установки. Выполните вручную: composer require %s',
+            'done'                 => 'Дополнительные компоненты установлены.',
+            'summary_locale'       => 'Язык: %s',
+            'summary_timezone'     => 'Часовой пояс: %s',
+        ],
+        'vi' => [
+            'skip'             => 'Chế độ không tương tác, bỏ qua trình hướng dẫn cài đặt.',
+            'default_choice'   => ' (mặc định %s)',
+            'timezone_prompt'  => 'Múi giờ (mặc định=%s, nhập để tự động hoàn thành): ',
+            'timezone_title'   => 'Múi giờ (mặc định=%s)',
+            'timezone_help'    => 'Nhập từ khóa, Tab để tự hoàn thành, dùng ↑↓ để chọn:',
+            'timezone_region'  => 'Chọn khu vực múi giờ',
+            'timezone_city'    => 'Chọn múi giờ',
+            'timezone_invalid' => 'Múi giờ không hợp lệ, sử dụng mặc định %s',
+            'timezone_input_prompt'  => 'Nhập múi giờ hoặc từ khóa:',
+            'timezone_pick_prompt'   => 'Nhập số thứ tự hoặc từ khóa:',
+            'timezone_no_match'      => 'Không tìm thấy múi giờ phù hợp, vui lòng thử lại.',
+            'timezone_invalid_index' => 'Số không hợp lệ, vui lòng thử lại.',
+            'yes'              => 'có',
+            'no'               => 'không',
+            'adding_package'   => '- Thêm gói %s',
+            'console_question' => 'Cài đặt thành phần console webman/console',
+            'db_question'      => 'Thành phần cơ sở dữ liệu',
+            'db_none'          => 'Không cài đặt',
+            'db_invalid'       => 'Vui lòng nhập tùy chọn hợp lệ',
+            'redis_question'       => 'Cài đặt thành phần Redis webman/redis',
+            'events_note'          => '  (Redis cần illuminate/events, đã tự động bao gồm)',
+            'validation_question'  => 'Cài đặt thành phần xác thực webman/validation',
+            'template_question'    => 'Công cụ mẫu',
+            'template_none'        => 'Không cài đặt',
+            'no_components'        => 'Không có thành phần tùy chọn nào được chọn.',
+            'installing'           => 'Đang cài đặt:',
+            'running'              => 'Đang thực thi:',
+            'error_install'        => 'Cài đặt thất bại. Thử thủ công: composer require %s',
+            'done'                 => 'Các thành phần tùy chọn đã được cài đặt.',
+            'summary_locale'       => 'Ngôn ngữ: %s',
+            'summary_timezone'     => 'Múi giờ: %s',
+        ],
+        'tr' => [
+            'skip'             => 'Etkileşimsiz mod, kurulum sihirbazı atlanıyor.',
+            'default_choice'   => ' (varsayılan %s)',
+            'timezone_prompt'  => 'Saat dilimi (varsayılan=%s, otomatik tamamlama için yazın): ',
+            'timezone_title'   => 'Saat dilimi (varsayılan=%s)',
+            'timezone_help'    => 'Anahtar kelime yazın, Tab tamamlar, ↑↓ ile seçin:',
+            'timezone_region'  => 'Saat dilimi bölgesini seçin',
+            'timezone_city'    => 'Saat dilimini seçin',
+            'timezone_invalid' => 'Geçersiz saat dilimi, varsayılan %s kullanılıyor',
+            'timezone_input_prompt'  => 'Saat dilimi veya anahtar kelime girin:',
+            'timezone_pick_prompt'   => 'Numara veya anahtar kelime girin:',
+            'timezone_no_match'      => 'Eşleşen saat dilimi bulunamadı, tekrar deneyin.',
+            'timezone_invalid_index' => 'Geçersiz numara, tekrar deneyin.',
+            'yes'              => 'evet',
+            'no'               => 'hayır',
+            'adding_package'   => '- Paket ekleniyor %s',
+            'console_question' => 'Konsol bileşeni webman/console yüklensin mi',
+            'db_question'      => 'Veritabanı bileşeni',
+            'db_none'          => 'Yok',
+            'db_invalid'       => 'Lütfen geçerli bir seçenek girin',
+            'redis_question'       => 'Redis bileşeni webman/redis yüklensin mi',
+            'events_note'          => '  (Redis, illuminate/events gerektirir, otomatik olarak dahil edildi)',
+            'validation_question'  => 'Doğrulama bileşeni webman/validation yüklensin mi',
+            'template_question'    => 'Şablon motoru',
+            'template_none'        => 'Yok',
+            'no_components'        => 'İsteğe bağlı bileşen seçilmedi.',
+            'installing'           => 'Yükleniyor:',
+            'running'              => 'Çalıştırılıyor:',
+            'error_install'        => 'Yükleme başarısız. Manuel olarak deneyin: composer require %s',
+            'done'                 => 'İsteğe bağlı bileşenler yüklendi.',
+            'summary_locale'       => 'Dil: %s',
+            'summary_timezone'     => 'Saat dilimi: %s',
+        ],
+        'id' => [
+            'skip'             => 'Mode non-interaktif, melewati wizard instalasi.',
+            'default_choice'   => ' (default %s)',
+            'timezone_prompt'  => 'Zona waktu (default=%s, ketik untuk melengkapi): ',
+            'timezone_title'   => 'Zona waktu (default=%s)',
+            'timezone_help'    => 'Ketik kata kunci, Tab untuk melengkapi, gunakan ↑↓ untuk memilih:',
+            'timezone_region'  => 'Pilih wilayah zona waktu',
+            'timezone_city'    => 'Pilih zona waktu',
+            'timezone_invalid' => 'Zona waktu tidak valid, menggunakan default %s',
+            'timezone_input_prompt'  => 'Masukkan zona waktu atau kata kunci:',
+            'timezone_pick_prompt'   => 'Masukkan nomor atau kata kunci:',
+            'timezone_no_match'      => 'Zona waktu tidak ditemukan, silakan coba lagi.',
+            'timezone_invalid_index' => 'Nomor tidak valid, silakan coba lagi.',
+            'yes'              => 'ya',
+            'no'               => 'tidak',
+            'adding_package'   => '- Menambahkan paket %s',
+            'console_question' => 'Instal komponen konsol webman/console',
+            'db_question'      => 'Komponen database',
+            'db_none'          => 'Tidak ada',
+            'db_invalid'       => 'Silakan masukkan opsi yang valid',
+            'redis_question'       => 'Instal komponen Redis webman/redis',
+            'events_note'          => '  (Redis memerlukan illuminate/events, otomatis disertakan)',
+            'validation_question'  => 'Instal komponen validasi webman/validation',
+            'template_question'    => 'Mesin template',
+            'template_none'        => 'Tidak ada',
+            'no_components'        => 'Tidak ada komponen opsional yang dipilih.',
+            'installing'           => 'Menginstal:',
+            'running'              => 'Menjalankan:',
+            'error_install'        => 'Instalasi gagal. Coba manual: composer require %s',
+            'done'                 => 'Komponen opsional terinstal.',
+            'summary_locale'       => 'Bahasa: %s',
+            'summary_timezone'     => 'Zona waktu: %s',
+        ],
+        'th' => [
+            'skip'             => 'โหมดไม่โต้ตอบ ข้ามตัวช่วยติดตั้ง',
+            'default_choice'   => ' (ค่าเริ่มต้น %s)',
+            'timezone_prompt'  => 'เขตเวลา (ค่าเริ่มต้น=%s พิมพ์เพื่อเติมอัตโนมัติ): ',
+            'timezone_title'   => 'เขตเวลา (ค่าเริ่มต้น=%s)',
+            'timezone_help'    => 'พิมพ์คีย์เวิร์ดแล้วกด Tab เพื่อเติมอัตโนมัติ ใช้ ↑↓ เพื่อเลือก:',
+            'timezone_region'  => 'เลือกภูมิภาคเขตเวลา',
+            'timezone_city'    => 'เลือกเขตเวลา',
+            'timezone_invalid' => 'เขตเวลาไม่ถูกต้อง ใช้ค่าเริ่มต้น %s',
+            'timezone_input_prompt'  => 'ป้อนเขตเวลาหรือคำค้น:',
+            'timezone_pick_prompt'   => 'ป้อนหมายเลขหรือคำค้น:',
+            'timezone_no_match'      => 'ไม่พบเขตเวลาที่ตรงกัน กรุณาลองอีกครั้ง',
+            'timezone_invalid_index' => 'หมายเลขไม่ถูกต้อง กรุณาลองอีกครั้ง',
+            'yes'              => 'ใช่',
+            'no'               => 'ไม่',
+            'adding_package'   => '- เพิ่มแพ็กเกจ %s',
+            'console_question' => 'ติดตั้งคอมโพเนนต์คอนโซล webman/console',
+            'db_question'      => 'คอมโพเนนต์ฐานข้อมูล',
+            'db_none'          => 'ไม่ติดตั้ง',
+            'db_invalid'       => 'กรุณาป้อนตัวเลือกที่ถูกต้อง',
+            'redis_question'       => 'ติดตั้งคอมโพเนนต์ Redis webman/redis',
+            'events_note'          => '  (Redis ต้องการ illuminate/events รวมไว้โดยอัตโนมัติ)',
+            'validation_question'  => 'ติดตั้งคอมโพเนนต์ตรวจสอบ webman/validation',
+            'template_question'    => 'เทมเพลตเอนจิน',
+            'template_none'        => 'ไม่ติดตั้ง',
+            'no_components'        => 'ไม่ได้เลือกคอมโพเนนต์เสริม',
+            'installing'           => 'กำลังติดตั้ง:',
+            'running'              => 'กำลังดำเนินการ:',
+            'error_install'        => 'ติดตั้งล้มเหลว ลองด้วยตนเอง: composer require %s',
+            'done'                 => 'คอมโพเนนต์เสริมติดตั้งเรียบร้อยแล้ว',
+            'summary_locale'       => 'ภาษา: %s',
+            'summary_timezone'     => 'เขตเวลา: %s',
+        ],
+    ];
+
+    // --- Interrupt message (Ctrl+C) ---
+
+    private const INTERRUPTED_MESSAGES = [
+        'zh_CN' => '安装中断,可运行 composer setup-webman 可重新设置。',
+        'zh_TW' => '安裝中斷,可運行 composer setup-webman 重新設置。',
+        'en'    => 'Setup interrupted. Run "composer setup-webman" to restart setup.',
+        'ja'    => 'セットアップが中断されました。composer setup-webman を実行して再設定できます。',
+        'ko'    => '설치가 중단되었습니다. composer setup-webman 을 실행하여 다시 설정할 수 있습니다.',
+        'fr'    => 'Installation interrompue. Exécutez « composer setup-webman » pour recommencer.',
+        'de'    => 'Einrichtung abgebrochen. Führen Sie "composer setup-webman" aus, um neu zu starten.',
+        'es'    => 'Instalación interrumpida. Ejecute "composer setup-webman" para reiniciar.',
+        'pt_BR' => 'Instalação interrompida. Execute "composer setup-webman" para reiniciar.',
+        'ru'    => 'Установка прервана. Выполните «composer setup-webman» для повторной настройки.',
+        'vi'    => 'Cài đặt bị gián đoạn. Chạy "composer setup-webman" để cài đặt lại.',
+        'tr'    => 'Kurulum kesildi. Yeniden kurmak için "composer setup-webman" komutunu çalıştırın.',
+        'id'    => 'Instalasi terganggu. Jalankan "composer setup-webman" untuk mengatur ulang.',
+        'th'    => 'การติดตั้งถูกขัดจังหวะ เรียกใช้ "composer setup-webman" เพื่อตั้งค่าใหม่',
+    ];
+
+    // --- Signal handling state ---
+
+    /** @var string|null Saved stty mode for terminal restoration on interrupt */
+    private static ?string $sttyMode = null;
+
+    /** @var string Current locale for interrupt message */
+    private static string $interruptLocale = 'en';
+
+    // ═══════════════════════════════════════════════════════════════
+    // Entry
+    // ═══════════════════════════════════════════════════════════════
+
+    public static function run(Event $event): void
+    {
+        $io = $event->getIO();
+
+        // Non-interactive mode: use English for skip message
+        if (!$io->isInteractive()) {
+            $io->write('<comment>' . self::MESSAGES['en']['skip'] . '</comment>');
+            return;
+        }
+
+        try {
+            self::doRun($event, $io);
+        } catch (\Throwable $e) {
+            $io->writeError('');
+            $io->writeError('<error>Setup wizard error: ' . $e->getMessage() . '</error>');
+            $io->writeError('<comment>Run "composer setup-webman" to retry.</comment>');
+        }
+    }
+
+    private static function doRun(Event $event, IOInterface $io): void
+    {
+        $io->write('');
+
+        // Register Ctrl+C handler
+        self::registerInterruptHandler();
+
+        // Banner title (must be before locale selection)
+        self::renderTitle();
+
+        // 1. Locale selection
+        $locale = self::askLocale($io);
+        self::$interruptLocale = $locale;
+        $defaultTimezone = self::LOCALE_DEFAULT_TIMEZONES[$locale] ?? 'UTC';
+        $msg = fn(string $key, string ...$args): string =>
+        empty($args) ? self::MESSAGES[$locale][$key] : sprintf(self::MESSAGES[$locale][$key], ...$args);
+
+        // Write locale config (update when not default)
+        if ($locale !== 'zh_CN') {
+            self::updateConfig($event, 'config/translation.php', "'locale'", $locale);
+        }
+
+        $io->write('');
+        $io->write('');
+
+        // 2. Timezone selection (default by locale)
+        $timezone = self::askTimezone($io, $msg, $defaultTimezone);
+        if ($timezone !== 'Asia/Shanghai') {
+            self::updateConfig($event, 'config/app.php', "'default_timezone'", $timezone);
+        }
+
+        // 3. Optional components
+        $packages = self::askComponents($io, $msg);
+
+        // 4. Remove unselected components
+        $removePackages = self::askRemoveComponents($event, $packages, $io, $msg);
+
+        // 5. Summary
+        $io->write('');
+        $io->write('─────────────────────────────────────');
+        $io->write('<info>' . $msg('summary_locale', self::LOCALE_LABELS[$locale]) . '</info>');
+        $io->write('<info>' . $msg('summary_timezone', $timezone) . '</info>');
+
+        // Remove unselected packages first to avoid dependency conflicts
+        if ($removePackages !== []) {
+            $io->write('');
+            $io->write('<info>' . $msg('removing') . '</info>');
+
+            $secondaryPackages = [
+                self::PACKAGE_ILLUMINATE_EVENTS,
+                self::PACKAGE_ILLUMINATE_PAGINATION,
+                self::PACKAGE_SYMFONY_VAR_DUMPER,
+            ];
+            $displayRemovePackages = array_diff($removePackages, $secondaryPackages);
+            foreach ($displayRemovePackages as $pkg) {
+                $io->write('  - ' . $pkg);
+            }
+            $io->write('');
+            self::runComposerRemove($removePackages, $io, $msg);
+        }
+
+        // Then install selected packages
+        if ($packages !== []) {
+            $io->write('');
+            $io->write('<info>' . $msg('installing') . '</info> ' . implode(', ', $packages));
+            $io->write('');
+            self::runComposerRequire($packages, $io, $msg);
+        } elseif ($removePackages === []) {
+            $io->write('<info>' . $msg('no_components') . '</info>');
+        }
+    }
+
+    private static function renderTitle(): void
+    {
+        $output = new ConsoleOutput();
+        $terminalWidth = (new Terminal())->getWidth();
+        if ($terminalWidth <= 0) {
+            $terminalWidth = 80;
+        }
+
+        $text = ' ' . self::SETUP_TITLE . ' ';
+        $minBoxWidth = 44;
+        $maxBoxWidth = min($terminalWidth, 96);
+        $boxWidth = min($maxBoxWidth, max($minBoxWidth, mb_strwidth($text) + 10));
+
+        $innerWidth = $boxWidth - 2;
+        $textWidth = mb_strwidth($text);
+        $pad = max(0, $innerWidth - $textWidth);
+        $left = intdiv($pad, 2);
+        $right = $pad - $left;
+        $line2 = '│' . str_repeat(' ', $left) . $text . str_repeat(' ', $right) . '│';
+        $line1 = '┌' . str_repeat('─', $innerWidth) . '┐';
+        $line3 = '└' . str_repeat('─', $innerWidth) . '┘';
+
+        $output->writeln('');
+        $output->writeln('<fg=blue;options=bold>' . $line1 . '</>');
+        $output->writeln('<fg=blue;options=bold>' . $line2 . '</>');
+        $output->writeln('<fg=blue;options=bold>' . $line3 . '</>');
+        $output->writeln('');
+    }
+
+    // ═══════════════════════════════════════════════════════════════
+    // Signal handling (Ctrl+C)
+    // ═══════════════════════════════════════════════════════════════
+
+    /**
+     * Register Ctrl+C (SIGINT) handler to show a friendly message on interrupt.
+     * Gracefully skipped when the required extensions are unavailable.
+     */
+    private static function registerInterruptHandler(): void
+    {
+        // Unix/Linux/Mac: pcntl extension with async signals for immediate delivery
+        /*if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) {
+            pcntl_async_signals(true);
+            pcntl_signal(\SIGINT, [self::class, 'handleInterrupt']);
+            return;
+        }*/
+
+        // Windows: sapi ctrl handler (PHP >= 7.4)
+        if (function_exists('sapi_windows_set_ctrl_handler')) {
+            sapi_windows_set_ctrl_handler(static function (int $event) {
+                if ($event === \PHP_WINDOWS_EVENT_CTRL_C) {
+                    self::handleInterrupt();
+                }
+            });
+        }
+    }
+
+    /**
+     * Handle Ctrl+C: restore terminal, show tip, then exit.
+     */
+    private static function handleInterrupt(): void
+    {
+        // Restore terminal if in raw mode
+        if (self::$sttyMode !== null && function_exists('shell_exec')) {
+            @shell_exec('stty ' . self::$sttyMode);
+            self::$sttyMode = null;
+        }
+
+        $output = new ConsoleOutput();
+        $output->writeln('');
+        $output->writeln('<comment>' . (self::INTERRUPTED_MESSAGES[self::$interruptLocale] ?? self::INTERRUPTED_MESSAGES['en']) . '</comment>');
+        exit(1);
+    }
+
+    // ═══════════════════════════════════════════════════════════════
+    // Interactive Menu System
+    // ═══════════════════════════════════════════════════════════════
+
+    /**
+     * Check if terminal supports interactive features (arrow keys, ANSI colors).
+     */
+    private static function supportsInteractive(): bool
+    {
+        return function_exists('shell_exec') && Terminal::hasSttyAvailable();
+    }
+
+    /**
+     * Display a selection menu with arrow key navigation (if supported) or text input fallback.
+     *
+     * @param IOInterface $io   Composer IO
+     * @param string      $title Menu title
+     * @param array       $items Indexed array of ['tag' => string, 'label' => string]
+     * @param int         $default Default selected index (0-based)
+     * @return int Selected index
+     */
+    private static function selectMenu(IOInterface $io, string $title, array $items, int $default = 0): int
+    {
+        // Append localized "default" hint to avoid ambiguity
+        // (Template should contain a single %s placeholder for the default tag.)
+        $defaultHintTemplate = null;
+        if (isset(self::MESSAGES[self::$interruptLocale]['default_choice'])) {
+            $defaultHintTemplate = self::MESSAGES[self::$interruptLocale]['default_choice'];
+        }
+
+        $defaultTag = $items[$default]['tag'] ?? '';
+        if ($defaultHintTemplate && $defaultTag !== '') {
+            $title .= sprintf($defaultHintTemplate, $defaultTag);
+        } elseif ($defaultTag !== '') {
+            // Fallback for early menus (e.g. locale selection) before locale is chosen.
+            $title .= sprintf(' (default %s)', $defaultTag);
+        }
+
+        if (self::supportsInteractive()) {
+            return self::arrowKeySelect($title, $items, $default);
+        }
+
+        return self::fallbackSelect($io, $title, $items, $default);
+    }
+
+    /**
+     * Display a yes/no confirmation as a selection menu.
+     *
+     * @param IOInterface $io    Composer IO
+     * @param string      $title Menu title
+     * @param bool        $default Default value (true = yes)
+     * @return bool User's choice
+     */
+    private static function confirmMenu(IOInterface $io, string $title, bool $default = true): bool
+    {
+        $locale = self::$interruptLocale;
+        $yes = self::MESSAGES[$locale]['yes'] ?? self::MESSAGES['en']['yes'] ?? 'yes';
+        $no = self::MESSAGES[$locale]['no'] ?? self::MESSAGES['en']['no'] ?? 'no';
+        $items = $default
+            ? [['tag' => 'Y', 'label' => $yes], ['tag' => 'n', 'label' => $no]]
+            : [['tag' => 'y', 'label' => $yes], ['tag' => 'N', 'label' => $no]];
+        $defaultIndex = $default ? 0 : 1;
+
+        return self::selectMenu($io, $title, $items, $defaultIndex) === 0;
+    }
+
+    /**
+     * Interactive select with arrow key navigation, manual input and ANSI reverse-video highlighting.
+     * Input area and option list highlighting are bidirectionally linked.
+     * Requires stty (Unix-like terminals).
+     */
+    private static function arrowKeySelect(string $title, array $items, int $default): int
+    {
+        $output = new ConsoleOutput();
+        $count = count($items);
+        $selected = $default;
+
+        $maxTagWidth = max(array_map(fn(array $item) => mb_strlen($item['tag']), $items));
+        $defaultTag = $items[$default]['tag'];
+        $input = $defaultTag;
+
+        // Print title and initial options
+        $output->writeln('');
+        $output->writeln('<fg=blue;options=bold>' . $title . '</>');
+        self::drawMenuItems($output, $items, $selected, $maxTagWidth);
+        $output->write('> ' . $input);
+
+        // Enter raw mode
+        self::$sttyMode = shell_exec('stty -g');
+        shell_exec('stty -icanon -echo');
+
+        try {
+            while (!feof(STDIN)) {
+                $c = fread(STDIN, 1);
+
+                if (false === $c || '' === $c) {
+                    break;
+                }
+
+                // ── Backspace ──
+                if ("\177" === $c || "\010" === $c) {
+                    if ('' !== $input) {
+                        $input = mb_substr($input, 0, -1);
+                    }
+                    $selected = self::findItemByTag($items, $input);
+                    $output->write("\033[{$count}A");
+                    self::drawMenuItems($output, $items, $selected, $maxTagWidth);
+                    $output->write("\033[2K\r> " . $input);
+                    continue;
+                }
+
+                // ── Escape sequences (arrow keys) ──
+                if ("\033" === $c) {
+                    $seq = fread(STDIN, 2);
+                    if (isset($seq[1])) {
+                        $changed = false;
+                        if ('A' === $seq[1]) { // Up
+                            $selected = ($selected <= 0 ? $count : $selected) - 1;
+                            $changed = true;
+                        } elseif ('B' === $seq[1]) { // Down
+                            $selected = ($selected + 1) % $count;
+                            $changed = true;
+                        }
+                        if ($changed) {
+                            // Sync input with selected item's tag
+                            $input = $items[$selected]['tag'];
+                            $output->write("\033[{$count}A");
+                            self::drawMenuItems($output, $items, $selected, $maxTagWidth);
+                            $output->write("\033[2K\r> " . $input);
+                        }
+                    }
+                    continue;
+                }
+
+                // ── Enter: confirm selection ──
+                if ("\n" === $c || "\r" === $c) {
+                    if ($selected < 0) {
+                        $selected = $default;
+                    }
+                    $output->write("\033[2K\r> <info>" . $items[$selected]['tag'] . ' ' . $items[$selected]['label'] . '</info>');
+                    $output->writeln('');
+                    break;
+                }
+
+                // ── Ignore other control characters ──
+                if (ord($c) < 32) {
+                    continue;
+                }
+
+                // ── Printable character (with UTF-8 multi-byte support) ──
+                if ("\x80" <= $c) {
+                    $extra = ["\xC0" => 1, "\xD0" => 1, "\xE0" => 2, "\xF0" => 3];
+                    $c .= fread(STDIN, $extra[$c & "\xF0"] ?? 0);
+                }
+                $input .= $c;
+                $selected = self::findItemByTag($items, $input);
+                $output->write("\033[{$count}A");
+                self::drawMenuItems($output, $items, $selected, $maxTagWidth);
+                $output->write("\033[2K\r> " . $input);
+            }
+        } finally {
+            if (self::$sttyMode !== null) {
+                shell_exec('stty ' . self::$sttyMode);
+                self::$sttyMode = null;
+            }
+        }
+
+        return $selected < 0 ? $default : $selected;
+    }
+
+    /**
+     * Fallback select for terminals without stty support. Uses plain text input.
+     */
+    private static function fallbackSelect(IOInterface $io, string $title, array $items, int $default): int
+    {
+        $maxTagWidth = max(array_map(fn(array $item) => mb_strlen($item['tag']), $items));
+        $defaultTag = $items[$default]['tag'];
+
+        $io->write('');
+        $io->write('<fg=blue;options=bold>' . $title . '</>');
+        foreach ($items as $item) {
+            $tag = str_pad($item['tag'], $maxTagWidth);
+            $io->write("  [$tag] " . $item['label']);
+        }
+
+        while (true) {
+            $io->write('> ', false);
+            $line = fgets(STDIN);
+            if ($line === false) {
+                return $default;
+            }
+            $answer = trim($line);
+
+            if ($answer === '') {
+                $io->write('> <info>' . $items[$default]['tag'] . ' ' . $items[$default]['label'] . '</info>');
+                return $default;
+            }
+
+            // Match by tag (case-insensitive)
+            foreach ($items as $i => $item) {
+                if (strcasecmp($item['tag'], $answer) === 0) {
+                    $io->write('> <info>' . $items[$i]['tag'] . ' ' . $items[$i]['label'] . '</info>');
+                    return $i;
+                }
+            }
+        }
+    }
+
+    /**
+     * Render menu items with optional ANSI reverse-video highlighting for the selected item.
+     * When $selected is -1, no item is highlighted.
+     */
+    private static function drawMenuItems(ConsoleOutput $output, array $items, int $selected, int $maxTagWidth): void
+    {
+        foreach ($items as $i => $item) {
+            $tag = str_pad($item['tag'], $maxTagWidth);
+            $line = "  [$tag] " . $item['label'];
+            if ($i === $selected) {
+                $output->writeln("\033[2K\r\033[7m" . $line . "\033[0m");
+            } else {
+                $output->writeln("\033[2K\r" . $line);
+            }
+        }
+    }
+
+    /**
+     * Find item index by tag (case-insensitive exact match).
+     * Returns -1 if no match found or input is empty.
+     */
+    private static function findItemByTag(array $items, string $input): int
+    {
+        if ($input === '') {
+            return -1;
+        }
+        foreach ($items as $i => $item) {
+            if (strcasecmp($item['tag'], $input) === 0) {
+                return $i;
+            }
+        }
+        return -1;
+    }
+
+    // ═══════════════════════════════════════════════════════════════
+    // Locale selection
+    // ═══════════════════════════════════════════════════════════════
+
+    private static function askLocale(IOInterface $io): string
+    {
+        $locales = array_keys(self::LOCALE_LABELS);
+        $items = [];
+        foreach ($locales as $i => $code) {
+            $items[] = ['tag' => (string) $i, 'label' => self::LOCALE_LABELS[$code] . " ($code)"];
+        }
+
+        $selected = self::selectMenu(
+            $io,
+            '语言 / Language / 言語 / 언어',
+            $items,
+            0
+        );
+
+        return $locales[$selected];
+    }
+
+    // ═══════════════════════════════════════════════════════════════
+    // Timezone selection
+    // ═══════════════════════════════════════════════════════════════
+
+    private static function askTimezone(IOInterface $io, callable $msg, string $default): string
+    {
+        if (self::supportsInteractive()) {
+            return self::askTimezoneAutocomplete($msg, $default);
+        }
+
+        return self::askTimezoneSelect($io, $msg, $default);
+    }
+
+    /**
+     * Option A: when stty is available, custom character-by-character autocomplete
+     * (case-insensitive, substring match). Interaction: type to filter, hint on right;
+     * ↑↓ change candidate, Tab accept, Enter confirm; empty input = use default.
+     */
+    private static function askTimezoneAutocomplete(callable $msg, string $default): string
+    {
+        $allTimezones = \DateTimeZone::listIdentifiers();
+        $output = new ConsoleOutput();
+        $cursor = new Cursor($output);
+
+        $output->writeln('');
+        $output->writeln('<fg=blue;options=bold>' . $msg('timezone_title', $default) . '</>');
+        $output->writeln($msg('timezone_help'));
+        $output->write('> ');
+
+        self::$sttyMode = shell_exec('stty -g');
+        shell_exec('stty -icanon -echo');
+
+        // Auto-fill default timezone in the input area; user can edit it directly.
+        $input = $default;
+        $output->write($input);
+
+        $ofs = 0;
+        $matches = self::filterTimezones($allTimezones, $input);
+        if (!empty($matches)) {
+            $hint = $matches[$ofs % count($matches)];
+            // Avoid duplicating hint when input already fully matches the only candidate.
+            if (!(count($matches) === 1 && $hint === $input)) {
+                $cursor->clearLineAfter();
+                $cursor->savePosition();
+                $output->write('  <fg=blue;options=bold>' . $hint . '</>');
+                if (count($matches) > 1) {
+                    $output->write('  <info>(' . count($matches) . ' matches, ↑↓)</info>');
+                }
+                $cursor->restorePosition();
+            }
+        }
+
+        try {
+            while (!feof(STDIN)) {
+                $c = fread(STDIN, 1);
+
+                if (false === $c || '' === $c) {
+                    break;
+                }
+
+                // ── Backspace ──
+                if ("\177" === $c || "\010" === $c) {
+                    if ('' !== $input) {
+                        $lastChar = mb_substr($input, -1);
+                        $input = mb_substr($input, 0, -1);
+                        $cursor->moveLeft(max(1, mb_strwidth($lastChar)));
+                    }
+                    $ofs = 0;
+
+                    // ── Escape sequences (arrows) ──
+                } elseif ("\033" === $c) {
+                    $seq = fread(STDIN, 2);
+                    if (isset($seq[1]) && !empty($matches)) {
+                        if ('A' === $seq[1]) {
+                            $ofs = ($ofs - 1 + count($matches)) % count($matches);
+                        } elseif ('B' === $seq[1]) {
+                            $ofs = ($ofs + 1) % count($matches);
+                        }
+                    }
+
+                    // ── Tab: accept current match ──
+                } elseif ("\t" === $c) {
+                    if (isset($matches[$ofs])) {
+                        self::replaceInput($output, $cursor, $input, $matches[$ofs]);
+                        $input   = $matches[$ofs];
+                        $matches = [];
+                    }
+                    $cursor->clearLineAfter();
+                    continue;
+
+                    // ── Enter: confirm ──
+                } elseif ("\n" === $c || "\r" === $c) {
+                    if (isset($matches[$ofs])) {
+                        self::replaceInput($output, $cursor, $input, $matches[$ofs]);
+                        $input = $matches[$ofs];
+                    }
+                    if ($input === '') {
+                        $input = $default;
+                    }
+                    // Re-render user input with <comment> style
+                    $cursor->moveToColumn(1);
+                    $cursor->clearLine();
+                    $output->write('> <info>' . $input . '</info>');
+                    $output->writeln('');
+                    break;
+
+                    // ── Other control chars: ignore ──
+                } elseif (ord($c) < 32) {
+                    continue;
+
+                    // ── Printable character ──
+                } else {
+                    if ("\x80" <= $c) {
+                        $extra = ["\xC0" => 1, "\xD0" => 1, "\xE0" => 2, "\xF0" => 3];
+                        $c .= fread(STDIN, $extra[$c & "\xF0"] ?? 0);
+                    }
+                    $output->write($c);
+                    $input .= $c;
+                    $ofs = 0;
+                }
+
+                // Update match list
+                $matches = self::filterTimezones($allTimezones, $input);
+
+                // Show autocomplete hint
+                $cursor->clearLineAfter();
+                if (!empty($matches)) {
+                    $hint = $matches[$ofs % count($matches)];
+                    $cursor->savePosition();
+                    $output->write('  <fg=blue;options=bold>' . $hint . '</>');
+                    if (count($matches) > 1) {
+                        $output->write('  <info>(' . count($matches) . ' matches, ↑↓)</info>');
+                    }
+                    $cursor->restorePosition();
+                }
+            }
+        } finally {
+            if (self::$sttyMode !== null) {
+                shell_exec('stty ' . self::$sttyMode);
+                self::$sttyMode = null;
+            }
+        }
+
+        $result = '' === $input ? $default : $input;
+
+        if (!in_array($result, $allTimezones, true)) {
+            $output->writeln('<comment>' . $msg('timezone_invalid', $default) . '</comment>');
+            return $default;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Clear current input and replace with new text.
+     */
+    private static function replaceInput(ConsoleOutput $output, Cursor $cursor, string $oldInput, string $newInput): void
+    {
+        if ('' !== $oldInput) {
+            $cursor->moveLeft(mb_strwidth($oldInput));
+        }
+        $cursor->clearLineAfter();
+        $output->write($newInput);
+    }
+
+    /**
+     * Case-insensitive substring match for timezones.
+     */
+    private static function filterTimezones(array $timezones, string $input): array
+    {
+        if ('' === $input) {
+            return [];
+        }
+        $lower = mb_strtolower($input);
+        return array_values(array_filter(
+            $timezones,
+            fn(string $tz) => str_contains(mb_strtolower($tz), $lower)
+        ));
+    }
+
+    /**
+     * Find an exact timezone match (case-insensitive).
+     * Returns the correctly-cased system timezone name, or null if not found.
+     */
+    private static function findExactTimezone(array $allTimezones, string $input): ?string
+    {
+        $lower = mb_strtolower($input);
+        foreach ($allTimezones as $tz) {
+            if (mb_strtolower($tz) === $lower) {
+                return $tz;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Search timezones by keyword (substring) and similarity.
+     * Returns combined results: substring matches first, then similarity matches (>=50%).
+     *
+     * @param string[] $allTimezones All valid timezone identifiers
+     * @param string   $keyword      User input to search for
+     * @param int      $limit        Maximum number of results
+     * @return string[] Matched timezone identifiers
+     */
+    private static function searchTimezones(array $allTimezones, string $keyword, int $limit = 15): array
+    {
+        // 1. Substring matches (higher priority)
+        $substringMatches = self::filterTimezones($allTimezones, $keyword);
+        if (count($substringMatches) >= $limit) {
+            return array_slice($substringMatches, 0, $limit);
+        }
+
+        // 2. Similarity matches for remaining slots (normalized: strip _ and /)
+        $substringSet = array_flip($substringMatches);
+        $normalizedKeyword = str_replace(['_', '/'], ' ', mb_strtolower($keyword));
+        $similarityMatches = [];
+
+        foreach ($allTimezones as $tz) {
+            if (isset($substringSet[$tz])) {
+                continue;
+            }
+            $parts = explode('/', $tz);
+            $city = str_replace('_', ' ', mb_strtolower(end($parts)));
+            $normalizedTz = str_replace(['_', '/'], ' ', mb_strtolower($tz));
+
+            similar_text($normalizedKeyword, $city, $cityPercent);
+            similar_text($normalizedKeyword, $normalizedTz, $fullPercent);
+
+            $bestPercent = max($cityPercent, $fullPercent);
+            if ($bestPercent >= 50.0) {
+                $similarityMatches[] = ['tz' => $tz, 'score' => $bestPercent];
+            }
+        }
+
+        usort($similarityMatches, fn(array $a, array $b) => $b['score'] <=> $a['score']);
+
+        $results = $substringMatches;
+        foreach ($similarityMatches as $item) {
+            $results[] = $item['tz'];
+            if (count($results) >= $limit) {
+                break;
+            }
+        }
+
+        return $results;
+    }
+
+    /**
+     * Option B: when stty is not available (e.g. Windows), keyword search with numbered list.
+     * Flow: enter timezone/keyword → exact match uses it directly; otherwise show
+     * numbered results (substring + similarity) → pick by number or refine keyword.
+     */
+    private static function askTimezoneSelect(IOInterface $io, callable $msg, string $default): string
+    {
+        $allTimezones = \DateTimeZone::listIdentifiers();
+
+        $io->write('');
+        $io->write('<fg=blue;options=bold>' . $msg('timezone_title', $default) . '</>');
+        $io->write($msg('timezone_input_prompt'));
+
+        /** @var string[]|null Currently displayed search result list */
+        $currentList = null;
+
+        while (true) {
+            $io->write('> ', false);
+            $line = fgets(STDIN);
+            if ($line === false) {
+                return $default;
+            }
+            $answer = trim($line);
+
+            // Empty input → use default
+            if ($answer === '') {
+                $io->write('> <info>' . $default . '</info>');
+                return $default;
+            }
+
+            // If a numbered list is displayed and input is a pure number
+            if ($currentList !== null && ctype_digit($answer)) {
+                $idx = (int) $answer;
+                if (isset($currentList[$idx])) {
+                    $io->write('> <info>' . $currentList[$idx] . '</info>');
+                    return $currentList[$idx];
+                }
+                $io->write('<comment>' . $msg('timezone_invalid_index') . '</comment>');
+                continue;
+            }
+
+            // Exact case-insensitive match → return the correctly-cased system value
+            $exact = self::findExactTimezone($allTimezones, $answer);
+            if ($exact !== null) {
+                $io->write('> <info>' . $exact . '</info>');
+                return $exact;
+            }
+
+            // Keyword + similarity search
+            $results = self::searchTimezones($allTimezones, $answer);
+
+            if (empty($results)) {
+                $io->write('<comment>' . $msg('timezone_no_match') . '</comment>');
+                $currentList = null;
+                continue;
+            }
+
+            // Single result → use it directly
+            if (count($results) === 1) {
+                $io->write('> <info>' . $results[0] . '</info>');
+                return $results[0];
+            }
+
+            // Display numbered list
+            $currentList = $results;
+            $padWidth = strlen((string) (count($results) - 1));
+            foreach ($results as $i => $tz) {
+                $io->write('  [' . str_pad((string) $i, $padWidth) . '] ' . $tz);
+            }
+            $io->write($msg('timezone_pick_prompt'));
+        }
+    }
+
+    // ═══════════════════════════════════════════════════════════════
+    // Optional component selection
+    // ═══════════════════════════════════════════════════════════════
+
+    private static function askComponents(IOInterface $io, callable $msg): array
+    {
+        $packages = [];
+        $addPackage = static function (string $package) use (&$packages, $io, $msg): void {
+            if (in_array($package, $packages, true)) {
+                return;
+            }
+            $packages[] = $package;
+            $io->write($msg('adding_package', '<info>' . $package . '</info>'));
+        };
+
+        // Console (default: yes)
+        if (self::confirmMenu($io, $msg('console_question'), true)) {
+            $addPackage(self::PACKAGE_CONSOLE);
+        }
+
+        // Database
+        $dbItems = [
+            ['tag' => '0', 'label' => $msg('db_none')],
+            ['tag' => '1', 'label' => 'webman/database'],
+            ['tag' => '2', 'label' => 'webman/think-orm'],
+            ['tag' => '3', 'label' => 'webman/database && webman/think-orm'],
+        ];
+        $dbChoice = self::selectMenu($io, $msg('db_question'), $dbItems, 0);
+        if ($dbChoice === 1) {
+            $addPackage(self::PACKAGE_DATABASE);
+        } elseif ($dbChoice === 2) {
+            $addPackage(self::PACKAGE_THINK_ORM);
+        } elseif ($dbChoice === 3) {
+            $addPackage(self::PACKAGE_DATABASE);
+            $addPackage(self::PACKAGE_THINK_ORM);
+        }
+
+        // If webman/database is selected, add required dependencies automatically
+        if (in_array(self::PACKAGE_DATABASE, $packages, true)) {
+            $addPackage(self::PACKAGE_ILLUMINATE_PAGINATION);
+            $addPackage(self::PACKAGE_ILLUMINATE_EVENTS);
+            $addPackage(self::PACKAGE_SYMFONY_VAR_DUMPER);
+        }
+
+        // Redis (default: no)
+        if (self::confirmMenu($io, $msg('redis_question'), false)) {
+            $addPackage(self::PACKAGE_REDIS);
+            $addPackage(self::PACKAGE_ILLUMINATE_EVENTS);
+        }
+
+        // Validation (default: no)
+        if (self::confirmMenu($io, $msg('validation_question'), false)) {
+            $addPackage(self::PACKAGE_VALIDATION);
+        }
+
+        // Template engine
+        $tplItems = [
+            ['tag' => '0', 'label' => $msg('template_none')],
+            ['tag' => '1', 'label' => 'webman/blade'],
+            ['tag' => '2', 'label' => 'twig/twig'],
+            ['tag' => '3', 'label' => 'topthink/think-template'],
+        ];
+        $tplChoice = self::selectMenu($io, $msg('template_question'), $tplItems, 0);
+        if ($tplChoice === 1) {
+            $addPackage(self::PACKAGE_BLADE);
+        } elseif ($tplChoice === 2) {
+            $addPackage(self::PACKAGE_TWIG);
+        } elseif ($tplChoice === 3) {
+            $addPackage(self::PACKAGE_THINK_TEMPLATE);
+        }
+
+        return $packages;
+    }
+
+    // ═══════════════════════════════════════════════════════════════
+    // Config file update
+    // ═══════════════════════════════════════════════════════════════
+
+    /**
+     * Update a config value like 'key' => 'old_value' in the given file.
+     */
+    private static function updateConfig(Event $event, string $relativePath, string $key, string $newValue): void
+    {
+        $root = dirname($event->getComposer()->getConfig()->get('vendor-dir'));
+        $file = $root . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relativePath);
+        if (!is_readable($file)) {
+            return;
+        }
+        $content = file_get_contents($file);
+        if ($content === false) {
+            return;
+        }
+        $pattern = '/' . preg_quote($key, '/') . "\s*=>\s*'[^']*'/";
+        $replacement = $key . " => '" . $newValue . "'";
+        $newContent = preg_replace($pattern, $replacement, $content);
+        if ($newContent !== null && $newContent !== $content) {
+            file_put_contents($file, $newContent);
+        }
+    }
+
+    // ═══════════════════════════════════════════════════════════════
+    // Composer require
+    // ═══════════════════════════════════════════════════════════════
+
+    private static function runComposerRequire(array $packages, IOInterface $io, callable $msg): void
+    {
+        $io->write('<comment>' . $msg('running') . '</comment> composer require ' . implode(' ', $packages));
+        $io->write('');
+
+        $code = self::runComposerCommand('require', $packages);
+
+        if ($code !== 0) {
+            $io->writeError('<error>' . $msg('error_install', implode(' ', $packages)) . '</error>');
+        } else {
+            $io->write('<info>' . $msg('done') . '</info>');
+        }
+    }
+
+    private static function askRemoveComponents(Event $event, array $selectedPackages, IOInterface $io, callable $msg): array
+    {
+        $requires = $event->getComposer()->getPackage()->getRequires();
+        $allOptionalPackages = [
+            self::PACKAGE_CONSOLE,
+            self::PACKAGE_DATABASE,
+            self::PACKAGE_THINK_ORM,
+            self::PACKAGE_REDIS,
+            self::PACKAGE_ILLUMINATE_EVENTS,
+            self::PACKAGE_ILLUMINATE_PAGINATION,
+            self::PACKAGE_SYMFONY_VAR_DUMPER,
+            self::PACKAGE_VALIDATION,
+            self::PACKAGE_BLADE,
+            self::PACKAGE_TWIG,
+            self::PACKAGE_THINK_TEMPLATE,
+        ];
+
+        $secondaryPackages = [
+            self::PACKAGE_ILLUMINATE_EVENTS,
+            self::PACKAGE_ILLUMINATE_PAGINATION,
+            self::PACKAGE_SYMFONY_VAR_DUMPER,
+        ];
+
+        $installedOptionalPackages = [];
+        foreach ($allOptionalPackages as $pkg) {
+            if (isset($requires[$pkg])) {
+                $installedOptionalPackages[] = $pkg;
+            }
+        }
+
+        $allPackagesToRemove = array_diff($installedOptionalPackages, $selectedPackages);
+
+        if (count($allPackagesToRemove) === 0) {
+            return [];
+        }
+
+        $displayPackagesToRemove = array_diff($allPackagesToRemove, $secondaryPackages);
+
+        if (count($displayPackagesToRemove) === 0) {
+            return $allPackagesToRemove;
+        }
+
+        $pkgListStr = "";
+        foreach ($displayPackagesToRemove as $pkg) {
+            $pkgListStr .= "\n  - {$pkg}";
+        }
+        $pkgListStr .= "\n";
+
+        $title = '<comment>' . $msg('remove_package_question', '') . '</comment>' . $pkgListStr;
+        if (self::confirmMenu($io, $title, false)) {
+            return $allPackagesToRemove;
+        }
+
+        return [];
+    }
+
+    private static function runComposerRemove(array $packages, IOInterface $io, callable $msg): void
+    {
+        $io->write('<comment>' . $msg('running') . '</comment> composer remove ' . implode(' ', $packages));
+        $io->write('');
+
+        $code = self::runComposerCommand('remove', $packages);
+
+        if ($code !== 0) {
+            $io->writeError('<error>' . $msg('error_remove', implode(' ', $packages)) . '</error>');
+        } else {
+            $io->write('<info>' . $msg('done_remove') . '</info>');
+        }
+    }
+
+    /**
+     * Run a Composer command (require/remove) in-process via Composer's Application API.
+     * No shell execution functions needed — works even when passthru/exec/shell_exec are disabled.
+     */
+    private static function runComposerCommand(string $command, array $packages): int
+    {
+        try {
+            // Already inside a user-initiated Composer session — suppress duplicate root/superuser warnings
+            $_SERVER['COMPOSER_ALLOW_SUPERUSER'] = '1';
+            if (function_exists('putenv')) {
+                putenv('COMPOSER_ALLOW_SUPERUSER=1');
+            }
+
+            $application = new ComposerApplication();
+            $application->setAutoExit(false);
+
+            return $application->run(
+                new ArrayInput([
+                    'command'  => $command,
+                    'packages' => $packages,
+                    '--no-interaction' => true,
+                    '--update-with-all-dependencies' => true,
+                ]),
+                new ConsoleOutput()
+            );
+        } catch (\Throwable) {
+            return 1;
+        }
+    }
+}

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