From 8da7b79a0547200343b754f89e18ca7c3fccd3da Mon Sep 17 00:00:00 2001 From: "zhangf@suq.cn" Date: Sat, 6 Dec 2025 17:23:02 +0800 Subject: [PATCH] feat:init --- .gitignore | 8 + LICENSE | 21 + README.md | 70 + app/controller/DistillationWordController.php | 23 + app/dao/DistillationWordDao.php | 17 + app/functions.php | 4 + app/middleware/StaticFile.php | 42 + app/model/DistillationExpandWord.php | 33 + app/model/DistillationTransWord.php | 33 + app/model/DistillationWord.php | 33 + app/process/Http.php | 10 + app/process/Monitor.php | 305 + app/route/route.php | 32 + app/service/DistillationWordService.php | 54 + app/validate/UserGroupValidate.php | 28 + app/view/index/view.html | 14 + composer.json | 65 + composer.lock | 7563 ++++++++++++++++++++ config/app.php | 26 + config/autoload.php | 21 + config/bootstrap.php | 18 + config/cache.php | 18 + config/container.php | 22 + config/database.php | 40 + config/dependence.php | 15 + config/event.php | 5 + config/exception.php | 17 + config/log.php | 32 + config/middleware.php | 15 + config/plugin/tinywan/jwt/app.php | 83 + config/plugin/tinywan/storage/app.php | 81 + config/plugin/webman/console/app.php | 24 + config/plugin/webman/event/app.php | 4 + config/plugin/webman/event/bootstrap.php | 17 + config/plugin/webman/event/command.php | 7 + config/plugin/x2nx/webman-migrate/app.php | 5 + config/plugin/x2nx/webman-migrate/command.php | 44 + config/plugin/x2nx/webman-migrate/phinx.php | 24 + config/process.php | 62 + config/redis.php | 18 + config/route.php | 24 + config/server.php | 23 + config/session.php | 65 + config/static.php | 23 + config/think-orm.php | 42 + config/translation.php | 25 + config/view.php | 22 + database/factories/UsersFactory.php | 37 + .../2025_09_01_145446_create_attachment_table.php | 37 + .../2025_10_20_141905_create_platform_table.php | 33 + database/seeders/DatabaseSeeder.php | 40 + plugin/piadmin/.idea/.gitignore | 8 + plugin/piadmin/.idea/modules.xml | 8 + plugin/piadmin/.idea/php.xml | 22 + plugin/piadmin/.idea/vcs.xml | 6 + plugin/piadmin/api/Install.php | 437 ++ plugin/piadmin/app/base/BaseController.php | 40 + plugin/piadmin/app/base/BaseDao.php | 796 ++ plugin/piadmin/app/base/BaseModel.php | 27 + plugin/piadmin/app/base/BaseService.php | 62 + plugin/piadmin/app/base/BaseValidate.php | 16 + plugin/piadmin/app/common/ContainerProxy.php | 53 + .../app/controller/v1/AttachmentController.php | 51 + .../app/controller/v1/DataDictionaryController.php | 16 + .../piadmin/app/controller/v1/IndexController.php | 37 + .../piadmin/app/controller/v1/LoginController.php | 50 + .../piadmin/app/controller/v1/MenuController.php | 96 + .../app/controller/v1/SystemAdminController.php | 83 + .../app/controller/v1/SystemDeptController.php | 76 + .../app/controller/v1/SystemRoleController.php | 99 + plugin/piadmin/app/dao/AttachmenatDao.php | 16 + plugin/piadmin/app/dao/InformationSchemaDao.php | 46 + plugin/piadmin/app/dao/SystemAdminDao.php | 19 + plugin/piadmin/app/dao/SystemAdminDeptDao.php | 15 + plugin/piadmin/app/dao/SystemAdminRoleDao.php | 15 + plugin/piadmin/app/dao/SystemDeptDao.php | 15 + plugin/piadmin/app/dao/SystemMenuDao.php | 17 + plugin/piadmin/app/dao/SystemRoleDao.php | 18 + plugin/piadmin/app/dao/SystemRoleMenuDao.php | 19 + plugin/piadmin/app/exception/ApiException.php | 17 + .../piadmin/app/exception/FrameworkException.php | 17 + plugin/piadmin/app/functions.php | 162 + .../middleware/AdminAuthorizationMiddleware.php | 81 + .../app/middleware/CrossDomainMiddleware.php | 28 + .../piadmin/app/middleware/LanguageMiddleware.php | 26 + .../app/middleware/ParameterConvertMiddleware.php | 145 + .../app/middleware/PermissionsMiddleware.php | 40 + plugin/piadmin/app/model/Attachment.php | 44 + plugin/piadmin/app/model/InformationSchema.php | 20 + plugin/piadmin/app/model/SystemAdmin.php | 45 + plugin/piadmin/app/model/SystemAdminDept.php | 11 + plugin/piadmin/app/model/SystemAdminRole.php | 27 + plugin/piadmin/app/model/SystemDept.php | 26 + plugin/piadmin/app/model/SystemMenu.php | 16 + plugin/piadmin/app/model/SystemRole.php | 30 + plugin/piadmin/app/model/SystemRoleMenu.php | 25 + plugin/piadmin/app/route/v1/attachment.php | 16 + plugin/piadmin/app/route/v1/route.php | 71 + plugin/piadmin/app/service/AttachmentService.php | 98 + .../piadmin/app/service/DataDictionaryService.php | 24 + plugin/piadmin/app/service/MenuService.php | 224 + .../piadmin/app/service/SystemAdminDeptService.php | 14 + .../piadmin/app/service/SystemAdminRoleService.php | 14 + plugin/piadmin/app/service/SystemAdminService.php | 387 + plugin/piadmin/app/service/SystemDeptService.php | 265 + plugin/piadmin/app/service/SystemRoleService.php | 245 + plugin/piadmin/app/utils/AdminTokenUtils.php | 89 + plugin/piadmin/app/utils/ArrayUtils.php | 339 + plugin/piadmin/app/utils/BcUtils.php | 61 + plugin/piadmin/app/utils/CacheHelper.php | 122 + plugin/piadmin/app/utils/CacheUtils.php | 116 + plugin/piadmin/app/utils/Captcha.php | 77 + plugin/piadmin/app/utils/CaptchaUtils.php | 183 + plugin/piadmin/app/utils/CurlUtils.php | 157 + plugin/piadmin/app/utils/DateTimeUtils.php | 243 + plugin/piadmin/app/utils/DbUtils.php | 17 + plugin/piadmin/app/utils/DictUtils.php | 105 + plugin/piadmin/app/utils/IdPathUtils.php | 17 + plugin/piadmin/app/utils/IdUtils.php | 24 + plugin/piadmin/app/utils/JwtUtils.php | 98 + plugin/piadmin/app/utils/LangUtils.php | 65 + plugin/piadmin/app/utils/RandomUtils.php | 50 + plugin/piadmin/app/utils/RequestUtils.php | 76 + plugin/piadmin/app/utils/SecretUtils.php | 25 + plugin/piadmin/app/utils/StrUtils.php | 50 + plugin/piadmin/app/utils/TreeUtils.php | 60 + plugin/piadmin/app/utils/UrlUtils.php | 29 + plugin/piadmin/app/utils/storage/AliOssStorage.php | 275 + plugin/piadmin/app/validate/LoginValidate.php | 28 + plugin/piadmin/app/validate/MenuValidate.php | 26 + .../piadmin/app/validate/SystemAdminValidate.php | 44 + plugin/piadmin/app/validate/SystemDeptValidate.php | 39 + plugin/piadmin/app/validate/SystemRoleValidate.php | 41 + .../app/validate/system/CaptchaValidate.php | 39 + .../piadmin/app/validate/system/LangValidate.php | 26 + plugin/piadmin/app/validate/system/OssValidate.php | 26 + plugin/piadmin/config/app.php | 10 + plugin/piadmin/config/autoload.php | 6 + plugin/piadmin/config/container.php | 6 + plugin/piadmin/config/exception.php | 6 + plugin/piadmin/config/log.php | 20 + plugin/piadmin/config/menu.php | 3 + plugin/piadmin/config/middleware.php | 13 + plugin/piadmin/config/oss.php | 15 + plugin/piadmin/config/piadmin.php | 44 + plugin/piadmin/config/process.php | 2 + plugin/piadmin/config/route.php | 14 + plugin/piadmin/config/settings.php | 122 + plugin/piadmin/config/static.php | 6 + plugin/piadmin/config/translation.php | 10 + plugin/piadmin/config/view.php | 10 + plugin/piadmin/exception/ExceptionHandler.php | 59 + plugin/piadmin/install.sql | 230 + .../piadmin/resource/translations/en/messages.php | 6 + .../resource/translations/zh_CN/messages.php | 64 + plugin/piadmin/update.php | 4 + public/favicon.ico | Bin 0 -> 4286 bytes .../dffb25364449abaabb13f44535480044c8efd268.png | Bin 0 -> 42646 bytes start.php | 5 + support/Request.php | 24 + support/Response.php | 24 + support/bootstrap.php | 139 + webman | 71 + windows.bat | 3 + windows.php | 136 + 165 files changed, 17204 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/controller/DistillationWordController.php create mode 100644 app/dao/DistillationWordDao.php create mode 100644 app/functions.php create mode 100644 app/middleware/StaticFile.php create mode 100644 app/model/DistillationExpandWord.php create mode 100644 app/model/DistillationTransWord.php create mode 100644 app/model/DistillationWord.php create mode 100644 app/process/Http.php create mode 100644 app/process/Monitor.php create mode 100644 app/route/route.php create mode 100644 app/service/DistillationWordService.php create mode 100644 app/validate/UserGroupValidate.php create mode 100644 app/view/index/view.html create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 config/app.php create mode 100644 config/autoload.php create mode 100644 config/bootstrap.php create mode 100644 config/cache.php create mode 100644 config/container.php create mode 100644 config/database.php create mode 100644 config/dependence.php create mode 100644 config/event.php create mode 100644 config/exception.php create mode 100644 config/log.php create mode 100644 config/middleware.php create mode 100644 config/plugin/tinywan/jwt/app.php create mode 100644 config/plugin/tinywan/storage/app.php create mode 100644 config/plugin/webman/console/app.php create mode 100644 config/plugin/webman/event/app.php create mode 100644 config/plugin/webman/event/bootstrap.php create mode 100644 config/plugin/webman/event/command.php create mode 100644 config/plugin/x2nx/webman-migrate/app.php create mode 100644 config/plugin/x2nx/webman-migrate/command.php create mode 100644 config/plugin/x2nx/webman-migrate/phinx.php create mode 100644 config/process.php create mode 100644 config/redis.php create mode 100644 config/route.php create mode 100644 config/server.php create mode 100644 config/session.php create mode 100644 config/static.php create mode 100644 config/think-orm.php create mode 100644 config/translation.php create mode 100644 config/view.php create mode 100644 database/factories/UsersFactory.php create mode 100644 database/migrations/2025_09_01_145446_create_attachment_table.php create mode 100644 database/migrations/2025_10_20_141905_create_platform_table.php create mode 100644 database/seeders/DatabaseSeeder.php create mode 100644 plugin/piadmin/.idea/.gitignore create mode 100644 plugin/piadmin/.idea/modules.xml create mode 100644 plugin/piadmin/.idea/php.xml create mode 100644 plugin/piadmin/.idea/vcs.xml create mode 100644 plugin/piadmin/api/Install.php create mode 100644 plugin/piadmin/app/base/BaseController.php create mode 100644 plugin/piadmin/app/base/BaseDao.php create mode 100644 plugin/piadmin/app/base/BaseModel.php create mode 100644 plugin/piadmin/app/base/BaseService.php create mode 100644 plugin/piadmin/app/base/BaseValidate.php create mode 100644 plugin/piadmin/app/common/ContainerProxy.php create mode 100644 plugin/piadmin/app/controller/v1/AttachmentController.php create mode 100644 plugin/piadmin/app/controller/v1/DataDictionaryController.php create mode 100644 plugin/piadmin/app/controller/v1/IndexController.php create mode 100644 plugin/piadmin/app/controller/v1/LoginController.php create mode 100644 plugin/piadmin/app/controller/v1/MenuController.php create mode 100644 plugin/piadmin/app/controller/v1/SystemAdminController.php create mode 100644 plugin/piadmin/app/controller/v1/SystemDeptController.php create mode 100644 plugin/piadmin/app/controller/v1/SystemRoleController.php create mode 100644 plugin/piadmin/app/dao/AttachmenatDao.php create mode 100644 plugin/piadmin/app/dao/InformationSchemaDao.php create mode 100644 plugin/piadmin/app/dao/SystemAdminDao.php create mode 100644 plugin/piadmin/app/dao/SystemAdminDeptDao.php create mode 100644 plugin/piadmin/app/dao/SystemAdminRoleDao.php create mode 100644 plugin/piadmin/app/dao/SystemDeptDao.php create mode 100644 plugin/piadmin/app/dao/SystemMenuDao.php create mode 100644 plugin/piadmin/app/dao/SystemRoleDao.php create mode 100644 plugin/piadmin/app/dao/SystemRoleMenuDao.php create mode 100644 plugin/piadmin/app/exception/ApiException.php create mode 100644 plugin/piadmin/app/exception/FrameworkException.php create mode 100644 plugin/piadmin/app/functions.php create mode 100644 plugin/piadmin/app/middleware/AdminAuthorizationMiddleware.php create mode 100644 plugin/piadmin/app/middleware/CrossDomainMiddleware.php create mode 100644 plugin/piadmin/app/middleware/LanguageMiddleware.php create mode 100644 plugin/piadmin/app/middleware/ParameterConvertMiddleware.php create mode 100644 plugin/piadmin/app/middleware/PermissionsMiddleware.php create mode 100644 plugin/piadmin/app/model/Attachment.php create mode 100644 plugin/piadmin/app/model/InformationSchema.php create mode 100644 plugin/piadmin/app/model/SystemAdmin.php create mode 100644 plugin/piadmin/app/model/SystemAdminDept.php create mode 100644 plugin/piadmin/app/model/SystemAdminRole.php create mode 100644 plugin/piadmin/app/model/SystemDept.php create mode 100644 plugin/piadmin/app/model/SystemMenu.php create mode 100644 plugin/piadmin/app/model/SystemRole.php create mode 100644 plugin/piadmin/app/model/SystemRoleMenu.php create mode 100644 plugin/piadmin/app/route/v1/attachment.php create mode 100644 plugin/piadmin/app/route/v1/route.php create mode 100644 plugin/piadmin/app/service/AttachmentService.php create mode 100644 plugin/piadmin/app/service/DataDictionaryService.php create mode 100644 plugin/piadmin/app/service/MenuService.php create mode 100644 plugin/piadmin/app/service/SystemAdminDeptService.php create mode 100644 plugin/piadmin/app/service/SystemAdminRoleService.php create mode 100644 plugin/piadmin/app/service/SystemAdminService.php create mode 100644 plugin/piadmin/app/service/SystemDeptService.php create mode 100644 plugin/piadmin/app/service/SystemRoleService.php create mode 100644 plugin/piadmin/app/utils/AdminTokenUtils.php create mode 100644 plugin/piadmin/app/utils/ArrayUtils.php create mode 100644 plugin/piadmin/app/utils/BcUtils.php create mode 100644 plugin/piadmin/app/utils/CacheHelper.php create mode 100644 plugin/piadmin/app/utils/CacheUtils.php create mode 100644 plugin/piadmin/app/utils/Captcha.php create mode 100644 plugin/piadmin/app/utils/CaptchaUtils.php create mode 100644 plugin/piadmin/app/utils/CurlUtils.php create mode 100644 plugin/piadmin/app/utils/DateTimeUtils.php create mode 100644 plugin/piadmin/app/utils/DbUtils.php create mode 100644 plugin/piadmin/app/utils/DictUtils.php create mode 100644 plugin/piadmin/app/utils/IdPathUtils.php create mode 100644 plugin/piadmin/app/utils/IdUtils.php create mode 100644 plugin/piadmin/app/utils/JwtUtils.php create mode 100644 plugin/piadmin/app/utils/LangUtils.php create mode 100644 plugin/piadmin/app/utils/RandomUtils.php create mode 100644 plugin/piadmin/app/utils/RequestUtils.php create mode 100644 plugin/piadmin/app/utils/SecretUtils.php create mode 100644 plugin/piadmin/app/utils/StrUtils.php create mode 100644 plugin/piadmin/app/utils/TreeUtils.php create mode 100644 plugin/piadmin/app/utils/UrlUtils.php create mode 100644 plugin/piadmin/app/utils/storage/AliOssStorage.php create mode 100644 plugin/piadmin/app/validate/LoginValidate.php create mode 100644 plugin/piadmin/app/validate/MenuValidate.php create mode 100644 plugin/piadmin/app/validate/SystemAdminValidate.php create mode 100644 plugin/piadmin/app/validate/SystemDeptValidate.php create mode 100644 plugin/piadmin/app/validate/SystemRoleValidate.php create mode 100644 plugin/piadmin/app/validate/system/CaptchaValidate.php create mode 100644 plugin/piadmin/app/validate/system/LangValidate.php create mode 100644 plugin/piadmin/app/validate/system/OssValidate.php create mode 100644 plugin/piadmin/config/app.php create mode 100644 plugin/piadmin/config/autoload.php create mode 100644 plugin/piadmin/config/container.php create mode 100644 plugin/piadmin/config/exception.php create mode 100644 plugin/piadmin/config/log.php create mode 100644 plugin/piadmin/config/menu.php create mode 100644 plugin/piadmin/config/middleware.php create mode 100644 plugin/piadmin/config/oss.php create mode 100644 plugin/piadmin/config/piadmin.php create mode 100644 plugin/piadmin/config/process.php create mode 100644 plugin/piadmin/config/route.php create mode 100644 plugin/piadmin/config/settings.php create mode 100644 plugin/piadmin/config/static.php create mode 100644 plugin/piadmin/config/translation.php create mode 100644 plugin/piadmin/config/view.php create mode 100644 plugin/piadmin/exception/ExceptionHandler.php create mode 100644 plugin/piadmin/install.sql create mode 100644 plugin/piadmin/resource/translations/en/messages.php create mode 100644 plugin/piadmin/resource/translations/zh_CN/messages.php create mode 100644 plugin/piadmin/update.php create mode 100644 public/favicon.ico create mode 100644 publicupload/20250901/dffb25364449abaabb13f44535480044c8efd268.png create mode 100644 start.php create mode 100644 support/Request.php create mode 100644 support/Response.php create mode 100644 support/bootstrap.php create mode 100644 webman create mode 100644 windows.bat create mode 100644 windows.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..516299c --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/runtime +/.idea +/.vscode +/vendor +*.log +.env +/tests/tmp +/tests/.phpunit.result.cache diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2c66292 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 walkor 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4031784 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +
+

webman

+ +基于workerman开发的超高性能PHP框架 + + +

学习

+ + + +
+ +

赞助商

+ +

特别赞助

+ + + + +

铂金赞助

+ + + + +
+ + +
+ +

请作者喝咖啡

+ + + +
+如果您觉得webman对您有所帮助,欢迎捐赠。 + + +
+ + +
+

LICENSE

+The webman is open-sourced software licensed under the MIT. +
+ +
+ + diff --git a/app/controller/DistillationWordController.php b/app/controller/DistillationWordController.php new file mode 100644 index 0000000..b44caf8 --- /dev/null +++ b/app/controller/DistillationWordController.php @@ -0,0 +1,23 @@ + '', + ]); + return success($service->listData($params)); + } + + public function read(DistillationWordService $service): Response + { + $id = input('id'); + return success($service->readData($id)); + } + +} \ No newline at end of file diff --git a/app/dao/DistillationWordDao.php b/app/dao/DistillationWordDao.php new file mode 100644 index 0000000..28fd6bb --- /dev/null +++ b/app/dao/DistillationWordDao.php @@ -0,0 +1,17 @@ + + * @copyright walkor + * @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('

403 forbidden

', 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; + } +} diff --git a/app/model/DistillationExpandWord.php b/app/model/DistillationExpandWord.php new file mode 100644 index 0000000..f112d1c --- /dev/null +++ b/app/model/DistillationExpandWord.php @@ -0,0 +1,33 @@ + + * @copyright walkor + * @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; + } + +} diff --git a/app/route/route.php b/app/route/route.php new file mode 100644 index 0000000..1fd5fcc --- /dev/null +++ b/app/route/route.php @@ -0,0 +1,32 @@ +middleware([ + AdminAuthorizationMiddleware::class, + PermissionsMiddleware::class +]); \ No newline at end of file diff --git a/app/service/DistillationWordService.php b/app/service/DistillationWordService.php new file mode 100644 index 0000000..039c5d6 --- /dev/null +++ b/app/service/DistillationWordService.php @@ -0,0 +1,54 @@ +dao = $dao; + } + + /** + * 获取前台用户列表 + * @param array $params + * @return array + */ + public function listData(array $params): array + { + [$page, $limit] = RequestUtils::getPageParameter(); + [$sortRule, $sortField] = RequestUtils::getSortParameter(); + + $query = [ + 'delete_time' => 0 + ]; + if (isNotBlank($params['name'])) { + $query[] = ['name', 'like', '%' . $params['name'] . '%']; + } + $list = $this->dao->getList($query, '*', $page, $limit, "$sortField $sortRule"); + $count = $this->dao->getCount($query); + return compact('list', 'count'); + } + + /** + * 获取用户信息 + * @param mixed $id + * @return array + */ + public function readData(mixed $id): array + { + $user = $this->dao->get(['id' => $id]); + if (empty($user)) { + throw new ApiException('数据不存在'); + } + return $user->toArray(); + } +} \ No newline at end of file diff --git a/app/validate/UserGroupValidate.php b/app/validate/UserGroupValidate.php new file mode 100644 index 0000000..28276fc --- /dev/null +++ b/app/validate/UserGroupValidate.php @@ -0,0 +1,28 @@ + [ + 'name' => 'require|max:90', + 'trans_words' => 'require|in:1,0', + 'expand_words' => 'require|in:1,2', + ], + 'update' => [ + 'id' => 'require', + 'name' => 'require|max:90', + 'trans_words' => 'require|in:1,0', + 'expand_words' => 'require|in:1,2', + ] + ]; + protected $message = [ + 'id.require' => 'ID不能为空', + 'name.require' => '主关键词不能为空', + 'trans_words.require' => '目标转化词不能为空', + 'expand_words.require' => '扩展词不能为空', + ]; +} \ No newline at end of file diff --git a/app/view/index/view.html b/app/view/index/view.html new file mode 100644 index 0000000..67ebb26 --- /dev/null +++ b/app/view/index/view.html @@ -0,0 +1,14 @@ + + + + + + + + webman + + + +hello + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..bfc1b12 --- /dev/null +++ b/composer.json @@ -0,0 +1,65 @@ +{ + "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", + "suqflash/piadmin": "^0.0.5", + "webman/database": "^2.1", + "illuminate/pagination": "^12.26", + "illuminate/events": "^12.26", + "symfony/var-dumper": "^7.3", + "x2nx/webman-migrate": "^0.0.8", + "tinywan/storage": "^1.1", + "aliyuncs/oss-sdk-php": "^2.7", + "ext-simplexml": "*", + "ext-dom": "*" + }, + "suggest": { + "ext-event": "For better performance. " + }, + "autoload": { + "psr-4": { + "": "./", + "app\\": "./app", + "App\\": "./app", + "app\\View\\Components\\": "./app/view/components" + } + }, + "scripts": { + "post-package-install": [ + "support\\Plugin::install" + ], + "post-package-update": [ + "support\\Plugin::install" + ], + "pre-package-uninstall": [ + "support\\Plugin::uninstall" + ] + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..6310bb5 --- /dev/null +++ b/composer.lock @@ -0,0 +1,7563 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "42f530dc213b8dcd54c50acea790541d", + "packages": [ + { + "name": "aliyuncs/oss-sdk-php", + "version": "v2.7.2", + "source": { + "type": "git", + "url": "https://github.com/aliyun/aliyun-oss-php-sdk.git", + "reference": "483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aliyun/aliyun-oss-php-sdk/zipball/483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5", + "reference": "483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "php-coveralls/php-coveralls": "*", + "phpunit/phpunit": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "OSS\\": "src/OSS" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aliyuncs", + "homepage": "http://www.aliyun.com" + } + ], + "description": "Aliyun OSS SDK for PHP", + "homepage": "http://www.aliyun.com/product/oss/", + "support": { + "issues": "https://github.com/aliyun/aliyun-oss-php-sdk/issues", + "source": "https://github.com/aliyun/aliyun-oss-php-sdk/tree/v2.7.2" + }, + "time": "2024-10-28T10:41:12+00:00" + }, + { + "name": "brick/math", + "version": "0.14.0", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.14.0" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2025-08-29T12:40:03+00:00" + }, + { + "name": "cakephp/chronos", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/cakephp/chronos.git", + "reference": "6c820947bc1372a250288ab164ec1b3bb7afab39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/chronos/zipball/6c820947bc1372a250288ab164ec1b3bb7afab39", + "reference": "6c820947bc1372a250288ab164ec1b3bb7afab39", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "cakephp/cakephp-codesniffer": "^5.0", + "phpunit/phpunit": "^10.1.0 || ^11.1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cake\\Chronos\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "http://nesbot.com" + }, + { + "name": "The CakePHP Team", + "homepage": "https://cakephp.org" + } + ], + "description": "A simple API extension for DateTime.", + "homepage": "https://cakephp.org", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "issues": "https://github.com/cakephp/chronos/issues", + "source": "https://github.com/cakephp/chronos" + }, + "time": "2025-06-28T11:35:59+00:00" + }, + { + "name": "cakephp/core", + "version": "5.2.9", + "source": { + "type": "git", + "url": "https://github.com/cakephp/core.git", + "reference": "231d67d9e192491e80f8e3f367822dbadcb6d15a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/core/zipball/231d67d9e192491e80f8e3f367822dbadcb6d15a", + "reference": "231d67d9e192491e80f8e3f367822dbadcb6d15a", + "shasum": "" + }, + "require": { + "cakephp/utility": "5.2.*@dev", + "league/container": "^4.2", + "php": ">=8.1", + "psr/container": "^1.1 || ^2.0" + }, + "provide": { + "psr/container-implementation": "^2.0" + }, + "suggest": { + "cakephp/cache": "To use Configure::store() and restore().", + "cakephp/event": "To use PluginApplicationInterface or plugin applications.", + "league/container": "To use Container and ServiceProvider classes" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-5.x": "5.2.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Cake\\Core\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/core/graphs/contributors" + } + ], + "description": "CakePHP Framework Core classes", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "core", + "framework" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/core" + }, + "time": "2025-08-30T05:23:22+00:00" + }, + { + "name": "cakephp/database", + "version": "5.2.9", + "source": { + "type": "git", + "url": "https://github.com/cakephp/database.git", + "reference": "3027321fdd696cba09b1cad536f89e90f6c6693f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/database/zipball/3027321fdd696cba09b1cad536f89e90f6c6693f", + "reference": "3027321fdd696cba09b1cad536f89e90f6c6693f", + "shasum": "" + }, + "require": { + "cakephp/chronos": "^3.1", + "cakephp/core": "5.2.*@dev", + "cakephp/datasource": "5.2.*@dev", + "php": ">=8.1", + "psr/log": "^3.0" + }, + "require-dev": { + "cakephp/i18n": "5.2.*@dev", + "cakephp/log": "5.2.*@dev" + }, + "suggest": { + "cakephp/i18n": "If you are using locale-aware datetime formats.", + "cakephp/log": "If you want to use query logging without providing a logger yourself." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-5.x": "5.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cake\\Database\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/database/graphs/contributors" + } + ], + "description": "Flexible and powerful Database abstraction library with a familiar PDO-like API", + "homepage": "https://cakephp.org", + "keywords": [ + "abstraction", + "cakephp", + "database", + "database abstraction", + "pdo" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/database" + }, + "time": "2025-10-15T09:58:33+00:00" + }, + { + "name": "cakephp/datasource", + "version": "5.2.9", + "source": { + "type": "git", + "url": "https://github.com/cakephp/datasource.git", + "reference": "35cd45fdea18854e4f8fd7bdb5fa487d104a8efd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/datasource/zipball/35cd45fdea18854e4f8fd7bdb5fa487d104a8efd", + "reference": "35cd45fdea18854e4f8fd7bdb5fa487d104a8efd", + "shasum": "" + }, + "require": { + "cakephp/core": "5.2.*@dev", + "php": ">=8.1", + "psr/simple-cache": "^2.0 || ^3.0" + }, + "require-dev": { + "cakephp/cache": "5.2.*@dev", + "cakephp/collection": "5.2.*@dev", + "cakephp/utility": "5.2.*@dev" + }, + "suggest": { + "cakephp/cache": "If you decide to use Query caching.", + "cakephp/collection": "If you decide to use ResultSetInterface.", + "cakephp/utility": "If you decide to use EntityTrait." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-5.x": "5.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cake\\Datasource\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/datasource/graphs/contributors" + } + ], + "description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "connection management", + "datasource", + "entity", + "query" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/datasource" + }, + "time": "2025-09-04T00:13:11+00:00" + }, + { + "name": "cakephp/utility", + "version": "5.2.9", + "source": { + "type": "git", + "url": "https://github.com/cakephp/utility.git", + "reference": "9d2bafa62f457084b7ce4737f2f71d2a40fc6812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/utility/zipball/9d2bafa62f457084b7ce4737f2f71d2a40fc6812", + "reference": "9d2bafa62f457084b7ce4737f2f71d2a40fc6812", + "shasum": "" + }, + "require": { + "cakephp/core": "5.2.*@dev", + "php": ">=8.1" + }, + "suggest": { + "ext-intl": "To use Text::transliterate() or Text::slug()", + "lib-ICU": "To use Text::transliterate() or Text::slug()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-5.x": "5.2.x-dev" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Cake\\Utility\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/utility/graphs/contributors" + } + ], + "description": "CakePHP Utility classes such as Inflector, String, Hash, and Security", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "hash", + "inflector", + "security", + "string", + "utility" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/utility" + }, + "time": "2025-09-06T07:02:20+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/dbal": "<4.0.0 || >=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "doctrine/annotations", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/901c2ee5d26eb64ff43c47976e114bf00843acf7", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2 || ^3", + "ext-tokenizer": "*", + "php": "^7.2 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^2.0", + "doctrine/coding-standard": "^10", + "phpstan/phpstan": "^1.10.28", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "symfony/cache": "^5.4 || ^6.4 || ^7", + "vimeo/psalm": "^4.30 || ^5.14" + }, + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/2.0.2" + }, + "abandoned": true, + "time": "2024-09-05T10:17:24+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v6.11.1", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" + }, + "time": "2025-04-09T20:32:01+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "symfony/http-foundation": "^4.4|^5.4|^6|^7" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2023-10-12T05:21:21+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:45:45+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "21dc724a0583619cd1652f673303492272778051" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.8.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-08-23T21:21:41+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:27:06+00:00" + }, + { + "name": "illuminate/bus", + "version": "v12.35.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/bus.git", + "reference": "ca6fc60c47a521209ca67a3bc12f22706bd4c188" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/bus/zipball/ca6fc60c47a521209ca67a3bc12f22706bd4c188", + "reference": "ca6fc60c47a521209ca67a3bc12f22706bd4c188", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/pipeline": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "suggest": { + "illuminate/queue": "Required to use closures when chaining jobs (^12.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Bus\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Bus package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-10-13T16:31:35+00:00" + }, + { + "name": "illuminate/collections", + "version": "v12.35.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/collections.git", + "reference": "b323866d9e571f8c444f3ccca6f645c05fadf568" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/collections/zipball/b323866d9e571f8c444f3ccca6f645c05fadf568", + "reference": "b323866d9e571f8c444f3ccca6f645c05fadf568", + "shasum": "" + }, + "require": { + "illuminate/conditionable": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "php": "^8.2", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33" + }, + "suggest": { + "illuminate/http": "Required to convert collections to API resources (^12.0).", + "symfony/var-dumper": "Required to use the dump method (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php", + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Collections package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-10-10T13:31:43+00:00" + }, + { + "name": "illuminate/conditionable", + "version": "v12.35.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/conditionable.git", + "reference": "ec677967c1f2faf90b8428919124d2184a4c9b49" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/conditionable/zipball/ec677967c1f2faf90b8428919124d2184a4c9b49", + "reference": "ec677967c1f2faf90b8428919124d2184a4c9b49", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Conditionable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-05-13T15:08:45+00:00" + }, + { + "name": "illuminate/console", + "version": "v12.35.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/console.git", + "reference": "ace3eb0f3a60c82adb7d640912a0d83b5ff44894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/console/zipball/ace3eb0f3a60c82adb7d640912a0d83b5ff44894", + "reference": "ace3eb0f3a60c82adb7d640912a0d83b5ff44894", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "illuminate/view": "^12.0", + "laravel/prompts": "^0.3.0", + "nunomaduro/termwind": "^2.0", + "php": "^8.2", + "symfony/console": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/process": "^7.2.0" + }, + "suggest": { + "dragonmantank/cron-expression": "Required to use scheduler (^3.3.2).", + "ext-pcntl": "Required to use signal trapping.", + "guzzlehttp/guzzle": "Required to use the ping methods on schedules (^7.8).", + "illuminate/bus": "Required to use the scheduled job dispatcher (^12.0).", + "illuminate/container": "Required to use the scheduler (^12.0).", + "illuminate/filesystem": "Required to use the generator command (^12.0).", + "illuminate/queue": "Required to use closures for scheduled jobs (^12.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Console\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Console package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-10-13T16:30:36+00:00" + }, + { + "name": "illuminate/container", + "version": "v12.35.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/container.git", + "reference": "d6eaa8afd48dbe16b6b3c412a87479cad67eeb12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/container/zipball/d6eaa8afd48dbe16b6b3c412a87479cad67eeb12", + "reference": "d6eaa8afd48dbe16b6b3c412a87479cad67eeb12", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^12.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0" + }, + "suggest": { + "illuminate/auth": "Required to use the Auth attribute", + "illuminate/cache": "Required to use the Cache attribute", + "illuminate/config": "Required to use the Config attribute", + "illuminate/database": "Required to use the DB attribute", + "illuminate/filesystem": "Required to use the Storage attribute", + "illuminate/log": "Required to use the Log or Context attributes" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Container\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Container package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-09-12T14:35:11+00:00" + }, + { + "name": "illuminate/contracts", + "version": "v12.35.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/contracts.git", + "reference": "5ab717c8f0dd4e84be703796bbb415ccff8de57a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/5ab717c8f0dd4e84be703796bbb415ccff8de57a", + "reference": "5ab717c8f0dd4e84be703796bbb415ccff8de57a", + "shasum": "" + }, + "require": { + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/simple-cache": "^1.0|^2.0|^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Contracts\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Contracts package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-10-07T19:59:08+00:00" + }, + { + "name": "illuminate/database", + "version": "v12.35.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/database.git", + "reference": "5f663ea61a13c1776e3353aa9ef5153221f44374" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/database/zipball/5f663ea61a13c1776e3353aa9ef5153221f44374", + "reference": "5f663ea61a13c1776e3353aa9ef5153221f44374", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13|^0.14", + "ext-pdo": "*", + "illuminate/collections": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "laravel/serializable-closure": "^1.3|^2.0", + "php": "^8.2", + "symfony/polyfill-php85": "^1.33" + }, + "suggest": { + "ext-filter": "Required to use the Postgres database driver.", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.24).", + "illuminate/console": "Required to use the database commands (^12.0).", + "illuminate/events": "Required to use the observers with Eloquent (^12.0).", + "illuminate/filesystem": "Required to use the migrations (^12.0).", + "illuminate/http": "Required to convert Eloquent models to API resources (^12.0).", + "illuminate/pagination": "Required to paginate the result set (^12.0).", + "symfony/finder": "Required to use Eloquent model factories (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Database\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Database package.", + "homepage": "https://laravel.com", + "keywords": [ + "database", + "laravel", + "orm", + "sql" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-10-20T21:28:57+00:00" + }, + { + "name": "illuminate/events", + "version": "v12.35.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/events.git", + "reference": "ba94fc7c734864e1eba802e75930725fd8074fce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/events/zipball/ba94fc7c734864e1eba802e75930725fd8074fce", + "reference": "ba94fc7c734864e1eba802e75930725fd8074fce", + "shasum": "" + }, + "require": { + "illuminate/bus": "^12.0", + "illuminate/collections": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Illuminate\\Events\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Events package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-09-30T10:20:25+00:00" + }, + { + "name": "illuminate/filesystem", + "version": "v12.35.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/filesystem.git", + "reference": "c7c3bbcd05c0836af89f1b49bf70c3170c011c13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/filesystem/zipball/c7c3bbcd05c0836af89f1b49bf70c3170c011c13", + "reference": "c7c3bbcd05c0836af89f1b49bf70c3170c011c13", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/finder": "^7.2.0" + }, + "suggest": { + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-hash": "Required to use the Filesystem class.", + "illuminate/http": "Required for handling uploaded files (^12.0).", + "league/flysystem": "Required to use the Flysystem local driver (^3.25.1).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/mime": "Required to enable support for guessing extensions (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Illuminate\\Filesystem\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Filesystem package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-09-30T17:35:38+00:00" + }, + { + "name": "illuminate/http", + "version": "v12.35.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/http.git", + "reference": "b46b5d316276b7215ddce91f5334835cd3e052c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/http/zipball/b46b5d316276b7215ddce91f5334835cd3e052c7", + "reference": "b46b5d316276b7215ddce91f5334835cd3e052c7", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "illuminate/collections": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/session": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php85": "^1.33" + }, + "suggest": { + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image()." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Http\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Http package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-10-14T13:45:31+00:00" + }, + { + "name": "illuminate/macroable", + "version": "v12.35.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/macroable.git", + "reference": "e862e5648ee34004fa56046b746f490dfa86c613" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/macroable/zipball/e862e5648ee34004fa56046b746f490dfa86c613", + "reference": "e862e5648ee34004fa56046b746f490dfa86c613", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Macroable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2024-07-23T16:31:01+00:00" + }, + { + "name": "illuminate/pagination", + "version": "v12.35.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/pagination.git", + "reference": "1d95e70671177108b202e6ceb61bc7bc9924bdbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/pagination/zipball/1d95e70671177108b202e6ceb61bc7bc9924bdbf", + "reference": "1d95e70671177108b202e6ceb61bc7bc9924bdbf", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Pagination\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Pagination package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-09-02T15:31:06+00:00" + }, + { + "name": "illuminate/pipeline", + "version": "v12.35.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/pipeline.git", + "reference": "b6a14c20d69a44bf0a6fba664a00d23ca71770ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/pipeline/zipball/b6a14c20d69a44bf0a6fba664a00d23ca71770ee", + "reference": "b6a14c20d69a44bf0a6fba664a00d23ca71770ee", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "suggest": { + "illuminate/database": "Required to use database transactions (^12.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Pipeline\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Pipeline package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-08-20T13:36:50+00:00" + }, + { + "name": "illuminate/redis", + "version": "v12.35.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/redis.git", + "reference": "ce8129e486563b944d80ae159023d9e970bacd04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/redis/zipball/ce8129e486563b944d80ae159023d9e970bacd04", + "reference": "ce8129e486563b944d80ae159023d9e970bacd04", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "suggest": { + "ext-redis": "Required to use the phpredis connector (^4.0|^5.0|^6.0).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Redis\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Redis package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-09-10T14:35:16+00:00" + }, + { + "name": "illuminate/session", + "version": "v12.35.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/session.git", + "reference": "5f42c194f3b43ad72fbf1ec5ee0874cbc0dbc146" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/session/zipball/5f42c194f3b43ad72fbf1ec5ee0874cbc0dbc146", + "reference": "5f42c194f3b43ad72fbf1ec5ee0874cbc0dbc146", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-session": "*", + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/filesystem": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0" + }, + "suggest": { + "illuminate/console": "Required to use the session:table command (^12.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Session\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Session package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-10-03T21:09:42+00:00" + }, + { + "name": "illuminate/support", + "version": "v12.35.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/support.git", + "reference": "eefcefcf6edff2c986f746f10fad3da3e79a1223" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/support/zipball/eefcefcf6edff2c986f746f10fad3da3e79a1223", + "reference": "eefcefcf6edff2c986f746f10fad3da3e79a1223", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-mbstring": "*", + "illuminate/collections": "^12.0", + "illuminate/conditionable": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "nesbot/carbon": "^3.8.4", + "php": "^8.2", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php85": "^1.33", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "replace": { + "spatie/once": "*" + }, + "suggest": { + "illuminate/filesystem": "Required to use the Composer class (^12.0).", + "laravel/serializable-closure": "Required to use the once function (^1.3|^2.0).", + "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.7).", + "league/uri": "Required to use the Uri class (^7.5.1).", + "ramsey/uuid": "Required to use Str::uuid() (^4.7).", + "symfony/process": "Required to use the Composer class (^7.2).", + "symfony/uid": "Required to use Str::ulid() (^7.2).", + "symfony/var-dumper": "Required to use the dd function (^7.2).", + "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.6.1)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php", + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Support package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-10-18T13:23:12+00:00" + }, + { + "name": "illuminate/view", + "version": "v12.35.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/view.git", + "reference": "7dd37bf2c957a54e9e580a097035685c49c3b9ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/view/zipball/7dd37bf2c957a54e9e580a097035685c49c3b9ca", + "reference": "7dd37bf2c957a54e9e580a097035685c49c3b9ca", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "illuminate/collections": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/events": "^12.0", + "illuminate/filesystem": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\View\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate View package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-10-04T23:32:04+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.7", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/a1891d362714bc40c8d23b0b1d7090f022ea27cc", + "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.7" + }, + "time": "2025-09-19T13:47:56+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.6", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "038ce42edee619599a1debb7e81d7b3759492819" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/038ce42edee619599a1debb7e81d7b3759492819", + "reference": "038ce42edee619599a1debb7e81d7b3759492819", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2025-10-09T13:42:30+00:00" + }, + { + "name": "league/container", + "version": "4.2.5", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/container.git", + "reference": "d3cebb0ff4685ff61c749e54b27db49319e2ec00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/container/zipball/d3cebb0ff4685ff61c749e54b27db49319e2ec00", + "reference": "d3cebb0ff4685ff61c749e54b27db49319e2ec00", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "psr/container": "^1.1 || ^2.0" + }, + "provide": { + "psr/container-implementation": "^1.0" + }, + "replace": { + "orno/di": "~2.0" + }, + "require-dev": { + "nette/php-generator": "^3.4", + "nikic/php-parser": "^4.10", + "phpstan/phpstan": "^0.12.47", + "phpunit/phpunit": "^8.5.17", + "roave/security-advisories": "dev-latest", + "scrutinizer/ocular": "^1.8", + "squizlabs/php_codesniffer": "^3.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev", + "dev-3.x": "3.x-dev", + "dev-4.x": "4.x-dev", + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Container\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Phil Bennett", + "email": "mail@philbennett.co.uk", + "role": "Developer" + } + ], + "description": "A fast and intuitive dependency injection container.", + "homepage": "https://github.com/thephpleague/container", + "keywords": [ + "container", + "dependency", + "di", + "injection", + "league", + "provider", + "service" + ], + "support": { + "issues": "https://github.com/thephpleague/container/issues", + "source": "https://github.com/thephpleague/container/tree/4.2.5" + }, + "funding": [ + { + "url": "https://github.com/philipobenito", + "type": "github" + } + ], + "time": "2025-05-20T12:55:37+00:00" + }, + { + "name": "monolog/monolog", + "version": "2.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "5cf826f2991858b54d5c3809bee745560a1042a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/5cf826f2991858b54d5c3809bee745560a1042a7", + "reference": "5cf826f2991858b54d5c3809bee745560a1042a7", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2@dev", + "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpspec/prophecy": "^1.15", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.38 || ^9.6.19", + "predis/predis": "^1.1 || ^2.0", + "rollbar/rollbar": "^1.3 || ^2 || ^3", + "ruflin/elastica": "^7", + "swiftmailer/swiftmailer": "^5.3|^6.0", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/2.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2024-11-12T12:43:37+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.10.3", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2025-09-06T13:39:36+00:00" + }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "FastRoute\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "support": { + "issues": "https://github.com/nikic/FastRoute/issues", + "source": "https://github.com/nikic/FastRoute/tree/master" + }, + "time": "2018-02-13T20:26:39+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.3.2", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "eb61920a53057a7debd718a5b89c2178032b52c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/eb61920a53057a7debd718a5b89c2178032b52c0", + "reference": "eb61920a53057a7debd718a5b89c2178032b52c0", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.3.4" + }, + "require-dev": { + "illuminate/console": "^11.46.1", + "laravel/pint": "^1.25.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Its like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.3.2" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2025-10-18T11:10:27+00:00" + }, + { + "name": "openspout/openspout", + "version": "v4.28.5", + "source": { + "type": "git", + "url": "https://github.com/openspout/openspout.git", + "reference": "ab05a09fe6fce57c90338f83280648a9786ce36b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/openspout/openspout/zipball/ab05a09fe6fce57c90338f83280648a9786ce36b", + "reference": "ab05a09fe6fce57c90338f83280648a9786ce36b", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-filter": "*", + "ext-libxml": "*", + "ext-xmlreader": "*", + "ext-zip": "*", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "ext-zlib": "*", + "friendsofphp/php-cs-fixer": "^3.68.3", + "infection/infection": "^0.29.10", + "phpbench/phpbench": "^1.4.0", + "phpstan/phpstan": "^2.1.2", + "phpstan/phpstan-phpunit": "^2.0.4", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^11.5.4" + }, + "suggest": { + "ext-iconv": "To handle non UTF-8 CSV files (if \"php-mbstring\" is not already installed or is too limited)", + "ext-mbstring": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "OpenSpout\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Adrien Loison", + "email": "adrien@box.com" + } + ], + "description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way", + "homepage": "https://github.com/openspout/openspout", + "keywords": [ + "OOXML", + "csv", + "excel", + "memory", + "odf", + "ods", + "office", + "open", + "php", + "read", + "scale", + "spreadsheet", + "stream", + "write", + "xlsx" + ], + "support": { + "issues": "https://github.com/openspout/openspout/issues", + "source": "https://github.com/openspout/openspout/tree/v4.28.5" + }, + "funding": [ + { + "url": "https://paypal.me/filippotessarotto", + "type": "custom" + }, + { + "url": "https://github.com/Slamdunk", + "type": "github" + } + ], + "time": "2025-01-30T13:51:11+00:00" + }, + { + "name": "php-di/invoker", + "version": "2.3.7", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/Invoker.git", + "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/3c1ddfdef181431fbc4be83378f6d036d59e81e1", + "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "psr/container": "^1.0|^2.0" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "mnapoli/hard-mode": "~0.3.0", + "phpunit/phpunit": "^9.0 || ^10 || ^11 || ^12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Invoker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Generic and extensible callable invoker", + "homepage": "https://github.com/PHP-DI/Invoker", + "keywords": [ + "callable", + "dependency", + "dependency-injection", + "injection", + "invoke", + "invoker" + ], + "support": { + "issues": "https://github.com/PHP-DI/Invoker/issues", + "source": "https://github.com/PHP-DI/Invoker/tree/2.3.7" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2025-08-30T10:22:22+00:00" + }, + { + "name": "php-di/php-di", + "version": "7.1.1", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/PHP-DI.git", + "reference": "f88054cc052e40dbe7b383c8817c19442d480352" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/f88054cc052e40dbe7b383c8817c19442d480352", + "reference": "f88054cc052e40dbe7b383c8817c19442d480352", + "shasum": "" + }, + "require": { + "laravel/serializable-closure": "^1.0 || ^2.0", + "php": ">=8.0", + "php-di/invoker": "^2.0", + "psr/container": "^1.1 || ^2.0" + }, + "provide": { + "psr/container-implementation": "^1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "friendsofphp/proxy-manager-lts": "^1", + "mnapoli/phpunit-easymock": "^1.3", + "phpunit/phpunit": "^9.6 || ^10 || ^11", + "vimeo/psalm": "^5|^6" + }, + "suggest": { + "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "DI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The dependency injection container for humans", + "homepage": "https://php-di.org/", + "keywords": [ + "PSR-11", + "container", + "container-interop", + "dependency injection", + "di", + "ioc", + "psr11" + ], + "support": { + "issues": "https://github.com/PHP-DI/PHP-DI/issues", + "source": "https://github.com/PHP-DI/PHP-DI/tree/7.1.1" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/php-di/php-di", + "type": "tidelift" + } + ], + "time": "2025-08-16T11:10:48+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.4", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-08-21T11:53:16+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.1" + }, + "time": "2025-09-04T20:59:21+00:00" + }, + { + "name": "robmorgan/phinx", + "version": "0.16.10", + "source": { + "type": "git", + "url": "https://github.com/cakephp/phinx.git", + "reference": "83f83ec105e55e3abba7acc23c0272b5fcf66929" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/phinx/zipball/83f83ec105e55e3abba7acc23c0272b5fcf66929", + "reference": "83f83ec105e55e3abba7acc23c0272b5fcf66929", + "shasum": "" + }, + "require": { + "cakephp/database": "^5.0.2", + "composer-runtime-api": "^2.0", + "php-64bit": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/config": "^4.0|^5.0|^6.0|^7.0", + "symfony/console": "^6.0|^7.0" + }, + "require-dev": { + "cakephp/cakephp-codesniffer": "^5.0", + "cakephp/i18n": "^5.0", + "ext-json": "*", + "ext-pdo": "*", + "phpunit/phpunit": "^9.5.19", + "symfony/yaml": "^4.0|^5.0|^6.0|^7.0" + }, + "suggest": { + "ext-json": "Install if using JSON configuration format", + "ext-pdo": "PDO extension is needed", + "symfony/yaml": "Install if using YAML configuration format" + }, + "bin": [ + "bin/phinx" + ], + "type": "library", + "autoload": { + "psr-4": { + "Phinx\\": "src/Phinx/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Morgan", + "email": "robbym@gmail.com", + "homepage": "https://robmorgan.id.au", + "role": "Lead Developer" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com", + "homepage": "https://shadowhand.me", + "role": "Developer" + }, + { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Developer" + }, + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/phinx/graphs/contributors", + "role": "Developer" + } + ], + "description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.", + "homepage": "https://phinx.org", + "keywords": [ + "database", + "database migrations", + "db", + "migrations", + "phinx" + ], + "support": { + "issues": "https://github.com/cakephp/phinx/issues", + "source": "https://github.com/cakephp/phinx/tree/0.16.10" + }, + "time": "2025-07-08T18:55:28+00:00" + }, + { + "name": "suqflash/piadmin", + "version": "v0.0.5", + "source": { + "type": "git", + "url": "https://github.com/suq-flash/piadmin.git", + "reference": "e899bb8e27aa3692b5dd12cfa05bcb4ca1b924c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/suq-flash/piadmin/zipball/e899bb8e27aa3692b5dd12cfa05bcb4ca1b924c8", + "reference": "e899bb8e27aa3692b5dd12cfa05bcb4ca1b924c8", + "shasum": "" + }, + "require": { + "doctrine/annotations": ">=1.14", + "guzzlehttp/guzzle": "^7.9", + "openspout/openspout": "^4.0", + "php": ">=8.1", + "php-di/php-di": "^6.3 || ^7.0", + "psr/container": ">=1.0", + "ramsey/uuid": "^4.9.0", + "symfony/translation": "^6.4.23 || ^7.3", + "tinywan/jwt": "^1.11", + "tinywan/storage": "^1.1", + "topthink/think-orm": "^2.0.53 || ^3.0.0", + "topthink/think-validate": "^3.0", + "vlucas/phpdotenv": "^5.6", + "webman/cache": "^2.1", + "webman/captcha": "^1.0", + "webman/console": "^2.1.8", + "webman/event": "^1.0", + "webman/redis": "^2.1", + "webman/think-orm": "^2.1", + "workerman/crontab": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Suqflash\\Piadmin\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Webman plugin suqflash/piadmin", + "support": { + "issues": "https://github.com/suq-flash/piadmin/issues", + "source": "https://github.com/suq-flash/piadmin/tree/v0.0.5" + }, + "time": "2025-07-30T08:07:33+00:00" + }, + { + "name": "symfony/cache", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "bf8afc8ffd4bfd3d9c373e417f041d9f1e5b863f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/bf8afc8ffd4bfd3d9c373e417f041d9f1e5b863f", + "reference": "bf8afc8ffd4bfd3d9c373e417f041d9f1e5b863f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^3.6", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.4|^7.0" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/filesystem": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:12:26+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/5d68a57d66910405e5c0b63d6f0af941e66fc868", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-13T15:25:07+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/config", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "8a09223170046d2cfda3d2e11af01df2c641e961" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/8a09223170046d2cfda3d2e11af01df2c641e961", + "reference": "8a09223170046d2cfda3d2e11af01df2c641e961", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.1", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-22T12:46:16+00:00" + }, + { + "name": "symfony/console", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", + "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-22T15:31:00+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/99f81bc944ab8e5dae4f21b4ca9972698bbad0e4", + "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^6.4|^7.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:12:26+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-13T11:49:31+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-07T08:17:47+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", + "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T13:41:35+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "c061c7c18918b1b64268771aad04b40be41dd2e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/c061c7c18918b1b64268771aad04b40be41dd2e6", + "reference": "c061c7c18918b1b64268771aad04b40be41dd2e6", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/clock": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-16T08:38:17+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "b796dffea7821f035047235e076b60ca2446e3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b796dffea7821f035047235e076b60ca2446e3cf", + "reference": "b796dffea7821f035047235e076b60ca2446e3cf", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^7.3", + "symfony/http-foundation": "^7.3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^7.1", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^7.1", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-27T12:32:17+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35", + "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-16T08:38:17+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, + { + "name": "symfony/process", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:12:26+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-25T09:37:31+00:00" + }, + { + "name": "symfony/string", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "f96476035142921000338bad71e5247fbc138872" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T14:36:48+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "ec25870502d0c7072d086e8ffba1420c85965174" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174", + "reference": "ec25870502d0c7072d086e8ffba1420c85965174", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-07T11:39:36+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-27T08:32:26+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", + "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:12:26+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", + "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:12:26+00:00" + }, + { + "name": "tinywan/jwt", + "version": "v1.13.0", + "source": { + "type": "git", + "url": "https://github.com/Tinywan/webman-jwt.git", + "reference": "19d47c89b1bfdbeb8b0360956eab611aaee6ebf3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Tinywan/webman-jwt/zipball/19d47c89b1bfdbeb8b0360956eab611aaee6ebf3", + "reference": "19d47c89b1bfdbeb8b0360956eab611aaee6ebf3", + "shasum": "" + }, + "require": { + "ext-json": "*", + "firebase/php-jwt": "^6.8", + "php": "^7.1||^8.0", + "workerman/webman-framework": "^1.2.1||^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.6", + "illuminate/database": "^8.83", + "mockery/mockery": "^1.5", + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9.0", + "topthink/think-orm": "^2.0", + "vimeo/psalm": "^4.22", + "workerman/webman": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Tinywan\\Jwt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "JSON Web Token (JWT) for webman plugin", + "support": { + "issues": "https://github.com/Tinywan/webman-jwt/issues", + "source": "https://github.com/Tinywan/webman-jwt/tree/v1.13.0" + }, + "time": "2025-09-02T07:39:55+00:00" + }, + { + "name": "tinywan/storage", + "version": "v1.1.2", + "source": { + "type": "git", + "url": "https://github.com/Tinywan/webman-storage.git", + "reference": "a39584356829860fd2acab396ded3e6d730c71ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Tinywan/webman-storage/zipball/a39584356829860fd2acab396ded3e6d730c71ae", + "reference": "a39584356829860fd2acab396ded3e6d730c71ae", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "workerman/webman-framework": "^1.2.1||^2.0" + }, + "require-dev": { + "aliyuncs/oss-sdk-php": "^2.4", + "friendsofphp/php-cs-fixer": "^3.6", + "league/flysystem-aws-s3-v3": "^1.0", + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9.5", + "qcloud/cos-sdk-v5": "^2.5", + "qiniu/php-sdk": "^7.4", + "workerman/webman": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Tinywan\\Storage\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "webman storage plugin", + "support": { + "issues": "https://github.com/Tinywan/webman-storage/issues", + "source": "https://github.com/Tinywan/webman-storage/tree/v1.1.2" + }, + "time": "2025-08-21T02:16:10+00:00" + }, + { + "name": "topthink/think-container", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-container.git", + "reference": "b2df244be1e7399ad4c8be1ccc40ed57868f730a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-container/zipball/b2df244be1e7399ad4c8be1ccc40ed57868f730a", + "reference": "b2df244be1e7399ad4c8be1ccc40ed57868f730a", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "psr/container": "^2.0", + "topthink/think-helper": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "files": [], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "PHP Container & Facade Manager", + "support": { + "issues": "https://github.com/top-think/think-container/issues", + "source": "https://github.com/top-think/think-container/tree/v3.0.2" + }, + "time": "2025-04-07T03:21:51+00:00" + }, + { + "name": "topthink/think-helper", + "version": "v3.1.11", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-helper.git", + "reference": "1d6ada9b9f3130046bf6922fe1bd159c8d88a33c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-helper/zipball/1d6ada9b9f3130046bf6922fe1bd159c8d88a33c", + "reference": "1d6ada9b9f3130046bf6922fe1bd159c8d88a33c", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/helper.php" + ], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "The ThinkPHP6 Helper Package", + "support": { + "issues": "https://github.com/top-think/think-helper/issues", + "source": "https://github.com/top-think/think-helper/tree/v3.1.11" + }, + "time": "2025-04-07T06:55:59+00:00" + }, + { + "name": "topthink/think-orm", + "version": "v3.0.34", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-orm.git", + "reference": "715e55da149fe32a12d68ef10e5b00e70bd3dbec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-orm/zipball/715e55da149fe32a12d68ef10e5b00e70bd3dbec", + "reference": "715e55da149fe32a12d68ef10e5b00e70bd3dbec", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pdo": "*", + "php": ">=8.0.0", + "psr/log": ">=1.0", + "psr/simple-cache": ">=1.0", + "topthink/think-helper": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6|^10" + }, + "suggest": { + "ext-mongodb": "provide mongodb support" + }, + "type": "library", + "autoload": { + "files": [ + "stubs/load_stubs.php" + ], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "the PHP Database&ORM Framework", + "keywords": [ + "database", + "orm" + ], + "support": { + "issues": "https://github.com/top-think/think-orm/issues", + "source": "https://github.com/top-think/think-orm/tree/v3.0.34" + }, + "time": "2025-01-14T06:03:33+00:00" + }, + { + "name": "topthink/think-validate", + "version": "v3.0.7", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-validate.git", + "reference": "85063f6d4ef8ed122f17a36179dc3e0949b30988" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-validate/zipball/85063f6d4ef8ed122f17a36179dc3e0949b30988", + "reference": "85063f6d4ef8ed122f17a36179dc3e0949b30988", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "topthink/think-container": ">=3.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/helper.php" + ], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "think validate", + "support": { + "issues": "https://github.com/top-think/think-validate/issues", + "source": "https://github.com/top-think/think-validate/tree/v3.0.7" + }, + "time": "2025-06-11T05:51:40+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.2", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.3", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-04-30T23:37:27+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2024-11-21T01:49:47+00:00" + }, + { + "name": "webman/cache", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/webman-php/cache.git", + "reference": "66a5461ea51d23364403b54ee218b736d26bb03f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/cache/zipball/66a5461ea51d23364403b54ee218b736d26bb03f", + "reference": "66a5461ea51d23364403b54ee218b736d26bb03f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/simple-cache": "^3.0", + "symfony/cache": "^6.0 || ^7.0", + "workerman/webman-framework": "^2.1 || dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "support\\": "src/support", + "Webman\\Cache\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "support": { + "issues": "https://github.com/webman-php/cache/issues", + "source": "https://github.com/webman-php/cache/tree/v2.1.2" + }, + "time": "2025-04-11T01:23:50+00:00" + }, + { + "name": "webman/captcha", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/webman-php/captcha.git", + "reference": "0b2645b813466e4e70bff311511364080bad2ec5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/captcha/zipball/0b2645b813466e4e70bff311511364080bad2ec5", + "reference": "0b2645b813466e4e70bff311511364080bad2ec5", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "ext-mbstring": "*", + "php": ">=7.2" + }, + "require-dev": { + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webman\\Captcha\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net" + }, + { + "name": "Grégoire Passault", + "email": "g.passault@gmail.com", + "homepage": "http://www.gregwar.com/" + }, + { + "name": "Jeremy Livingston", + "email": "jeremy.j.livingston@gmail.com" + } + ], + "description": "Captcha generator", + "keywords": [ + "bot", + "captcha", + "spam" + ], + "support": { + "source": "https://github.com/webman-php/captcha/tree/v1.0.5" + }, + "time": "2025-03-01T08:43:36+00:00" + }, + { + "name": "webman/console", + "version": "v2.1.9", + "source": { + "type": "git", + "url": "https://github.com/webman-php/console.git", + "reference": "09c995bf3d9a136c7c471b6db837a1fd56b7d206" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/console/zipball/09c995bf3d9a136c7c471b6db837a1fd56b7d206", + "reference": "09c995bf3d9a136c7c471b6db837a1fd56b7d206", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "php": ">=8.1", + "symfony/console": ">=6.0" + }, + "require-dev": { + "workerman/webman": "^2.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webman\\Console\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net", + "homepage": "http://www.workerman.net", + "role": "Developer" + } + ], + "description": "Webman console", + "homepage": "http://www.workerman.net", + "keywords": [ + "webman console" + ], + "support": { + "email": "walkor@workerman.net", + "forum": "http://www.workerman.net/questions", + "issues": "https://github.com/webman-php/console/issues", + "source": "https://github.com/webman-php/console", + "wiki": "http://www.workerman.net/doc/webman" + }, + "time": "2025-10-14T09:11:21+00:00" + }, + { + "name": "webman/database", + "version": "v2.1.6", + "source": { + "type": "git", + "url": "https://github.com/webman-php/database.git", + "reference": "943c5a0f92d8959d9160c2ee3925112b15c308c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/database/zipball/943c5a0f92d8959d9160c2ee3925112b15c308c8", + "reference": "943c5a0f92d8959d9160c2ee3925112b15c308c8", + "shasum": "" + }, + "require": { + "illuminate/database": "^10.0 || ^11.0 || ^12.0", + "illuminate/http": "^10.0 || ^11.0 || ^12.0", + "laravel/serializable-closure": "^1.0 || ^2.0", + "workerman/webman-framework": "^2.1 || dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "support\\": "src/support", + "Webman\\Database\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Webman database", + "support": { + "issues": "https://github.com/webman-php/database/issues", + "source": "https://github.com/webman-php/database/tree/v2.1.6" + }, + "time": "2025-04-04T13:23:25+00:00" + }, + { + "name": "webman/event", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/webman-php/event.git", + "reference": "b1c3f6b70fd290e48288703d59bead0e28f9fb84" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/event/zipball/b1c3f6b70fd290e48288703d59bead0e28f9fb84", + "reference": "b1c3f6b70fd290e48288703d59bead0e28f9fb84", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webman\\Event\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Webman event plugin", + "support": { + "issues": "https://github.com/webman-php/event/issues", + "source": "https://github.com/webman-php/event/tree/v1.0.5" + }, + "time": "2023-12-04T09:22:12+00:00" + }, + { + "name": "webman/redis", + "version": "v2.1.3", + "source": { + "type": "git", + "url": "https://github.com/webman-php/redis.git", + "reference": "559eb1692d39c6fef5cf526223fff728be6c0fb9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/redis/zipball/559eb1692d39c6fef5cf526223fff728be6c0fb9", + "reference": "559eb1692d39c6fef5cf526223fff728be6c0fb9", + "shasum": "" + }, + "require": { + "illuminate/redis": "^10.0 || ^11.0 || ^12.0", + "workerman/webman-framework": "^2.1 || dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "support\\": "src/support", + "Webman\\Redis\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Webman redis", + "support": { + "issues": "https://github.com/webman-php/redis/issues", + "source": "https://github.com/webman-php/redis/tree/v2.1.3" + }, + "time": "2025-03-14T03:52:14+00:00" + }, + { + "name": "webman/think-orm", + "version": "v2.1.8", + "source": { + "type": "git", + "url": "https://github.com/webman-php/think-orm.git", + "reference": "7650102429b133c1299135cac563c0834e2652aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/think-orm/zipball/7650102429b133c1299135cac563c0834e2652aa", + "reference": "7650102429b133c1299135cac563c0834e2652aa", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "topthink/think-container": "^2.0|^3.0", + "topthink/think-orm": "^2.0.53 || ^3.0.0 || ^4.0.30 || dev-master", + "workerman/webman-framework": "^2.1 || dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "support\\": "src/support", + "Webman\\ThinkOrm\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "support": { + "issues": "https://github.com/webman-php/think-orm/issues", + "source": "https://github.com/webman-php/think-orm/tree/v2.1.8" + }, + "time": "2025-09-02T01:32:49+00:00" + }, + { + "name": "workerman/coroutine", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/workerman-php/coroutine.git", + "reference": "b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/workerman-php/coroutine/zipball/b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44", + "reference": "b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "psr/log": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Workerman\\": "src", + "Workerman\\Coroutine\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Workerman coroutine", + "support": { + "issues": "https://github.com/workerman-php/coroutine/issues", + "source": "https://github.com/workerman-php/coroutine/tree/v1.1.4" + }, + "time": "2025-10-11T15:09:08+00:00" + }, + { + "name": "workerman/crontab", + "version": "v1.0.7", + "source": { + "type": "git", + "url": "https://github.com/walkor/crontab.git", + "reference": "74f51ca8204e8eb628e57bc0e640561d570da2cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/crontab/zipball/74f51ca8204e8eb628e57bc0e640561d570da2cb", + "reference": "74f51ca8204e8eb628e57bc0e640561d570da2cb", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "workerman/workerman": ">=4.0.20" + }, + "type": "library", + "autoload": { + "psr-4": { + "Workerman\\Crontab\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net", + "homepage": "http://www.workerman.net", + "role": "Developer" + } + ], + "description": "A crontab written in PHP based on workerman", + "homepage": "http://www.workerman.net", + "keywords": [ + "crontab" + ], + "support": { + "email": "walkor@workerman.net", + "forum": "http://wenda.workerman.net/", + "issues": "https://github.com/walkor/workerman/issues", + "source": "https://github.com/walkor/crontab", + "wiki": "http://doc.workerman.net/" + }, + "time": "2025-01-15T07:20:50+00:00" + }, + { + "name": "workerman/webman-framework", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/walkor/webman-framework.git", + "reference": "f803bd867f07bb0929faef060b59a19a44186bfc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/webman-framework/zipball/f803bd867f07bb0929faef060b59a19a44186bfc", + "reference": "f803bd867f07bb0929faef060b59a19a44186bfc", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nikic/fast-route": "^1.3", + "php": ">=8.1", + "psr/container": ">=1.0", + "psr/log": "^3.0", + "workerman/workerman": "^5.1 || dev-master" + }, + "suggest": { + "ext-event": "For better performance. " + }, + "type": "library", + "autoload": { + "files": [ + "./src/support/helpers.php" + ], + "psr-4": { + "Webman\\": "./src", + "Support\\": "./src/support", + "support\\": "./src/support", + "Support\\View\\": "./src/support/view", + "Support\\Bootstrap\\": "./src/support/bootstrap", + "Support\\Exception\\": "./src/support/exception" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net", + "homepage": "https://www.workerman.net", + "role": "Developer" + } + ], + "description": "High performance HTTP Service Framework.", + "homepage": "https://www.workerman.net", + "keywords": [ + "High Performance", + "http service" + ], + "support": { + "email": "walkor@workerman.net", + "forum": "https://wenda.workerman.net/", + "issues": "https://github.com/walkor/webman/issues", + "source": "https://github.com/walkor/webman-framework", + "wiki": "https://doc.workerman.net/" + }, + "time": "2025-03-10T11:52:22+00:00" + }, + { + "name": "workerman/workerman", + "version": "v5.1.4", + "source": { + "type": "git", + "url": "https://github.com/walkor/workerman.git", + "reference": "ff4e17babdc92b16b3252060233c88f6c2e9a61a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/workerman/zipball/ff4e17babdc92b16b3252060233c88f6c2e9a61a", + "reference": "ff4e17babdc92b16b3252060233c88f6c2e9a61a", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=8.1", + "workerman/coroutine": "^1.1 || dev-main" + }, + "conflict": { + "ext-swow": "=8.1", + "ext-simplexml": "*", + "ext-dom": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..f26e358 --- /dev/null +++ b/config/app.php @@ -0,0 +1,26 @@ + + * @copyright walkor + * @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, +]; diff --git a/config/autoload.php b/config/autoload.php new file mode 100644 index 0000000..69a8135 --- /dev/null +++ b/config/autoload.php @@ -0,0 +1,21 @@ + + * @copyright walkor + * @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', + ] +]; diff --git a/config/bootstrap.php b/config/bootstrap.php new file mode 100644 index 0000000..ce7bfb2 --- /dev/null +++ b/config/bootstrap.php @@ -0,0 +1,18 @@ + + * @copyright walkor + * @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, +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..192cc31 --- /dev/null +++ b/config/cache.php @@ -0,0 +1,18 @@ + getenv('CACHE_MODE'), + 'stores' => [ + 'file' => [ + 'driver' => 'file', + 'path' => runtime_path('cache') + ], + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default' + ], + 'array' => [ + 'driver' => 'array' + ] + ] +]; \ No newline at end of file diff --git a/config/container.php b/config/container.php new file mode 100644 index 0000000..ba344c0 --- /dev/null +++ b/config/container.php @@ -0,0 +1,22 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +/** + * 初始化DI容器 + */ +$builder = new \DI\ContainerBuilder(); +$builder->addDefinitions(config('dependence', [])); +$builder->useAutowiring(true); +$builder->useAttributes(true); +return $builder->build(); \ No newline at end of file diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..fd39af4 --- /dev/null +++ b/config/database.php @@ -0,0 +1,40 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + // 默认数据库 + 'default' => 'default', + + // 各种数据库配置 + 'connections' => [ + 'default' => [ + 'driver' => env('DB_CONNECTION', 'mysql'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', 3306), + 'database' => env('DB_NAME', ''), + 'username' => env('DB_USER', ''), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => env('DB_PREFIX', ''), + 'suffix' => env('DB_SUFFIX', ''), + 'strict' => env('DB_STRICT', false), + 'engine' => env('DB_ENGINE', null), + 'options' => [ + \PDO::ATTR_TIMEOUT => env('DB_TIMEOUT', 5), + ] + ], + ], +]; diff --git a/config/dependence.php b/config/dependence.php new file mode 100644 index 0000000..8e964ed --- /dev/null +++ b/config/dependence.php @@ -0,0 +1,15 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return []; \ No newline at end of file diff --git a/config/event.php b/config/event.php new file mode 100644 index 0000000..28a3b2c --- /dev/null +++ b/config/event.php @@ -0,0 +1,5 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + '' => support\exception\Handler::class, +]; \ No newline at end of file diff --git a/config/log.php b/config/log.php new file mode 100644 index 0000000..7f05de5 --- /dev/null +++ b/config/log.php @@ -0,0 +1,32 @@ + + * @copyright walkor + * @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], + ], + ] + ], + ], +]; diff --git a/config/middleware.php b/config/middleware.php new file mode 100644 index 0000000..8e964ed --- /dev/null +++ b/config/middleware.php @@ -0,0 +1,15 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return []; \ No newline at end of file diff --git a/config/plugin/tinywan/jwt/app.php b/config/plugin/tinywan/jwt/app.php new file mode 100644 index 0000000..0bbe637 --- /dev/null +++ b/config/plugin/tinywan/jwt/app.php @@ -0,0 +1,83 @@ + true, + 'jwt' => [ + /** 算法类型 HS256、HS384、HS512、RS256、RS384、RS512、ES256、ES384、ES512、PS256、PS384、PS512 */ + 'algorithms' => 'HS256', + + /** access令牌秘钥 */ + 'access_secret_key' => '2022d3d3LmJq', + + /** access令牌过期时间,单位:秒。默认 2 小时 */ + 'access_exp' => 7200, + + /** refresh令牌秘钥 */ + 'refresh_secret_key' => '2022KTxigxc9o50c', + + /** refresh令牌过期时间,单位:秒。默认 7 天 */ + 'refresh_exp' => 604800, + + /** refresh 令牌是否禁用,默认不禁用 false */ + 'refresh_disable' => false, + + /** 令牌签发者 */ + 'iss' => 'webman.tinywan.cn', + + /** 某个时间点后才能访问,单位秒。(如:30 表示当前时间30秒后才能使用) */ + 'nbf' => 0, + + /** 时钟偏差冗余时间,单位秒。建议这个余地应该不大于几分钟 */ + 'leeway' => 60, + + /** 是否允许单设备登录,默认不允许 false */ + 'is_single_device' => false, + + /** 缓存令牌时间,单位:秒。默认 7 天 */ + 'cache_token_ttl' => 604800, + + /** 缓存令牌前缀,默认 JWT:TOKEN: */ + 'cache_token_pre' => 'JWT:TOKEN:', + + /** 缓存刷新令牌前缀,默认 JWT:REFRESH_TOKEN: */ + 'cache_refresh_token_pre' => 'JWT:REFRESH_TOKEN:', + + /** 用户信息模型 */ + 'user_model' => function ($uid) { + return []; + }, + + /** 是否支持 get 请求获取令牌 */ + 'is_support_get_token' => false, + /** GET 请求获取令牌请求key */ + 'is_support_get_token_key' => 'authorization', + + /** access令牌私钥 */ + 'access_private_key' => << << << << true, + 'storage' => [ + 'default' => 'oss', // 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' => [ + 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'pdf', + 'png', 'jpg', 'jpeg', 'gif', 'bmp' + ], // 被允许的文件类型列表 + 'exclude' => [ + 'php', 'py', 'js', 'html', 'css', 'class' + ], // 不被允许的文件类型列表 + // 本地对象存储 + 'local' => [ + 'adapter' => \Tinywan\Storage\Adapter\LocalAdapter::class, + 'root' => public_path().'/uploads/', + 'dirname' => function () { + return date('Ymd'); + }, + 'domain' => 'http://127.0.0.1:8787', + 'uri' => '/uploads/', // 如果 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', + ], + ], +]; diff --git a/config/plugin/webman/console/app.php b/config/plugin/webman/console/app.php new file mode 100644 index 0000000..074e986 --- /dev/null +++ b/config/plugin/webman/console/app.php @@ -0,0 +1,24 @@ + true, + + 'build_dir' => BASE_PATH . DIRECTORY_SEPARATOR . 'build', + + 'phar_filename' => 'webman.phar', + + 'bin_filename' => 'webman.bin', + + 'signature_algorithm'=> Phar::SHA256, //set the signature algorithm for a phar and apply it. The signature algorithm must be one of Phar::MD5, Phar::SHA1, Phar::SHA256, Phar::SHA512, or Phar::OPENSSL. + + 'private_key_file' => '', // The file path for certificate or OpenSSL private key file. + + 'exclude_pattern' => '#^(?!.*(composer.json|/.github/|/.idea/|/.git/|/.setting/|/runtime/|/vendor-bin/|/build/|/vendor/webman/admin/))(.*)$#', + + 'exclude_files' => [ + '.env', 'LICENSE', 'composer.json', 'composer.lock', 'start.php', 'webman.phar', 'webman.bin' + ], + + 'custom_ini' => ' +memory_limit = 256M + ', +]; diff --git a/config/plugin/webman/event/app.php b/config/plugin/webman/event/app.php new file mode 100644 index 0000000..8f9c426 --- /dev/null +++ b/config/plugin/webman/event/app.php @@ -0,0 +1,4 @@ + true, +]; \ No newline at end of file diff --git a/config/plugin/webman/event/bootstrap.php b/config/plugin/webman/event/bootstrap.php new file mode 100644 index 0000000..e5b09ba --- /dev/null +++ b/config/plugin/webman/event/bootstrap.php @@ -0,0 +1,17 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + Webman\Event\BootStrap::class, +]; diff --git a/config/plugin/webman/event/command.php b/config/plugin/webman/event/command.php new file mode 100644 index 0000000..e860cf7 --- /dev/null +++ b/config/plugin/webman/event/command.php @@ -0,0 +1,7 @@ + true, + 'driver' => 'migrate' +]; \ No newline at end of file diff --git a/config/plugin/x2nx/webman-migrate/command.php b/config/plugin/x2nx/webman-migrate/command.php new file mode 100644 index 0000000..92e9647 --- /dev/null +++ b/config/plugin/x2nx/webman-migrate/command.php @@ -0,0 +1,44 @@ + [ + "migrations" => "database/migrations", + "seeds" => "database/seeders" + ], + "environments" => [ + "default_migration_table" => "migrations", + "default_environment" => "development", + "development" => [ + "adapter" => getenv('DB_TYPE'), + "host" => getenv('DB_HOST', '127.0.0.1'), + "name" => getenv('DB_NAME'), + "user" => getenv('DB_USER'), + "pass" => getenv('DB_PASSWORD'), + "port" => getenv('DB_PORT'), + "charset" => 'utf8', + "table_prefix" => 'pi_', + "table_suffix" => '', + ], + 'version_order' => 'creation' + ] +]; \ No newline at end of file diff --git a/config/process.php b/config/process.php new file mode 100644 index 0000000..23f6396 --- /dev/null +++ b/config/process.php @@ -0,0 +1,62 @@ + + * @copyright walkor + * @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 [ + 'webman' => [ + 'handler' => Http::class, + 'listen' => 'http://0.0.0.0:8788', + '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 === '/', + ] + ] + ] +]; diff --git a/config/redis.php b/config/redis.php new file mode 100644 index 0000000..12caf40 --- /dev/null +++ b/config/redis.php @@ -0,0 +1,18 @@ + [ + 'password' => getenv('REDIS_PASSWORD'), + 'host' => getenv('REDIS_HOST'), + 'port' => getenv('REDIS_PORT'), + 'database' => getenv('REDIS_DB'), + 'prefix' => getenv('REDIS_PREFIX'), + 'pool' => [ + 'max_connections' => 5, + 'min_connections' => 1, + 'wait_timeout' => 3, + 'idle_timeout' => 60, + 'heartbeat_interval' => 50, + ], + ] +]; \ No newline at end of file diff --git a/config/route.php b/config/route.php new file mode 100644 index 0000000..6fb573a --- /dev/null +++ b/config/route.php @@ -0,0 +1,24 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +use Webman\Route; + +// 引入业务 +require_once base_path() . '/app/route/route.php'; + +// 处理404路由 +Route::fallback(function(){ + return error([],'Route not found'); +}); + diff --git a/config/server.php b/config/server.php new file mode 100644 index 0000000..054d01f --- /dev/null +++ b/config/server.php @@ -0,0 +1,23 @@ + + * @copyright walkor + * @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 +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..043f8c4 --- /dev/null +++ b/config/session.php @@ -0,0 +1,65 @@ + + * @copyright walkor + * @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], + +]; diff --git a/config/static.php b/config/static.php new file mode 100644 index 0000000..6313679 --- /dev/null +++ b/config/static.php @@ -0,0 +1,23 @@ + + * @copyright walkor + * @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, + ], +]; \ No newline at end of file diff --git a/config/think-orm.php b/config/think-orm.php new file mode 100644 index 0000000..17f3bf4 --- /dev/null +++ b/config/think-orm.php @@ -0,0 +1,42 @@ + 'mysql', + 'connections' => [ + 'mysql' => [ + // 数据库类型 + 'type' => getenv('DB_TYPE'), + // 服务器地址 + 'hostname' => getenv('DB_HOST'), + // 数据库名 + 'database' => getenv('DB_NAME'), + // 数据库用户名 + 'username' => getenv('DB_USER'), + // 数据库密码 + 'password' => getenv('DB_PASSWORD'), + // 数据库连接端口 + 'hostport' => getenv('DB_PORT'), + // 数据库连接参数 + 'params' => [ + // 连接超时3秒 + \PDO::ATTR_TIMEOUT => 3, + ], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => getenv('DB_PREFIX'), + // 断线重连 + 'break_reconnect' => true, + // 自定义分页类 + 'bootstrap' => '', + // 连接池配置 + 'pool' => [ + 'max_connections' => 5, // 最大连接数 + 'min_connections' => 1, // 最小连接数 + 'wait_timeout' => 3, // 从连接池获取连接等待超时时间 + 'idle_timeout' => 60, // 连接最大空闲时间,超过该时间会被回收 + 'heartbeat_interval' => 50, // 心跳检测间隔,需要小于60秒 + ], + ], + ], +]; \ No newline at end of file diff --git a/config/translation.php b/config/translation.php new file mode 100644 index 0000000..96589b2 --- /dev/null +++ b/config/translation.php @@ -0,0 +1,25 @@ + + * @copyright walkor + * @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', +]; \ No newline at end of file diff --git a/config/view.php b/config/view.php new file mode 100644 index 0000000..e3a7b85 --- /dev/null +++ b/config/view.php @@ -0,0 +1,22 @@ + + * @copyright walkor + * @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 +]; diff --git a/database/factories/UsersFactory.php b/database/factories/UsersFactory.php new file mode 100644 index 0000000..3aabcba --- /dev/null +++ b/database/factories/UsersFactory.php @@ -0,0 +1,37 @@ + + */ +class UsersFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + 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, + ]); + } +} diff --git a/database/migrations/2025_09_01_145446_create_attachment_table.php b/database/migrations/2025_09_01_145446_create_attachment_table.php new file mode 100644 index 0000000..7dfd380 --- /dev/null +++ b/database/migrations/2025_09_01_145446_create_attachment_table.php @@ -0,0 +1,37 @@ +create('attachment', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('storage')->default('1')->comment('附件引擎 1本地 2七牛 3腾讯 4阿里'); + $table->integer('type')->default(0)->comment('0 所有 1 压缩包 2 图片 3 视频 4 文件 5 音频'); + $table->string('name')->default('')->comment('文件名'); + $table->string('path')->default('')->comment('文件路径'); + $table->string('suffix')->default('')->comment('文件后缀'); + $table->string('original_name')->default('')->comment('文件原名'); + $table->float('size', 2)->default(0)->comment('文件大小'); + $table->bigInteger('creator_id')->default(0)->comment('创建者ID'); + $table->timestamps(); + $table->softDeletes(); + $table->comment('附件表'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Db::schema()->dropIfExists('attachment'); + } +}; diff --git a/database/migrations/2025_10_20_141905_create_platform_table.php b/database/migrations/2025_10_20_141905_create_platform_table.php new file mode 100644 index 0000000..3154e47 --- /dev/null +++ b/database/migrations/2025_10_20_141905_create_platform_table.php @@ -0,0 +1,33 @@ +create('platform', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('name')->default('')->comment('名称'); + $table->integer('status')->default(1)->comment('状态 1启用 0禁用'); + $table->bigInteger('create_by')->default(0)->comment('创建者ID'); + $table->bigInteger('update_by')->default(0)->comment('编辑者ID'); + $table->timestamps(); + $table->softDeletes(); + $table->comment('平台表'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Db::schema()->dropIfExists('platform'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..0fbe9a4 --- /dev/null +++ b/database/seeders/DatabaseSeeder.php @@ -0,0 +1,40 @@ + 'admin', + 'email' => Str::random(10).'@qq.com', + 'password' => ('') + ] + */ + Users::factory(10)->create(); + } +} \ No newline at end of file diff --git a/plugin/piadmin/.idea/.gitignore b/plugin/piadmin/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/plugin/piadmin/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/plugin/piadmin/.idea/modules.xml b/plugin/piadmin/.idea/modules.xml new file mode 100644 index 0000000..f59672d --- /dev/null +++ b/plugin/piadmin/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/plugin/piadmin/.idea/php.xml b/plugin/piadmin/.idea/php.xml new file mode 100644 index 0000000..29059d0 --- /dev/null +++ b/plugin/piadmin/.idea/php.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugin/piadmin/.idea/vcs.xml b/plugin/piadmin/.idea/vcs.xml new file mode 100644 index 0000000..c2365ab --- /dev/null +++ b/plugin/piadmin/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/plugin/piadmin/api/Install.php b/plugin/piadmin/api/Install.php new file mode 100644 index 0000000..324a124 --- /dev/null +++ b/plugin/piadmin/api/Install.php @@ -0,0 +1,437 @@ + 'mysql', + 'connections' => [ + 'mysql' => [ + // 数据库类型 + 'type' => getenv('DB_TYPE'), + // 服务器地址 + 'hostname' => getenv('DB_HOST'), + // 数据库名 + 'database' => getenv('DB_NAME'), + // 数据库用户名 + 'username' => getenv('DB_USER'), + // 数据库密码 + 'password' => getenv('DB_PASSWORD'), + // 数据库连接端口 + 'hostport' => getenv('DB_PORT'), + // 数据库连接参数 + 'params' => [ + // 连接超时3秒 + \PDO::ATTR_TIMEOUT => 3, + ], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => getenv('DB_PREFIX'), + // 断线重连 + 'break_reconnect' => true, + // 自定义分页类 + 'bootstrap' => '', + // 连接池配置 + 'pool' => [ + 'max_connections' => 5, // 最大连接数 + 'min_connections' => 1, // 最小连接数 + 'wait_timeout' => 3, // 从连接池获取连接等待超时时间 + 'idle_timeout' => 60, // 连接最大空闲时间,超过该时间会被回收 + 'heartbeat_interval' => 50, // 心跳检测间隔,需要小于60秒 + ], + ], + ], +]; +EOF; + file_put_contents(base_path() . '/config/think-orm.php', $think_orm_config); + + // 2、chache配置文件 + $cache_config = << getenv('CACHE_MODE'), + 'stores' => [ + 'file' => [ + 'driver' => 'file', + 'path' => runtime_path('cache') + ], + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default' + ], + 'array' => [ + 'driver' => 'array' + ] + ] +]; +EOF; + file_put_contents(base_path() . '/config/cache.php', $cache_config); + + // 3、redis配置文件 + $redis_config = << [ + 'password' => getenv('REDIS_PASSWORD'), + 'host' => getenv('REDIS_HOST'), + 'port' => getenv('REDIS_PORT'), + 'database' => getenv('REDIS_DB'), + 'prefix' => getenv('REDIS_PREFIX'), + 'pool' => [ + 'max_connections' => 5, + 'min_connections' => 1, + 'wait_timeout' => 3, + 'idle_timeout' => 60, + 'heartbeat_interval' => 50, + ], + ] +]; +EOF; + file_put_contents(base_path() . '/config/redis.php', $redis_config); + + // 4、依赖注入配置 + $dependence_config = <<addDefinitions(config('dependence', [])); +$builder->useAutowiring(true); +$builder->useAttributes(true); +return $builder->build(); +EOF; + file_put_contents(base_path() . '/config/container.php', $dependence_config); + + self::tinywanConfig(); + + } + + /** + * @desc 文件上传配置文件 + * @author zhangf@suq.cn + * @date 2025.9.1 + * @return void + */ + private static function tinywanConfig() + { + // 4、依赖注入配置 + $tinywan_config = << true, + 'storage' => [ + 'default' => 'oss', // 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' => [ + 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'pdf', + 'png', 'jpg', 'jpeg', 'gif', 'bmp' + ], // 被允许的文件类型列表 + 'exclude' => [ + 'php', 'py', 'js', 'html', 'css', 'class' + ], // 不被允许的文件类型列表 + // 本地对象存储 + 'local' => [ + 'adapter' => \Tinywan\Storage\Adapter\LocalAdapter::class, + 'root' => public_path().'/uploads/', + 'dirname' => function () { + return date('Ymd'); + }, + 'domain' => 'http://127.0.0.1:8787', + 'uri' => '/uploads/', // 如果 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', + ], + ], +]; + +EOF; + file_put_contents(base_path() . '/config/plugin/tinywan/storage/app.php', $tinywan_config); + } + + /** + * 卸载 + * + * @param $version + * @return void + */ + public static function uninstall($version) + { + // 删除菜单 + foreach (static::getMenus() as $menu) { + Menu::delete($menu['key']); + } + // 卸载数据库 + static::uninstallSql(); + } + + /** + * 更新 + * + * @param $from_version + * @param $to_version + * @param $context + * @return void + */ + public static function update($from_version, $to_version, $context = null) + { + // 删除不用的菜单 + if (isset($context['previous_menus'])) { + static::removeUnnecessaryMenus($context['previous_menus']); + } + // 安装数据库 + static::installSql(); + // 导入新菜单 + if ($menus = static::getMenus()) { + Menu::import($menus); + } + // 执行更新操作 + $update_file = __DIR__ . '/../update.php'; + if (is_file($update_file)) { + include $update_file; + } + } + + /** + * 更新前数据收集等 + * + * @param $from_version + * @param $to_version + * @return array|array[] + */ + public static function beforeUpdate($from_version, $to_version) + { + // 在更新之前获得老菜单,通过context传递给 update + return ['previous_menus' => static::getMenus()]; + } + + /** + * 获取菜单 + * + * @return array|mixed + */ + public static function getMenus() + { + clearstatcache(); + if (is_file($menu_file = __DIR__ . '/../config/menu.php')) { + $menus = include $menu_file; + return $menus ?: []; + } + return []; + } + + /** + * 删除不需要的菜单 + * + * @param $previous_menus + * @return void + */ + public static function removeUnnecessaryMenus($previous_menus) + { + $menus_to_remove = array_diff(Menu::column($previous_menus, 'name'), Menu::column(static::getMenus(), 'name')); + foreach ($menus_to_remove as $name) { + Menu::delete($name); + } + } + + /** + * 安装SQL + * + * @return void + */ + protected static function installSql() + { + static::importSql(__DIR__ . '/../install.sql'); + } + + /** + * 卸载SQL + * + * @return void + */ + protected static function uninstallSql() { + // 如果卸载数据库文件存在责直接使用 + $uninstallSqlFile = __DIR__ . '/../uninstall.sql'; + if (is_file($uninstallSqlFile)) { + static::importSql($uninstallSqlFile); + return; + } + // 否则根据install.sql生成卸载数据库文件uninstall.sql + $installSqlFile = __DIR__ . '/../install.sql'; + if (!is_file($installSqlFile)) { + return; + } + $installSql = file_get_contents($installSqlFile); + preg_match_all('/CREATE TABLE `(.+?)`/si', $installSql, $matches); + $dropSql = ''; + foreach ($matches[1] as $table) { + $dropSql .= "DROP TABLE IF EXISTS `$table`;\n"; + } + file_put_contents($uninstallSqlFile, $dropSql); + static::importSql($uninstallSqlFile); + unlink($uninstallSqlFile); + } + + /** + * 导入数据库 + * + * @return void + */ + public static function importSql($mysqlDumpFile) + { + if (!$mysqlDumpFile || !is_file($mysqlDumpFile)) { + return; + } + foreach (explode(';', file_get_contents($mysqlDumpFile)) as $sql) { + if ($sql = trim($sql)) { + try { + Db::connection(static::$connection)->statement($sql); + } catch (Throwable $e) {} + } + } + } + +} \ No newline at end of file diff --git a/plugin/piadmin/app/base/BaseController.php b/plugin/piadmin/app/base/BaseController.php new file mode 100644 index 0000000..1736586 --- /dev/null +++ b/plugin/piadmin/app/base/BaseController.php @@ -0,0 +1,40 @@ +request = \request(); + + // 控制器初始化 + $this->initialize(); + } + + // 初始化 + protected function initialize() + {} + +} diff --git a/plugin/piadmin/app/base/BaseDao.php b/plugin/piadmin/app/base/BaseDao.php new file mode 100644 index 0000000..26cf34e --- /dev/null +++ b/plugin/piadmin/app/base/BaseDao.php @@ -0,0 +1,796 @@ +search($where, $search)->count(); + } + + /** + * 根据指定条件选择数据列表 + * + * 此方法用于根据多个条件查询数据,并支持分页、排序和关联加载等功能 + * 它是构建复杂查询的一个灵活入口点,允许开发者指定所需的查询条件和选项, + * 并获取一个数据集合作为结果返回 + * + * @param array $where 查询条件数组,用于指定查询的过滤条件 + * @param string $field 要查询的字段,默认为 '*',表示查询所有字段 + * @param int $page 分页页码,默认为 0,表示不分页 + * @param int $limit 每页数据条数,默认为 0,表示不限制条数 + * @param string $order 排序条件字符串,默认为空,表示不排序 + * @param array $with 关联加载数组,用于指定要加载的关联关系 + * @param bool $search 是否使用搜索模式,默认为 false + * + * @return Collection 查询结果集合 + * @throws ReflectionException + */ + public function selectList(array $where, string $field = '*', int $page = 0, int $limit = 0, + string $order = '', array $with = [], bool $search = false): Collection + { + return $this->selectModel($where, $field, $page, $limit, $order, $with, $search)->select(); + } + + /** + * 根据查询条件构造模型 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @param string $order + * @param array $with + * @param bool $search + * @return BaseModel + * @throws ReflectionException + */ + private function selectModel(array $where, string $field = '*', int $page = 0, int $limit = 0, string $order = '', array $with = [], bool $search = false): mixed + { + if ($search) { + $model = $this->search($where); + } else { + $model = $this->getModel()->where($where); + } + return $model->field($field)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when($order !== '', function ($query) use ($order) { + $query->order($order); + })->when($with, function ($query) use ($with) { + $query->with($with); + }); + } + + /** + * 获取满足条件的记录数 + * + * 该方法用于执行一个查询,以获取数据库中满足给定条件的记录数 + * 它通过传递一个数组参数来指定查询条件,然后使用模型的where方法来过滤数据, + * 最后使用count方法来统计满足条件的记录数 + * + * @param array $where 查询条件数组,用于指定过滤条件 + * @return int 满足条件的记录数 + */ + public function getCount(array $where): int + { + return $this->getModel()->where($where)->count(); + } + + /** + * 获取唯一值的计数 + * + * 该方法用于计算给定字段在特定条件下的唯一值数量根据$search参数的值, + * 选择使用search方法还是getModel方法来执行查询 + * + * @param array $where 查询条件,用于筛选记录 + * @param string $field 需要计算唯一值的字段名称 + * @param bool $search 指示是否使用search方法来执行查询,默认为true如果为false,则使用getModel方法 + * + * @return int 返回计算得到的唯一值数量如果没有匹配的记录,返回0 + */ + public function getDistinctCount(array $where, $field, bool $search = true): int + { + if ($search) { + // 使用search方法执行查询,并计算指定字段的唯一值数量 + return $this->search($where)->field('COUNT(distinct(' . $field . ')) as count')->select()->toArray()[0]['count'] ?? 0; + } else { + // 使用getModel方法执行查询,并计算指定字段的唯一值数量 + return $this->getModel()->where($where)->field('COUNT(distinct(' . $field . ')) as count')->select()->toArray()[0]['count'] ?? 0; + } + } + + /** + * 获取模型 + * @return BaseModel + */ + public function getModel(): BaseModel + { + return app()->make($this->setModel()); + } + + /** + * 获取主键 + * @return array|string + */ + protected function getPk(): array|string + { + return $this->getModel()->getPk(); + } + + /** + * 获取模型对应的表名 + * @return string + */ + public function getTableName(): string + { + return $this->getModel()->getName(); + } + + /** + * 根据主键获取一条或多条数据 + * @param $id + * @param array|null $field + * @param array|null $with + * @return array|Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function get($id, ?array $field = [], ?array $with = []): array|Model|null + { + if (is_array($id)) { + $where = $id; + } else { + $where = [$this->getPk() => $id]; + } + return $this->getModel()->where($where)->when(count($with), function ($query) use ($with) { + $query->with($with); + })->field($field ?? ['*'])->find(); + } + + /** + * 查询一条数据是否存在 + * @param $map + * @param string $field + * @return bool 是否存在 + */ + public function be($map, string $field = '') + { + if (!is_array($map) && empty($field)) $field = $this->getPk(); + $map = !is_array($map) ? [$field => $map] : $map; + return 0 < $this->getModel()->where($map)->count(); + } + + /** + * 根据条件获取一条数据 + * @param array $where + * @param string|null $field + * @param array $with + * @return array|Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOne(array $where, ?string $field = '*', array $with = []) + { + $field = explode(',', $field); + return $this->get($where, $field, $with); + } + + /** + * 获取单个字段值 + * @param $where + * @param string|null $field + * @param string $order + * @return mixed + */ + public function value($where, ?string $field = '', string $order = ''): mixed + { + $pk = $this->getPk(); + return $this->getModel()->where($where)->order($order)->value($field ?: $pk); + } + + /** + * 根据条件查询某一列数据 + * @param array $where + * @param string $field + * @param string $key + * @return array + */ + public function getColumn(array $where, string $field, string $key = ''): array + { + return $this->getModel()->where($where)->column($field, $key); + } + + + /** + * 删除记录 + * + * 该方法支持根据单个或多个条件删除记录当$id为数组时,表示根据多个条件删除记录; + * 当$id为单个值时,表示根据主键或指定的$key删除记录 + * + * @param array|int|string $id 要删除的记录的ID或条件数组 + * @param string|null $key 当$id为单个值时,指定的字段名,默认为主键 + * + * @return bool 成功删除返回true,否则返回false + */ + public function delete(array|int|string $id, ?string $key = null): bool + { + // 判断$id是否为数组,如果是,则直接使用$id作为删除条件 + if (is_array($id)) { + $where = $id; + } else { + // 如果$id不是数组,则根据是否设置了$key来决定使用主键还是$key作为删除条件 + $where = [is_null($key) ? $this->getPk() : $key => $id]; + } + // 使用确定的删除条件执行删除操作,并返回删除结果 + return $this->getModel()->where($where)->delete(); + } + + /** + * 强制删除记录 + * + * 该方法用于删除一个指定ID的资源。可以根据$force参数决定是软删除还是硬删除。 + * + * @param int $id 要删除的资源的ID + * @param bool $force 是否强制删除,true为硬删除,false为软删除(默认) + * @return mixed 删除操作的结果,具体类型取决于getModel()->destroy方法的返回值 + */ + public function destroy(int $id, bool $force = false) + { + return $this->getModel()->destroy($id, $force); + } + + /** + * 更新数据 + * + * 该方法用于更新数据库中与给定ID或条件相符的记录它接受一个ID(可以是数组、整数或字符串), + * 一个包含要更新的数据的数组,和一个可选的键名称如果ID是一个数组,它将直接用作查询条件 + * 否则,它将与主键或提供的键名称一起构建查询条件 + * + * @param array|int|string $id 要更新的记录的ID,可以是数组、整数或字符串如果是数组,则视为查询条件 + * @param array $data 要更新的数据,以数组形式提供 + * @param string|null $key 可选的键名称,用于指定查询时使用的键如果未提供,则使用主键 + * @return mixed 更新操作的结果,具体类型取决于底层数据库操作 + */ + public function update(array|int|string $id, array $data, ?string $key = null): mixed + { + // 根据$id的类型和$key的值构建查询条件 + if (is_array($id)) { + $where = $id; + } else { + $where = [is_null($key) ? $this->getPk() : $key => $id]; + } + // 调用模型的update方法执行数据库更新操作 + return $this->getModel()::update($data, $where); + } + + /** + * @param $where + * @return array|mixed + */ + protected function setWhere($where, ?string $key = null): mixed + { + if (!is_array($where)) { + $where = [is_null($key) ? $this->getPk() : $key => $where]; + } + return $where; + } + + /** + * 批量更新记录 + * + * 该方法用于根据一组ID批量更新数据库中的记录它允许指定更新的数据以及可选的键, + * 如果未提供键,则使用主键进行更新操作 + * + * @param array $ids 需要更新的记录的ID数组 + * @param array $data 要更新的数据,以键值对形式表示 + * @param string|null $key 可选参数,用于指定更新操作使用的键如果未提供,则默认使用主键 + * @return BaseModel 更新操作的结果, + */ + public function batchUpdate(array $ids, array $data, ?string $key = null): BaseModel + { + // 获取模型实例,并使用whereIn方法来匹配IDs,如果$key为null,则使用主键 + // 这里使用了三元运算符来判断$key是否为null,从而决定使用哪个键进行查询 + // 最后调用update方法来执行批量更新操作 + return $this->getModel()->whereIn(is_null($key) ? $this->getPk() : $key, $ids)->update($data); + } + + + /** + * 保存数据到模型 + * + * 该方法主要用于将给定的数据数组保存到与模型关联的数据库表中它首先获取模型的字段信息, + * 然后对数据进行预处理,包括设置一些特殊字段的默认值最后,使用处理后的数据创建一个新的模型实例 + * + * @param array $data 要保存到模型的数据数组 + */ + public function save(array $data): BaseModel + { + // 获取模型的字段信息 + $fields = $this->getModelField(); + + // 对数据进行预处理,设置特殊字段的默认值 + $data = $this->setSpecialFieldDefaultValue($data,$fields); + + // 使用处理后的数据创建并返回新的模型实例 + return $this->getModel()::create($data); + } + + + /** + * 批量保存数据 + * + * @param array $data 要保存的数据数组,每个元素代表一条记录 + * @param bool $replace 是否使用替换插入的方式,默认为true + * + * @return mixed 保存结果,具体取决于底层数据库操作的返回值 + * @throws \Exception + */ + public function saveAll(array $data, bool $replace = true): mixed + { + // 获取模型字段信息,为后续数据处理做准备 + $fields = $this->getModelField(); + + // 初始化用于保存处理后数据的数组 + $saveData = []; + + // 遍历传入的数据数组,对每条记录进行处理 + foreach ($data as $v) { + // 设置特殊字段的默认值,并将处理后的数据添加到保存数组中 + $saveData[] = $this->setSpecialFieldDefaultValue($v,$fields); + } + + // 调用模型的saveAll方法,批量保存处理后的数据 + return $this->getModel()->saveAll($saveData, $replace); + } + + + /** + * 批量插入数据 + * + * 该方法主要用于批量插入数据到数据库中它首先获取模型的字段信息,然后对输入的数据进行处理, + * 设置一些特殊字段的默认值,最后将处理后的数据批量插入数据库 + * + * @param array $data 要插入的数据数组,每个元素代表一行数据 + * @return int 插入操作影响的行数 + */ + public function insertAll(array $data): int + { + // 获取模型字段信息,用于后续处理数据时参考 + $fields = $this->getModelField(); + + // 初始化一个空数组,用于存储处理后的数据 + $saveData = []; + + // 遍历输入的数据数组 + foreach ($data as $v) { + // 对每行数据,设置特殊字段的默认值,并将处理后的数据添加到保存数组中 + $saveData[] = $this->setSpecialFieldDefaultValue($v,$fields); + } + + // 调用模型的批量插入方法,插入处理后的数据,并返回插入操作影响的行数 + return $this->getModel()->insertAll($saveData); + } + + private function setSpecialFieldDefaultValue($data, $fields) + { +// if(in_array('id',$fields)){ +// $data['id'] = $data['id'] ?? IdUtils::uuid(); +// } +// if(session(('user'))){ +// if(in_array('create_by',$fields)){ +// $data['create_by'] = $data['create_by'] ?? session('user')['uid']; +// } +// if(in_array('update_by',$fields)){ +// $data['update_by'] = $data['update_by'] ?? session('user')['uid']; +// } +// } + + $admininfo = RequestUtils::getAdminInfo(); + if($admininfo){ + if(in_array('create_by',$fields)){ + $data['create_by'] = $data['create_by'] ?? $admininfo['id']; + } + if(in_array('update_by',$fields)){ + $data['update_by'] = $data['update_by'] ?? $admininfo['id']; + } + } + + if(in_array('create_time',$fields)){ + $data['create_time'] = $data['create_time'] ?? time(); + } + if(in_array('update_time',$fields)){ + $data['update_time'] = $data['update_time'] ?? time(); + } + return $data; + } + + /** + * 获取搜索器和搜索条件key,以及不在搜索器的条件数组 + * @param array $where + * @return array[] + * @throws ReflectionException + */ + private function getSearchData(array $where) + { + $with = []; + $otherWhere = []; + $responses = new \ReflectionClass($this->setModel()); + foreach ($where as $key => $value) { + if($value === '' || $value === null){ + continue; + } + $method = 'search' . Str::studly($key) . 'Attr'; + if ($responses->hasMethod($method)) { + $with[] = $key; + } else { + if(!in_array($key,['user'])){ + if (!is_array($value)) { + $otherWhere[] = [$key, '=', $value]; + } else if (count($value) === 3) { + $otherWhere[] = $value; + } + } + } + } + return [$with, $otherWhere]; + } + + /** + * 根据搜索器获取搜索内容 + * @param $where + * @param $search + * @return BaseModel + * @throws ReflectionException + */ + protected function withSearchSelect($where, $search) + { + [$with, $otherWhere] = $this->getSearchData($where); + return $this->getModel()->withSearch($with, $where)->when($search, function ($query) use ($otherWhere) { + $query->where($this->filterWhere($otherWhere)); + }); + } + + /** + * 过滤数据表中不存在的where条件字段 + * @param array $where + * @return array + */ + protected function filterWhere(array $where = []): array + { + $fields = $this->getModel()->getTableFields(); + foreach ($where as $key => $item) { + // 带有表别名前缀的去掉前缀 + $cols = explode('.', $item[0]); + $col = $cols[count($cols) - 1]; + if (!in_array($col, $fields)) { + unset($where[$key]); + } + } + return $where; + } + + + /** + * 根据给定的条件搜索模型 + * + * 当提供了一个非空的条件数组时,该方法会执行一个带有搜索条件的查询; + * 如果条件数组为空,则直接返回模型实例 + * + * @param array $where 搜索条件数组,用于指定查询条件,默认为空数组 + * @param bool $search 是否执行搜索的标志,用于控制是否应用搜索条件,默认为true + * + * @return mixed 返回一个BaseModel实例,具体类型取决于getModel和withSearchSelect方法的返回值 + */ + public function search(array $where = [], bool $search = true): mixed + { + // 判断是否有搜索条件提供 + if ($where) { + // 如果有搜索条件,则调用withSearchSelect方法执行带有搜索条件的查询 + return $this->withSearchSelect($where, $search); + } else { + // 如果没有搜索条件,则直接返回模型实例 + return $this->getModel(); + } + } + + /** + * 求和 + * @param array $where + * @param string $field + * @param bool $search + * @return float + * @throws ReflectionException + */ + public function sum(array $where, string $field, bool $search = false): float + { + if ($search) { + return $this->search($where)->sum($field); + } else { + return $this->getModel()->where($where)->sum($field); + } + } + + /** + * 高精度加法 + * @param $key + * @param string $incField + * @param string $inc + * @param string|null $keyField + * @param int $acc + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function bcInc($key, string $incField, string $inc, string $keyField = null, int $acc = 2): bool + { + return $this->bc($key, $incField, $inc, $keyField, 1); + } + + /** + * 高精度 减法 + * @param $key + * @param string $decField + * @param string $dec + * @param string|null $keyField + * @param int $acc + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function bcDec($key, string $decField, string $dec, string $keyField = null, int $acc = 2): bool + { + return $this->bc($key, $decField, $dec, $keyField, 2); + } + + /** + * 高精度计算并保存 + * @param $key + * @param string $incField + * @param string $inc + * @param string|null $keyField + * @param int $type + * @param int $acc + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function bc($key, string $incField, string $inc, string $keyField = null, int $type = 1, int $acc = 2): bool + { + if ($keyField === null) { + $result = $this->get($key); + } else { + $result = $this->getOne([$keyField => $key]); + } + if (!$result) return false; + $new = 0; + if ($type === 1) { + $new = bcadd($result[$incField], $inc, $acc); + } else if ($type === 2) { + if ($result[$incField] < $inc) return false; + $new = bcsub($result[$incField], $inc, $acc); + } + $result->{$incField} = $new; + return false !== $result->save(); + } + + /** + * 获取条件数据中的某个值的最大值 + * @param array $where + * @param string $field + * @return mixed + */ + public function getMax(array $where = [], string $field = ''): mixed + { + return $this->getModel()->where($where)->max($field); + } + + /** + * 获取条件数据中的某个值的最小值 + * @param array $where + * @param string $field + * @return mixed + */ + public function getMin(array $where = [], string $field = ''): mixed + { + return $this->getModel()->where($where)->min($field); + } + + /** + * 软删除 + * @param string $id + * @return mixed + */ + public function softDel(string $id): mixed + { + return $this->getModel()->where('id', $id)->update(['delete_time' => time()]); + } + + /** + * 保存唯一验证 + * @param array $where + * @param string $msg + */ + public function saveUnique(array $where, string $msg = '', $field = '*'): void + { + $res = $this->getModel()->field($field)->where($where)->findOrEmpty()->toArray(); + if (!empty($res)) { + throw new ApiException($msg); + } + } + + /** + * 更新唯一验证 + * @param array $where + * @param int $id + * @param string $msg + */ + public function updateUnique(array $where, $id, string $msg = '', $field = '*'): void + { + $res = $this->getModel()->field($field)->where($where)->findOrEmpty()->toArray(); + + if (!empty($res) && $res['id'] != $id) { + throw new ApiException($msg); + } + } + + public function getList($where, $field='*', $page=0, $limit=0, $order='id desc',$with=[]): array + { + $query = $this->search($where)->with($with)->field($field)->order($order); + if($page !== 0 && $limit !== 0){ + $query = $query->page($page,$limit); + } + return $query->select()->toArray(); + } + + /** + * 批量插入或更新 + * @param array $data + * @param string $duplicateKey + * @return bool + * @throws ApiException + */ + public function batchInsertOrUpdate(array $data,$duplicateKey = 'id'): bool + { + if(empty($data)){ + return true; + } + // update on duplicate key + $fields = $this->getModel()->getTableFields(); + // 获取所有字段 + $userFields = array_keys($data[0]); + // 检查字段是否存在 + foreach ($userFields as $field) { + if (!in_array($field, $fields)) { + throw new ApiException($field . '字段不存在',400 ); + } + } + $fields = $userFields; + + + $table = $this->getModel()->getTable(); + + // 将数据转换为 SQL 语句中的值部分 + $values = []; + foreach ($data as $row) { + $valueStr = '(' . implode(',', array_map(function ($value) { + return "'" . addslashes($value) . "'"; + }, $row)) . ')'; + $values[] = $valueStr; + } + + // 构建更新部分的 SQL 语句 + $updateStr = ''; + foreach ($fields as $field) { + // 移除duplicateKey + if($field == $duplicateKey){ + continue; + } + $updateStr .= "`$field` = VALUES(`$field`),"; + } + + // 构建字段部分的 SQL 语句 + $fieldStr = implode(',', array_map(function ($field) { + return "`$field`"; + }, $fields)); + // 移除最后一个逗号 + $updateStr = rtrim($updateStr, ','); + + // 构建完整的 SQL 语句 + $sql = "INSERT INTO {$table} ({$fieldStr}) VALUES " . implode(',', $values) . " ON DUPLICATE KEY UPDATE {$updateStr}"; + // 使用 execute 方法执行 SQL 语句 + return Db::execute($sql); + } + + /** + * 根据条件更新数据 + * @param array $where + * @param array $data + */ + public function updateByWhere(array $where, array $data) + { + return $this->getModel()->where($where)->update($data); + } + + /** + * 获取模型字段列表 + * + * 该方法用于获取当前模型所在表的所有字段名称 + * 它通过查询数据库表的列信息来实现 + * + * @return array 返回字段名称数组 + */ + public function getModelField(): array + { + // 获取模型对应的表名 + $table = $this->getModel()->getTable(); + + // 执行SQL语句,获取表中所有列的详细信息 + $fields = Db::query("SHOW FULL COLUMNS FROM $table"); + + // 从查询结果中提取所有字段名,并返回 + return array_column($fields, 'Field'); + } + + public function __call(string $name, array $arguments) + { + return call_user_func_array([$this->getModel(), $name], $arguments); + } +} diff --git a/plugin/piadmin/app/base/BaseModel.php b/plugin/piadmin/app/base/BaseModel.php new file mode 100644 index 0000000..cbc5650 --- /dev/null +++ b/plugin/piadmin/app/base/BaseModel.php @@ -0,0 +1,27 @@ +dao, $name], $arguments); + } + + +} \ No newline at end of file diff --git a/plugin/piadmin/app/base/BaseValidate.php b/plugin/piadmin/app/base/BaseValidate.php new file mode 100644 index 0000000..f293e3c --- /dev/null +++ b/plugin/piadmin/app/base/BaseValidate.php @@ -0,0 +1,16 @@ + '^(?:(?:https?|ftp):\/\/)?(?:www\.)?[a-zA-Z0-9-]{1,63}(?:\.[a-zA-Z0-9-]{1,63})+(?::\d{1,5})?(?:\/[^\s?#]*)?(?:\?[^#\s]*)?(?:#[^\s]*)?$' + ]; +} \ No newline at end of file diff --git a/plugin/piadmin/app/common/ContainerProxy.php b/plugin/piadmin/app/common/ContainerProxy.php new file mode 100644 index 0000000..8137b3e --- /dev/null +++ b/plugin/piadmin/app/common/ContainerProxy.php @@ -0,0 +1,53 @@ + $abstract + * @param array $parameters + * @return T + */ + public function make($abstract, array $parameters = []) + { + return WebmanContainer::get($abstract, $parameters); + } + + /** + * 获取实例(兼容TP语法) + * @template T + * @param class-string $abstract + * @return T + */ + public function get($abstract) + { + return WebmanContainer::get($abstract); + } + + // 添加其他ThinkPHP兼容方法... + public function bind($abstract, $concrete = []) + { + WebmanContainer::make($abstract, $concrete); + return $this; + } + + public function has($abstract) + { + return WebmanContainer::has($abstract); + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/controller/v1/AttachmentController.php b/plugin/piadmin/app/controller/v1/AttachmentController.php new file mode 100644 index 0000000..4cde565 --- /dev/null +++ b/plugin/piadmin/app/controller/v1/AttachmentController.php @@ -0,0 +1,51 @@ +service = $service; + } + + /** + * @desc 文件上传 + * @author zhangf@suq.cn + * @date 2025.9.1 + * @return \support\Response + */ + public function upload() + { + $file = request()->file('file'); + return success($this->service->upload($file)); + } + + /** + * @desc 文件列表 + * @author zhangf@suq.cn + * @date 2025.9.1 + * @return \support\Response + */ + public function index() + { + $params = requestOnly([ + 'original_name' => '' + ]); + return success($this->service->listData($params)); + } + + public function delete() + { + $id = input('id'); + return success($this->service->deleteData($id)); + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/controller/v1/DataDictionaryController.php b/plugin/piadmin/app/controller/v1/DataDictionaryController.php new file mode 100644 index 0000000..0b2fd45 --- /dev/null +++ b/plugin/piadmin/app/controller/v1/DataDictionaryController.php @@ -0,0 +1,16 @@ +dictionary()); + } + +} diff --git a/plugin/piadmin/app/controller/v1/IndexController.php b/plugin/piadmin/app/controller/v1/IndexController.php new file mode 100644 index 0000000..52fc1ad --- /dev/null +++ b/plugin/piadmin/app/controller/v1/IndexController.php @@ -0,0 +1,37 @@ +$request->admin]); + } + + public function test(Request $request) + { + $c = Cache::store(); + var_dump($c); + } + +} diff --git a/plugin/piadmin/app/controller/v1/LoginController.php b/plugin/piadmin/app/controller/v1/LoginController.php new file mode 100644 index 0000000..1789a8f --- /dev/null +++ b/plugin/piadmin/app/controller/v1/LoginController.php @@ -0,0 +1,50 @@ +service = app()->make(SystemAdminService::class); + } + + public function login(Request $request){ + $params = $request->only([ + 'username', + 'password' + ]); + validate(LoginValidate::class)->scene('loginByPassword')->check($params); + // 用户登录 + $res = $this->service->login($params); + return success($res); + } + + + public function captcha(){ + + $captcha = Captcha::imageCaptcha(); + + return success($captcha); + } + + + + +} diff --git a/plugin/piadmin/app/controller/v1/MenuController.php b/plugin/piadmin/app/controller/v1/MenuController.php new file mode 100644 index 0000000..f8337e5 --- /dev/null +++ b/plugin/piadmin/app/controller/v1/MenuController.php @@ -0,0 +1,96 @@ +service = app()->make(MenuService::class); + } + + public function index(Request $request) + { + + $all = $request->all(); + + $list = $this->service->getMenuList($all); + + return success($list); + } + + public function save(Request $request) + { + $params = requestOnly([ + 'pid' => 0, + 'name' => '', + 'code' => '', + 'icon' => '', + 'route' => '', + 'component' => '', + 'redirect' => '', + 'is_hidden' => 2, + 'is_layout' => 2, + 'type' => '', + 'status' => 1, + 'sort' => 1000, + 'remark' => '' + ]); + validate(MenuValidate::class)->scene('save')->check($params); + $res = $this->service->saveData($params); + return success($res); + } + + public function update(Request $request) + { + $params = requestOnly([ + 'id' => '', + 'pid' => 0, + 'name' => '', + 'code' => '', + 'icon' => '', + 'route' => '', + 'component' => '', + 'redirect' => '', + 'is_hidden' => 2, + 'is_layout' => 2, + 'type' => '', + 'status' => 1, + 'sort' => 1000, + 'remark' => '' + ]); + validate(MenuValidate::class)->scene('update')->check($params); + $res = $this->service->updateData($params); + return success($res); + } + + public function delete(Request $request) + { + $ids = $request->input('ids'); + $res = $this->service->deleteData($ids); + return success($res); + } + + public function read(Request $request) + { + $id = $request->input('id'); + $res = $this->service->readData($id); + return success($res); + } + + public function getMenuPermissions(MenuService $service) + { + return success($service->getUserMenu()); + } + +} diff --git a/plugin/piadmin/app/controller/v1/SystemAdminController.php b/plugin/piadmin/app/controller/v1/SystemAdminController.php new file mode 100644 index 0000000..c33faa6 --- /dev/null +++ b/plugin/piadmin/app/controller/v1/SystemAdminController.php @@ -0,0 +1,83 @@ + '', + 'account' => '', + 'dept_id' => '', + 'password' => '', + 'phone' => '', + 'email' => '', + 'status' => 1, + 'gender' => 3, + 'role_ids' => [] + ]); + + validate(SystemAdminValidate::class)->check($params, 'save'); + + return success($service->saveData($params)); + } + + public function update(SystemAdminService $service): Response + { + $params = requestOnly([ + 'id' => '', + 'name' => '', + 'account' => '', + 'dept_id' => '', + 'password' => '', + 'phone' => '', + 'email' => '', + 'status' => 1, + 'gender' => 3, + 'role_ids' => [] + ]); + + validate(SystemAdminValidate::class)->check($params, 'update'); + + return success($service->updateData(ArrayUtils::filterNotEmpty($params))); + } + + public function index(SystemAdminService $service): Response + { + $params = requestOnly([ + 'name' => '', + 'email' => '', + 'phone' => '', + 'status' => '', + 'dept_id' => '' + ]); + return success($service->listData($params)); + } + + public function read(SystemAdminService $service): Response + { + $id = input('id'); + return success($service->readData($id)); + } + + public function delete(SystemAdminService $service): Response + { + $id = input('id'); + return success($service->deleteData($id)); + } + + public function ban(SystemAdminService $service) + { + $params = requestOnly([ + 'id' => '', + ]); + return success($service->banAdmin($params)); + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/controller/v1/SystemDeptController.php b/plugin/piadmin/app/controller/v1/SystemDeptController.php new file mode 100644 index 0000000..6f1d275 --- /dev/null +++ b/plugin/piadmin/app/controller/v1/SystemDeptController.php @@ -0,0 +1,76 @@ + 0, + 'name' => '', + 'sort' => 1000, + 'leader' => '', + 'phone' => '', + 'email' => '', + 'status' => 1 + ]); + + validate(SystemDeptValidate::class)->check($params, 'save'); + + return success($service->saveData($params)); + } + + public function update(SystemDeptService $service): Response + { + $params = requestOnly([ + 'id' => '', + 'pid' => 0, + 'name' => '', + 'sort' => 1000, + 'leader' => '', + 'phone' => '', + 'email' => '', + 'status' => 1 + ]); + + validate(SystemDeptValidate::class)->check($params, 'update'); + + return success($service->updateData(ArrayUtils::filterNotEmpty($params))); + } + + public function index(SystemDeptService $service): Response + { + $params = requestOnly([ + 'name' => '', + 'status' => '' + ]); + return success($service->listData($params)); + } + + public function read(SystemDeptService $service): Response + { + $id = input('id'); + return success($service->readData($id)); + } + + public function delete(SystemDeptService $service): Response + { + $ids = input('ids'); + return success($service->deleteData($ids)); + } + + public function pureIndex(SystemDeptService $service): Response + { + $params = requestOnly([ + 'name' => '' + ]); + return success($service->listPureData($params)); + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/controller/v1/SystemRoleController.php b/plugin/piadmin/app/controller/v1/SystemRoleController.php new file mode 100644 index 0000000..ce051e8 --- /dev/null +++ b/plugin/piadmin/app/controller/v1/SystemRoleController.php @@ -0,0 +1,99 @@ + '', + 'code' => '', + 'status' => 1, + 'sort' => 1000, + 'remark' => '', + 'menu_ids' => [] + ]); + validate(SystemRoleValidate::class)->check($params, 'save'); + return success($service->saveData($params)); + } + + /** + * 更新角色 + * @return Response + */ + public function update(SystemRoleService $service): Response + { + $params = requestOnly([ + 'id' => '', + 'name' => '', + 'code' => '', + 'status' => 1, + 'sort' => 1000, + 'remark' => '', + 'menu_ids' => [] + ]); + validate(SystemRoleValidate::class)->check($params, 'update'); + return success($service->updateData(ArrayUtils::filterNotEmpty($params))); + } + + /** + * 角色列表 + * @param SystemRoleService $service + * @return Response + */ + public function index(SystemRoleService $service): Response + { + $params = requestOnly([ + 'name' => '', + 'code' => '', + 'status' => '' + ]); + return success($service->listData($params)); + } + + /** + * 角色详情 + * @param SystemRoleService $service + * @return Response + */ + public function read(SystemRoleService $service): Response + { + $id = input('id'); + return success($service->readData($id)); + } + + /** + * 删除角色 + * @param SystemRoleService $service + * @return Response + */ + public function delete(SystemRoleService $service): Response + { + $id = input('id'); + return success($service->deleteData($id)); + } + + /** + * 简单角色列表 + * @param SystemRoleService $service + * @return Response + */ + public function pureIndex(SystemRoleService $service): Response + { + $params = requestOnly([ + 'name' => '' + ]); + return success($service->listPureData($params)); + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/dao/AttachmenatDao.php b/plugin/piadmin/app/dao/AttachmenatDao.php new file mode 100644 index 0000000..3543527 --- /dev/null +++ b/plugin/piadmin/app/dao/AttachmenatDao.php @@ -0,0 +1,16 @@ +setTable('information_schema.tables'); + $tables = $tableModel->field('table_name, table_comment') + ->where('table_schema', config('think-orm.connections.mysql.database')) + ->where('table_type', 'BASE TABLE') + ->order('table_name') + ->select(); + + $columnModel = new InformationSchema(); + $columnModel->setTable('information_schema.columns'); + $tableList = []; + foreach ($tables as $table) { + // 获取表字段信息 + $columns = $columnModel + ->field('column_name,column_type,is_nullable,column_default,column_comment,column_key') + ->where('table_schema', config('think-orm.connections.mysql.database')) + ->where('table_name', $table->table_name) + ->order('ordinal_position') + ->select(); + + $tableList[] = [ + 'name' => $table->table_name, + 'comment' => $table->table_comment ?: '无注释', + 'columns' => $columns + ]; + } + return $tableList; + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/dao/SystemAdminDao.php b/plugin/piadmin/app/dao/SystemAdminDao.php new file mode 100644 index 0000000..12aa0b2 --- /dev/null +++ b/plugin/piadmin/app/dao/SystemAdminDao.php @@ -0,0 +1,19 @@ + $this->getCode() ?: 500, 'msg' => $this->getMessage()]); + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/exception/FrameworkException.php b/plugin/piadmin/app/exception/FrameworkException.php new file mode 100644 index 0000000..020167f --- /dev/null +++ b/plugin/piadmin/app/exception/FrameworkException.php @@ -0,0 +1,17 @@ + $this->getCode() ?: 500, 'msg' => $this->getMessage()]); + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/functions.php b/plugin/piadmin/app/functions.php new file mode 100644 index 0000000..4ef58c7 --- /dev/null +++ b/plugin/piadmin/app/functions.php @@ -0,0 +1,162 @@ + $code, + 'msg' => $msg, + 'data' => $data, + ]; + + $result = array_merge($result, $other_result); + return json($result); + } +} + +if (!function_exists('error')) { + /** + * api返回错误 + * @param $data + * @param $msg + * @param $other_result + * @param $code + * @return \support\Response + */ + function error($data = '', $msg = 'fail', $other_result = [], $code = 422): \support\Response + { + if (empty($msg) && is_string($data)) { + $msg = $data; + } + + $result = [ + 'code' => $code, + 'msg' => $msg, + 'data' => $data, + ]; + + $result = array_merge($result, $other_result); + return json($result); + } +} + +if (!function_exists('isNotBlank')) { + /** + * 判非空 + * @param $var + * @return bool + */ + function isNotBlank($var): bool + { + return $var !== '' && $var !== null && $var !== false && $var !== []; + } +} + +if (!function_exists('isBlank')) { + /** + * 判空 + * @param $var + * @return bool + */ + function isBlank($var): bool + { + return !isNotBlank($var); + } +} + +if (!function_exists('getColumnForKeyArray')) { + function getColumnForKeyArray(array $data, string $key = 'id'): array + { + $item = []; + foreach ($data as $val) { + $keys = explode(',', $key); + $tmpKey = ''; + foreach ($keys as $kv) { + if (!array_key_exists($kv, $val)) { + break; + } + $tmpKey .= $val[$kv] ?? null; + } + $item[$tmpKey] = $val; + + } + return $item; + } +} + +if (!function_exists('requestOnly')) { + + /** + * 获取某些请求参数 仿tp request()->only()方法 + * @param array $inputParams + * @return array + */ + function requestOnly(array $inputParams): array + { + $paramsKeys = array_keys($inputParams); + $data = request()->only($paramsKeys); + $result = []; + foreach ($paramsKeys as $key) { + if (!array_key_exists($key, $data) || empty($data[$key])) { + $result[$key] = $inputParams[$key]; + } else { + $result[$key] = $data[$key]; + } + } + return $result; + } +} + diff --git a/plugin/piadmin/app/middleware/AdminAuthorizationMiddleware.php b/plugin/piadmin/app/middleware/AdminAuthorizationMiddleware.php new file mode 100644 index 0000000..8ff6fd2 --- /dev/null +++ b/plugin/piadmin/app/middleware/AdminAuthorizationMiddleware.php @@ -0,0 +1,81 @@ +header($authHeader, ''); + if (empty($token)) { + throw new ApiException(403); + } + $token = trim(str_replace('Bearer ', '', $token)); + // 解token payload + $key = config("plugin.piadmin.piadmin.jwt.admin.key"); + $keyId = config("plugin.piadmin.piadmin.jwt.admin.key_id"); + $payload = JWT::decode($token, new Key($key, 'HS256')); + if (empty($token)) { + throw new ApiException(403); + } + // 缓存 + $tokenCacheData = CacheUtils::get('admin.token.'. md5($token)); + + if (empty($tokenCacheData)) { + throw new ApiException(403); + } + $tokenCacheData = json_decode($tokenCacheData, true); + if (empty($tokenCacheData) || !isset($tokenCacheData['uid'])) { + throw new ApiException(403); + } + // 验证账号状态 + $uid = $tokenCacheData['uid']; + $adminDao = app()->make(SystemAdminDao::class); + $admin = $adminDao->getOne(['id' => $uid]); + if (empty($admin)) { + throw new ApiException(403); + } + if ($admin['status'] != 1 ) { + throw new ApiException(400006); + } + $adminInfo = $admin->toArray(); + $adminInfo['is_login'] = true; + $adminInfo['uid'] = $adminInfo['id']; + $adminInfo['token'] = $token; + // 检查版本 + $this->checkVersionKey($uid, $token); + $request->admin = $adminInfo; + // todo 补全session + Context::set('admin', $adminInfo); + // session('user', $userInfo); + return $handler($request); + } + + /** + * 版本检查 + * @param string|int $uid + * @param string $token + * @return void + * @throws ApiException + */ + private function checkVersionKey(string|int $uid, string $token): void + { + $userVersionKey = "admin.version.{$uid}"; + $version = CacheUtils::get($userVersionKey); + if ($version != getPiAdminVersion()) { + CacheUtils::delete(md5($token)); + throw new ApiException(403); + } + } +} diff --git a/plugin/piadmin/app/middleware/CrossDomainMiddleware.php b/plugin/piadmin/app/middleware/CrossDomainMiddleware.php new file mode 100644 index 0000000..a85728d --- /dev/null +++ b/plugin/piadmin/app/middleware/CrossDomainMiddleware.php @@ -0,0 +1,28 @@ +method() == 'OPTIONS' ? response('') : $handler($request); + + // 给响应添加跨域相关的http头 + $response->withHeaders([ + 'Access-Control-Allow-Credentials' => 'true', + 'Access-Control-Allow-Origin' => $request->header('origin', '*'), + 'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'), + 'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'), + ]); + + return $response; + } +} diff --git a/plugin/piadmin/app/middleware/LanguageMiddleware.php b/plugin/piadmin/app/middleware/LanguageMiddleware.php new file mode 100644 index 0000000..55cdc21 --- /dev/null +++ b/plugin/piadmin/app/middleware/LanguageMiddleware.php @@ -0,0 +1,26 @@ +header('PIADMIN_LANG','zh_CN'); + + $fallback_locale = config('plugin.piadmin.translation.fallback_locale'); + + if(in_array($lang,$fallback_locale)){ + locale($lang); + } + + return $handler($request); + } +} diff --git a/plugin/piadmin/app/middleware/ParameterConvertMiddleware.php b/plugin/piadmin/app/middleware/ParameterConvertMiddleware.php new file mode 100644 index 0000000..be14261 --- /dev/null +++ b/plugin/piadmin/app/middleware/ParameterConvertMiddleware.php @@ -0,0 +1,145 @@ +convertRequestKeysToSnakeCase($request); + + // 继续传递请求,获取响应 + $response = $handler($request); + + // 2. 响应返回前:转换返回数据为小驼峰格式 + $this->convertResponseKeysToCamelCase($response); + + return $response; + } + + /** + * 将请求中的所有参数键名转换为小写下划线格式 + * 支持 GET、POST 及 JSON 请求 + */ + protected function convertRequestKeysToSnakeCase(Request $request): void + { + // 转换 GET 参数 + $getParams = $request->get(); + if (!empty($getParams)) { + $convertedGet = $this->convertArrayKeysToSnakeCase($getParams); + $request->setGet($convertedGet); + } + + // 转换 POST 参数 (application/x-www-form-urlencoded 或 multipart/form-data) + $postParams = $request->post(); + if (!empty($postParams)) { + $convertedPost = $this->convertArrayKeysToSnakeCase($postParams); + $request->setGet($convertedPost); + } + + // 处理 JSON 请求体 (application/json) + $contentType = $request->header('content-type', ''); + if (strpos($contentType, 'application/json') !== false) { + $rawBody = $request->rawBody(); + if (!empty($rawBody)) { + $jsonData = json_decode($rawBody, true); + if (is_array($jsonData) && !empty($jsonData)) { + $convertedJson = $this->convertArrayKeysToSnakeCase($jsonData); + // 替换请求的原始body和post数据 + $request->setPost(json_encode($convertedJson)); + // 同时更新 post 数据,因为后续可能通过 post() 方法获取 +// $request->request->replace($convertedJson); + } + } + } + } + + /** + * 将响应内容中的键名转换为小驼峰格式 + * 支持 JSON 响应 + */ + protected function convertResponseKeysToCamelCase(Response $response): void + { + $contentType = $response->getHeader('Content-Type'); + if (is_array($contentType)) { + $contentType = implode('; ', $contentType); + } + + // 只处理 JSON 响应 + if (strpos($contentType, 'application/json') !== false) { + $originalContent = $response->rawBody(); + if (!empty($originalContent)) { + $data = json_decode($originalContent, true); + if (is_array($data) && !empty($data)) { + $convertedData = $this->convertArrayKeysToCamelCase($data); + $response->withBody(json_encode($convertedData)); + } + } + } + } + + /** + * 递归地将数组的键转换为小写下划线格式 (snake_case) + */ + protected function convertArrayKeysToSnakeCase(array $data): array + { + $result = []; + foreach ($data as $key => $value) { + // 转换键名 + $newKey = $this->camelCaseToSnakeCase($key); + + // 递归处理数组值 + if (is_array($value)) { + $value = $this->convertArrayKeysToSnakeCase($value); + } + + $result[$newKey] = $value; + } + return $result; + } + + /** + * 递归地将数组的键转换为小驼峰格式 (camelCase) + */ + protected function convertArrayKeysToCamelCase(array $data): array + { + $result = []; + foreach ($data as $key => $value) { + // 转换键名 + $newKey = $this->snakeCaseToCamelCase($key); + + // 递归处理数组值 + if (is_array($value)) { + $value = $this->convertArrayKeysToCamelCase($value); + } + + $result[$newKey] = $value; + } + return $result; + } + + /** + * 将驼峰命名字符串转换为下划线命名 + * 例如: helloWorld -> hello_world, UserID -> user_id + */ + protected function camelCaseToSnakeCase(string $input): string + { + return strtolower(preg_replace('/(? helloWorld, user_id -> userId + */ + protected function snakeCaseToCamelCase(string $input): string + { + return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input)))); + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/middleware/PermissionsMiddleware.php b/plugin/piadmin/app/middleware/PermissionsMiddleware.php new file mode 100644 index 0000000..9ab5676 --- /dev/null +++ b/plugin/piadmin/app/middleware/PermissionsMiddleware.php @@ -0,0 +1,40 @@ +route->param('perm')[0]; + //接口白名单 + $white_list = [ + 'login', + 'captcha', + 'getMenuPermissions', + ]; + if (in_array($requestRoute, $white_list)) { + return $handler($request); + } + $menu_ids = AdminTokenUtils::getMenus(); + $menuModel = new SystemMenu(); + $routes = $menuModel->whereIn('id', $menu_ids)->field('route')->select()->toArray(); + //权限路由 + $routes = array_column($routes, 'route'); + if (!in_array($requestRoute, $routes)) { + throw new ApiException('没有权限', 401); + } + return $handler($request); + } + +} diff --git a/plugin/piadmin/app/model/Attachment.php b/plugin/piadmin/app/model/Attachment.php new file mode 100644 index 0000000..f4d1a89 --- /dev/null +++ b/plugin/piadmin/app/model/Attachment.php @@ -0,0 +1,44 @@ +belongsTo(SystemAdmin::class, 'create_by', 'id'); + } + + /** + * @desc 上传方式格式化 + * @author zhangf@suq.cn + * @date 2025.9.1 + */ + public static function getStorage() + { + // local:本地 oss:阿里云 cos:腾讯云 qos:七牛云 + $storage = Storage::getConfig('default'); + $storages = [ + 'local' => 1, + 'qos' => 2, + 'cos' => 3, + 'oss' => 4, + ]; + return $storages[$storage]; + } + +} \ No newline at end of file diff --git a/plugin/piadmin/app/model/InformationSchema.php b/plugin/piadmin/app/model/InformationSchema.php new file mode 100644 index 0000000..edf47a7 --- /dev/null +++ b/plugin/piadmin/app/model/InformationSchema.php @@ -0,0 +1,20 @@ +table = $table; + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/model/SystemAdmin.php b/plugin/piadmin/app/model/SystemAdmin.php new file mode 100644 index 0000000..e6b106f --- /dev/null +++ b/plugin/piadmin/app/model/SystemAdmin.php @@ -0,0 +1,45 @@ +where('admin_id', $id)->column('role_id'); + $systemRole = new SystemRole(); + $data = $systemRole->whereIn('id', $role_ids)->column('name'); + return implode(',', $data); + } + + // 定义管理员与角色的多对多关联 + public function roles() + { + return $this->belongsToMany(SystemRole::class, SystemAdminRole::class,'role_guid','admin_guid'); + } + + // 部门信息 + public function dept() + { + return $this->hasOne(SystemDept::class,'id','dept_id')->bind(['dept_name' => 'name']); + } + +} \ No newline at end of file diff --git a/plugin/piadmin/app/model/SystemAdminDept.php b/plugin/piadmin/app/model/SystemAdminDept.php new file mode 100644 index 0000000..8146e5c --- /dev/null +++ b/plugin/piadmin/app/model/SystemAdminDept.php @@ -0,0 +1,11 @@ +belongsTo(SystemAdmin::class); + } + + // 定义中间表与角色的一对一关联 + public function role() + { + return $this->belongsTo(SystemRole::class); + } + +} \ No newline at end of file diff --git a/plugin/piadmin/app/model/SystemDept.php b/plugin/piadmin/app/model/SystemDept.php new file mode 100644 index 0000000..89ff78a --- /dev/null +++ b/plugin/piadmin/app/model/SystemDept.php @@ -0,0 +1,26 @@ +value('name') ?? ''; + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/model/SystemMenu.php b/plugin/piadmin/app/model/SystemMenu.php new file mode 100644 index 0000000..ec20827 --- /dev/null +++ b/plugin/piadmin/app/model/SystemMenu.php @@ -0,0 +1,16 @@ +belongsToMany(SystemAdmin::class, SystemAdminRole::class); + } + + // 角色与菜单的多对多关联 + public function menus() + { + return $this->belongsToMany(SystemMenu::class, SystemRoleMenu::class,'menu_guid','role_guid'); + } + +} \ No newline at end of file diff --git a/plugin/piadmin/app/model/SystemRoleMenu.php b/plugin/piadmin/app/model/SystemRoleMenu.php new file mode 100644 index 0000000..048c588 --- /dev/null +++ b/plugin/piadmin/app/model/SystemRoleMenu.php @@ -0,0 +1,25 @@ +belongsTo(SystemMenu::class); + } + + public function role() + { + return $this->belongsTo(SystemRole::class); + } + +} \ No newline at end of file diff --git a/plugin/piadmin/app/route/v1/attachment.php b/plugin/piadmin/app/route/v1/attachment.php new file mode 100644 index 0000000..579874e --- /dev/null +++ b/plugin/piadmin/app/route/v1/attachment.php @@ -0,0 +1,16 @@ +middleware([AdminAuthorizationMiddleware::class]); diff --git a/plugin/piadmin/app/route/v1/route.php b/plugin/piadmin/app/route/v1/route.php new file mode 100644 index 0000000..b8401a9 --- /dev/null +++ b/plugin/piadmin/app/route/v1/route.php @@ -0,0 +1,71 @@ +setParams(['perm' => ['login']]); + Route::get('/captcha', [LoginController::class, 'captcha'])->setParams(['perm' => ['captcha']]); +}); + +// 需要登录的接口 +Route::group('/piadmin/v1', function () { + // 获取当前用户菜单权限 + Route::get('/getMenuPermissions', [MenuController::class, 'getMenuPermissions'])->setParams(['perm' => ['getMenuPermissions']]); + + // 菜单 + Route::group('/menu', function () { + Route::get('/index', [MenuController::class, 'index'])->setParams(['perm' => ['menuIndex']]); + Route::post('/save', [MenuController::class, 'save'])->setParams(['perm' => ['menuSave']]); + Route::post('/update', [MenuController::class, 'update'])->setParams(['perm' => ['menuUpdate']]); + Route::get('/read', [MenuController::class, 'read'])->setParams(['perm' => ['menuRead']]); + Route::post('/delete', [MenuController::class, 'delete'])->setParams(['perm' => ['menuDelete']]); + }); + + // 角色 + Route::group('/role', function () { + Route::post('/save', [SystemRoleController::class, 'save'])->setParams(['perm' => ['roleSave']]); + Route::post('/update', [SystemRoleController::class, 'update'])->setParams(['perm' => ['roleUpdate']]); + Route::get('/index', [SystemRoleController::class, 'index'])->setParams(['perm' => ['roleIndex']]); + Route::get('/read', [SystemRoleController::class, 'read'])->setParams(['perm' => ['roleRead']]); + Route::post('/delete', [SystemRoleController::class, 'delete'])->setParams(['perm' => ['roleDelete']]); + Route::get('/pure/index', [SystemRoleController::class, 'pureIndex'])->setParams(['perm' => ['rolePureIndex']]); + }); + + // 部门 + Route::group('/dept', function () { + Route::post('/save', [SystemDeptController::class, 'save'])->setParams(['perm' => ['deptSave']]); + Route::post('/update', [SystemDeptController::class, 'update'])->setParams(['perm' => ['deptUpdate']]); + Route::get('/index', [SystemDeptController::class, 'index'])->setParams(['perm' => ['deptIndex']]); + Route::get('/read', [SystemDeptController::class, 'read'])->setParams(['perm' => ['deptRead']]); + Route::post('/delete', [SystemDeptController::class, 'delete'])->setParams(['perm' => ['deptDelete']]); + Route::get('/pure/index', [SystemDeptController::class, 'pureIndex'])->setParams(['perm' => ['deptPureIndex']]); + }); + + // 用户 + Route::group('/admin', function () { + Route::post('/save', [SystemAdminController::class, 'save'])->setParams(['perm' => ['adminSave']]); + Route::post('/update', [SystemAdminController::class, 'update'])->setParams(['perm' => ['adminUpdate']]); + Route::get('/index', [SystemAdminController::class, 'index'])->setParams(['perm' => ['adminIndex']]); + Route::get('/read', [SystemAdminController::class, 'read'])->setParams(['perm' => ['adminRead']]); + Route::post('/delete', [SystemAdminController::class, 'delete'])->setParams(['perm' => ['adminDelete']]); + //封禁/解封 + Route::post('/ban', [SystemAdminController::class, 'ban'])->setParams(['perm' => ['adminBan']]); + }); + //数据字典 + Route::get('/dataDictionary', [DataDictionaryController::class, 'dictionary'])->setParams(['perm' => ['dataDictionary']]); +})->middleware([ + AdminAuthorizationMiddleware::class, + PermissionsMiddleware::class +]); diff --git a/plugin/piadmin/app/service/AttachmentService.php b/plugin/piadmin/app/service/AttachmentService.php new file mode 100644 index 0000000..1b1ba7c --- /dev/null +++ b/plugin/piadmin/app/service/AttachmentService.php @@ -0,0 +1,98 @@ +ossStorage = new AliOssStorage(); + $this->dao = app()->make(AttachmenatDao::class); + } + + /** + * @desc 文件上传 + * @author zhangf@suq.cn + * @date 2025.9.1 + * @param $res + * @return array|\support\Response + */ + public function upload(array|UploadFile|null $file) + { + $fileExt = pathinfo($file->getUploadName(), PATHINFO_EXTENSION); + $uniqueName = uniqid(); + $result = $this->ossStorage->uploadObject($file->getPathname(), "$uniqueName.$fileExt", 'geo/images/user_avatar'); + $aliyunOssUrl = $result['info']['url'] ?? ''; + $path = parse_url($aliyunOssUrl, PHP_URL_PATH); + //封装数据 + $data = [ + 'id' => IdUtils::uuid(), + 'storage' => Attachment::getStorage(), + 'type' => $file->getUploadMimeType(), + 'name' => "$uniqueName.$fileExt", + 'path' => config('plugin.piadmin.oss.ali.domain') . '/' . $path, + 'suffix' => $fileExt, + 'original_name' => $file->getUploadName(), + 'size' => $file->getSize(), + ]; + $res = $this->dao->save($data); + return [ + 'id' => $res['id'], + 'url' => config('plugin.piadmin.oss.ali.domain') . '/' . $path + ]; + } + + /** + * @desc 获取文件列表 + * @author zhangf@suq.cn + * @date 2025.9.1 + * @param array $params + * @return array + */ + public function listData(array $params): array + { + [$page, $limit] = RequestUtils::getPageParameter(); + [$sortRule, $sortField] = RequestUtils::getSortParameter(); + + $query = [ + 'delete_time' => 0 + ]; + // 文件原名筛选 + if (isNotBlank($params['original_name'])) { + $query[] = ['original_name', 'like', '%' . $params['original_name'] . '%']; + } + $list = $this->dao->getList($query, '*', $page, $limit, "$sortField $sortRule", ['creator']); + $count = $this->dao->getCount($query); + return compact('list', 'count'); + } + + /** + * @desc 删除文件 + * @author zhangf@suq.com + * @date 2025.9.1 + * @param $id + * @return void + */ + public function deleteData($id) + { + Db::startTrans(); + try { + $this->dao->softDel($id); + Db::commit(); + } catch (\Exception $exception) { + Db::rollback(); + throw new ApiException($exception->getMessage()); + } + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/service/DataDictionaryService.php b/plugin/piadmin/app/service/DataDictionaryService.php new file mode 100644 index 0000000..8a1d447 --- /dev/null +++ b/plugin/piadmin/app/service/DataDictionaryService.php @@ -0,0 +1,24 @@ +dao = app()->make(InformationSchemaDao::class); + } + + public function dictionary() + { + return $this->dao->getInfomation(); + } + +} diff --git a/plugin/piadmin/app/service/MenuService.php b/plugin/piadmin/app/service/MenuService.php new file mode 100644 index 0000000..ed140da --- /dev/null +++ b/plugin/piadmin/app/service/MenuService.php @@ -0,0 +1,224 @@ +dao = $dao; + } + + + public function getMenuList($params = []) + { + $where = []; + $list = $this->dao->getList($where); + return TreeUtils::toTree($list); + } + + /** + * 保存数据 + * @param $params + * @return \plugin\piadmin\app\base\BaseModel + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function saveData($params) + { + // 检查code + $codeExist = $this->dao->be([ + 'code'=>$params['code'] + ]); + if ($codeExist) { + throw new ApiException(trans(401003)); + } + $parentMenu = $this->getMenu($params['pid']); + if (empty($parentMenu)) { + throw new ApiException(trans(401001)); + } + $newUuid = IdUtils::uuid(); + $newIdPath = IdPathUtils::addItem($parentMenu['id_path'], $newUuid); + $params['id_path'] = $newIdPath; + $params['uuid'] = $newUuid; + return $this->dao->save($params); + } + + /** + * 更新数据 + * @param $params + * @return \plugin\piadmin\app\base\BaseModel + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function updateData($params) + { + $menu = $this->dao->get($params['id']); + if (empty($menu)) { + throw new ApiException(trans(401005)); + } + if($params['pid'] == $menu['id']){ + throw new ApiException(trans(401008)); + } + // code有没有变化 + if ($params['code'] != $menu['code']) { + $codeExist = $this->dao->be([ + 'code'=>$params['code'] + ]); + if ($codeExist) { + throw new ApiException(trans(401003)); + } + } + $newIdPath = ''; + // 上级菜单有没有变化 + if ($params['pid'] != $menu['pid']) { + $parentMenu = $this->getMenu($params['pid']); + if (empty($parentMenu)) { + throw new ApiException(trans(401001)); + } + $newIdPath = IdPathUtils::addItem($parentMenu['id_path'], $menu['id']); + $params['id_path'] = $newIdPath; + } + unset($params['id']); + + // 落库 + Db::startTrans(); + try { + $this->dao->update(['id' => $menu['id']], $params); + if (!empty($newIdPath)) { + $this->updateAllChildrenIdPath($menu['id_path'], $newIdPath); + } + Db::commit(); + } catch (\Exception $exception) { + Db::rollback(); + Log::error($exception->getTraceAsString()); + throw new ApiException($exception->getMessage()); + } + return $params; + } + + + /** + * 删除数据 + * @param mixed $id + * @return array + */ + public function deleteData($ids) + { + //根据ids获取所有子菜单 + $ids = $this->getChildrenMenuIdsBatch($ids); + // 落库 + Db::startTrans(); + try { + foreach ($ids as $id) { + $this->dao->softDel($id); + } + Db::commit(); + } catch (\Exception $exception) { + Db::rollback(); + Log::error($exception->getTraceAsString()); + throw new ApiException($exception->getMessage()); + } + return []; + } + + /** + * 获取数据 + * @param mixed $id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function readData(mixed $id) + { + $menu = $this->dao->get(['id' => $id]); + if (empty($menu)) { + return []; + } + return $menu->toArray(); + } + + public function getUserMenu() + { + $menu_ids = AdminTokenUtils::getMenus(); + $list = $this->dao->getList([['id', 'in', $menu_ids]], '*'); + return TreeUtils::toTree($list); + } + + // ============================================================ 私有方法 =============================================== + + + /** + * 获取菜单 + * @param $id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + private function getMenu($id): array + { + if ($id == 0) { + return [ + 'id' => 0, + 'uuid' => 0, + 'name' => '顶级菜单', + 'code' => 'top', + 'type' => 'M', + 'status' => 1, + 'sort' => 1000, + 'pid' => 0, + 'id_path' => '0', + ]; + } + $menu = $this->dao->get(['id' => $id]); + if (empty($menu)) { + return []; + } + return $menu->toArray(); + } + + private function updateAllChildrenIdPath($oldIdPath, $newIdPath) + { + // 获取所有子级 + $childrenIds = $this->dao->getColumn([ + ['id_path', 'like', $oldIdPath . ',%'] + ], 'id'); + $children = $this->dao->getList([ + ['id', 'in', $childrenIds] + ], 'id, id_path'); + foreach ($children as $child) { + $newIdPath = str_replace($oldIdPath, $newIdPath, $child['id_path']); + $this->dao->update(['id' => $child['id']], [ + 'id_path' => $newIdPath + ]); + } + } + + public function getChildrenMenuIdsBatch(array $ids): array + { + $allMenus = $this->dao->getList([], 'id, pid'); + $allChildrenIds = []; + + // 批量处理所有ID的子节点查找 + foreach ($ids as $id) { + $childIds = TreeUtils::getChildrenIds($allMenus, $id); + $allChildrenIds = array_merge($allChildrenIds, $childIds); + } + return array_values(array_unique($allChildrenIds)); // 重新索引并去重 + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/service/SystemAdminDeptService.php b/plugin/piadmin/app/service/SystemAdminDeptService.php new file mode 100644 index 0000000..a6764f3 --- /dev/null +++ b/plugin/piadmin/app/service/SystemAdminDeptService.php @@ -0,0 +1,14 @@ +dao = app()->make(SystemAdminDeptDao::class); + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/service/SystemAdminRoleService.php b/plugin/piadmin/app/service/SystemAdminRoleService.php new file mode 100644 index 0000000..37c8cb2 --- /dev/null +++ b/plugin/piadmin/app/service/SystemAdminRoleService.php @@ -0,0 +1,14 @@ +dao = app()->make(SystemAdminRoleDao::class); + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/service/SystemAdminService.php b/plugin/piadmin/app/service/SystemAdminService.php new file mode 100644 index 0000000..d6d43c2 --- /dev/null +++ b/plugin/piadmin/app/service/SystemAdminService.php @@ -0,0 +1,387 @@ +dao = $dao; + $this->deptDao = app()->make(SystemDeptDao::class); + $this->roleDao = app()->make(SystemRoleDao::class); + $this->adminRoleDao = app()->make(SystemAdminRoleDao::class); + $this->adminDeptDao = app()->make(SystemAdminDeptDao::class); + } + + + // 用户登录 + public function login($params) + { + $adminUser = $this->dao->getModel()->where(function ($query) use ($params){ + $query->where('email', '=', $params['username']) + ->whereOr('phone', '=', $params['username']); + })->find(); + $this->validPassword($adminUser, $params); + $tokenInfo = $this->commonLogin($adminUser, $params); + $tokenInfo['user'] = $adminUser->toArray(); + $tokenInfo['expire_time'] = $tokenInfo['expire'] + time(); + //封装前端权限 + $menu_ids = AdminTokenUtils::getMenus($adminUser['id']); + $menu = new SystemMenu(); + $tokenInfo['permissions'] = $menu->whereIn('id', $menu_ids)->column('code'); + return $tokenInfo; + } + + /** + * 保存用户信息 + * @param array $params + * @return array + */ + public function saveData(array $params): array + { + // 检查账号 + $accountExist = $this->dao->be(['account' => $params['account']]); + var_dump($params['account']); + if ($accountExist) { + throw new ApiException(4040013); + } + // 检查部门 + $deptExist = $this->deptDao->be(['id' => $params['dept_id']]); + if (!$deptExist) { + throw new ApiException(trans(4030009)); + } + // 检查角色 + $roleIds = $this->roleDao->getColumn([ + ['id', 'in', $params['role_ids']] + ], 'id'); + if (empty($roleIds)) { + throw new ApiException(trans(4040014)); + } + $params['password'] = password_hash($params['password'], PASSWORD_ARGON2I); + // 落库 + Db::startTrans(); + try { + // 落库 + $admin = $this->dao->save($params); + // 部门关联 + $this->adminDeptDao->save([ + 'admin_id' => $admin['id'], + 'dept_id' => $params['dept_id'] + ]); + // 角色关联 + $adminRoles = []; + foreach ($roleIds as $roleId) { + $adminRoles[] = [ + 'admin_id' => $admin['id'], + 'role_id' => $roleId + ]; + } + $this->adminRoleDao->saveAll($adminRoles); + Db::commit(); + } catch (\Exception $exception) { + Db::rollback(); + Log::error($exception->getTraceAsString()); + throw new ApiException($exception->getMessage()); + } + return $admin->toArray(); + + } + + /** + * 修改用户信息 + * @param array $params + * @return array + */ + public function updateData(array $params): array + { + $admin = $this->dao->get(['id' => $params['id']]); + if (empty($admin)) { + throw new ApiException(trans(4040016)); + } + // 超级管理员禁止修改角色 + if ($admin['is_super_admin'] == 1) { + unset($params['role_ids']); + } + // 检查账号 + if (!empty($admin['account']) && $admin['account'] != $params['account']) { + $accountExist = $this->dao->be(['account' => $params['account']]); + if ($accountExist) { + throw new ApiException(4040013); + } + } + // 检查部门 + $updateDept = false; + if (!empty($admin['dept_id']) && $admin['dept_id'] != $params['dept_id']) { + $deptExist = $this->deptDao->be(['id' => $params['dept_id']]); + if (!$deptExist) { + throw new ApiException(trans(4030009)); + } + $updateDept = true; + } + // 检查密码 + if (!empty($params['password']) && !password_verify($params['password'], $admin['password'])) { + $params['password'] = password_hash($params['password'], PASSWORD_ARGON2I); + } + + // 落库 + Db::startTrans(); + try { + $this->dao->update(['id' => $params['id']], $params); + if ($updateDept) { + // 部门关联 + $this->adminDeptDao->update(['admin_id' => $params['id']], [ + 'dept_id' => $params['dept_id'] + ]); + } + if (!empty($params['role_ids'])) { + $this->adminRoleDao->delete(['admin_id' => $params['id']]); + $adminRoles = []; + foreach ($params['role_ids'] as $roleId) { + $adminRoles[] = [ + 'admin_id' => $params['id'], + 'role_id' => $roleId + ]; + } + $this->adminRoleDao->saveAll($adminRoles); + } + Db::commit(); + } catch (\Exception $exception) { + Db::rollback(); + Log::error($exception->getTraceAsString()); + throw new ApiException($exception->getMessage()); + } + return $params; + } + + /** + * 获取用户列表 + * @param array $params + * @return array + */ + public function listData(array $params): array + { + [$page, $limit] = RequestUtils::getPageParameter(); + [$sortRule, $sortField] = RequestUtils::getSortParameter(); + + $query = []; + // 账号筛选 + if (isNotBlank($params['email'])) { + $query[] = ['email', 'like', '%' . $params['email'] . '%']; + } + // 名称筛选 + if (isNotBlank($params['name'])) { + $query[] = ['name', 'like', '%' . $params['name'] . '%']; + } + // 手机号筛选 + if (isNotBlank($params['phone'])) { + $query[] = ['phone', 'like', '%' . $params['phone'] . '%']; + } + // 部门筛选 + if (isNotBlank($params['dept_id'])) { + $systemDeptModel = new SystemDept(); + $allDepts = $systemDeptModel->select()->toArray(); + $ids = TreeUtils::getChildrenIds($allDepts, $params['dept_id']); + $query[] = ['dept_id', 'in', $ids]; + } + // 状态筛选 + if (isNotBlank($params['status'])) { + $query[] = ['status', '=', $params['status']]; + } + + $list = $this->dao->getList($query, '*', $page, $limit, "$sortField $sortRule", ['dept']); + $count = $this->dao->getCount($query); + return compact('list', 'count'); + } + + /** + * 获取用户信息 + * @param mixed $id + * @return array + */ + public function readData(mixed $id): array + { + $admin = $this->dao->get(['id' => $id]); + if (empty($admin)) { + throw new ApiException(trans(4040016)); + } + $admin['role_ids'] = $this->adminRoleDao->getColumn([ + ['admin_id', '=', $id] + ], 'role_id'); + return $admin->toArray(); + } + + /** + * 删除用户信息 + * @param mixed $id + * @return array + */ + public function deleteData(mixed $id): array + { + $admin = $this->dao->get(['id' => $id]); + if (empty($admin)) { + return []; + } + if ($admin['is_super_admin'] == 1) { + throw new ApiException(4040017); + } + // 落库 + Db::startTrans(); + try { + $this->dao->softDel($id); + $this->adminDeptDao->delete(['admin_id' => $id]); + $this->adminRoleDao->delete(['admin_id' => $id]); + Db::commit(); + } catch (\Exception $exception) { + Db::rollback(); + Log::error($exception->getTraceAsString()); + throw new ApiException($exception->getMessage()); + } + // todo 踢下线 + return ['id' => $id]; + } + + public function banAdmin($params) + { + $admin = $this->dao->get(['id' => $params['id']]); + if (empty($admin)) { + throw new ApiException(trans(4040016)); + } + // 超级管理员禁止修改角色 + if ($admin['is_super_admin'] == 1) { + throw new ApiException(trans(4040018)); + } + if ($admin['status'] == 1) { + //todo 销毁token + } + $status = $admin['status'] == 1 ? 2 : 1; + $this->dao->update(['id' => $params['id']], ['status' => $status]); + return [ + 'id' => $params['id'], + 'status' => $status + ]; + } + + + // ============================================================ 私有方法 =============================================== + + + /** + * 验证密码 + * @param mixed $adminUser + * @param $params + * @return void + */ + private function validPassword(mixed $adminUser, $params): void + { + // 验证登录信息 + if (empty($adminUser)) { + throw new ApiException(trans(400005)); + } + // 密码验证 + if (!password_verify($params['password'], $adminUser['password'])) { + throw new ApiException(trans(400005)); + } + } + + /** + * 通用登录流程 + * @param mixed $adminUser + * @param array $params + * @return array + * @throws ApiException + */ + private function commonLogin(mixed $adminUser, array $params) + { + if ($adminUser['status'] == 2) { + throw new ApiException(trans(400006)); + } + $tokenInfo = JwtUtils::createToken($adminUser['id'], 'admin', []); + $tokenCacheInfo = [ + 'uid' => $adminUser['id'], + 'type' => 'admin_token', + 'token' => $tokenInfo['token'], + 'expire_time' => time() + $tokenInfo['expire'], + 'admin' => $adminUser->toArray(), + 'login_time' => date('Y-m-d H:i:s'), + 'os_type' => $this->getClientOS(), + 'browser_name' => $this->getClientBrowserName(), + 'login_ip' => request()->getRemoteIp() + ]; + // 保存token缓存 + CacheUtils::set('admin.token.' . md5($tokenInfo['token']), json_encode($tokenCacheInfo), (int)$tokenInfo['expire']); + // 保存用户对应版本缓存 + $currentVersion = getPiAdminVersion(); + $userVersionKey = "admin.version.{$adminUser['id']}"; + CacheUtils::set($userVersionKey, $currentVersion, (int)$tokenInfo['expire']); + return $tokenInfo; + } + + /** + * 获取客户端操作系统 + * @return int + */ + private function getClientOS(): int + { + $userAgent = request()->header('user-agent'); + if (stripos($userAgent, 'Windows') !== false) { + $os = 1; + } elseif (stripos($userAgent, 'Mac') !== false) { + $os = 2; + } elseif (stripos($userAgent, 'Linux') !== false) { + $os = 3; + } elseif (stripos($userAgent, 'iPhone|iPad|iPod') !== false) { + $os = 4; + } elseif (stripos($userAgent, 'Android') !== false) { + $os = 5; + } else { + $os = 6; + } + return $os; + } + + /** + * 获取客户端浏览器名称 + * @return string + */ + private function getClientBrowserName(): string + { + $userAgent = request()->header('user-agent'); + $pattern = '/(MSIE|Trident|Edge|Firefox|Chrome|Safari|Opera|OPR)\/?\s*(\d+\.\d+)?/i'; + preg_match($pattern, $userAgent, $matches); + if (isset($matches[1])) { + // 处理Edge和Opera的特殊标识 + if ($matches[1] === 'Trident') return 'Internet Explorer'; + if ($matches[1] === 'OPR') return 'Opera'; + return $matches[1]; + } + return 'Unknown Browser'; + } + + + + +} \ No newline at end of file diff --git a/plugin/piadmin/app/service/SystemDeptService.php b/plugin/piadmin/app/service/SystemDeptService.php new file mode 100644 index 0000000..263ecdd --- /dev/null +++ b/plugin/piadmin/app/service/SystemDeptService.php @@ -0,0 +1,265 @@ +dao = app()->make(SystemDeptDao::class); + $this->adminDao = app()->make(SystemAdminDao::class); + } + + /** + * 保存数据 + * @param array $params + * @return \plugin\piadmin\app\base\BaseModel + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function saveData(array $params) + { + var_dump($params); + $params['uuid'] = IdUtils::uuid(); + $parentDept = $this->getDept($params['pid']); + if (empty($parentDept)) { + throw new ApiException(trans(4030007)); + } + $params['id_path'] = IdPathUtils::addItem($parentDept['id_path'], $params['uuid']); + // todo 检查负责人信息 + return $this->dao->save($params); + } + + /** + * 更新数据 + * @param array $params + * @return void + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function updateData(array $params) + { + $menu = $this->dao->get(['id' => $params['id']]); + if (empty($menu)) { + throw new ApiException(trans(4030009)); + } + $newIdPath = ''; + // 检查pid + if (isNotBlank($params['pid']) && $params['pid'] != $menu['pid']) { + $parentDept = $this->getDept($params['pid']); + if (empty($parentDept)) { + throw new ApiException(trans(4030007)); + } + $newIdPath = IdPathUtils::addItem($parentDept['id_path'], $params['uuid']); + } + // todo 负责人更新检查 + $params['id_path'] = $newIdPath; + // 落库 + Db::startTrans(); + try { + $this->dao->update(['id' => $menu['id']], $params); + if (!empty($newIdPath)) { + $this->updateAllChildrenIdPath($menu['id_path'], $newIdPath); + } + Db::commit(); + } catch (\Exception $exception) { + Db::rollback(); + Log::error($exception->getTraceAsString()); + throw new ApiException($exception->getMessage()); + } + + } + + /** + * 列表数据 + * @param array $params + * @return array + */ + public function listData(array $params): array + { + $query = []; + $isFilter = false; + if (isNotBlank($params['name'])) { + $query[] = ['name', 'like', '%' . $params['name'] . '%']; + $isFilter = true; + } + if (isNotBlank($params['status'])) { + $query[] = ['status', '=', $params['status']]; + $isFilter = true; + } + $list = $this->dao->getList($query); + if ($isFilter) { + $idPaths = array_column($list, 'id_path'); + $uuids = []; + foreach ($idPaths as $idPath) { + $uuids = array_merge($uuids, explode(',', $idPath)); + } + $uuids = array_unique($uuids); + $parentIds = $this->dao->getColumn([ + ['uuid', 'in', $uuids] + ], 'id'); + $parents = $this->dao->getList([ + ['id', 'in', $parentIds] + ]); + $list = array_merge($list, $parents); + } + return TreeUtils::toTree($list); + } + + /** + * 详情数据 + * @param mixed $id + * @return array + */ + public function readData(mixed $id): array + { + $dept = $this->dao->get(['id' => $id]); + if (empty($dept)) { + return []; + } + return $dept->toArray(); + } + + public function deleteData($ids) + { + //根据ids获取所有子部门 + $ids = $deptIds = $this->getChildrenDeptIdsBatch($ids); + //验证部门下是否有用户 + if ($this->adminDao->be([ + ['dept_id', 'in', $deptIds] + ])) { + throw new ApiException('部门下包含用户,删除失败'); + } + // todo 删除关联表数据 + // 落库 + Db::startTrans(); + try { + foreach ($ids as $id) { + $this->dao->softDel($id); + } + Db::commit(); + } catch (\Exception $exception) { + Db::rollback(); + Log::error($exception->getTraceAsString()); + throw new ApiException($exception->getMessage()); + } + return ['id' => $id]; + } + + /** + * 简单数据列表 + * @param array $params + * @return array + */ + public function listPureData(array $params): array + { + $query = [ + 'status' => 1 + ]; + $isFilter = false; + if (isNotBlank($params['name'])) { + $query[] = ['name', 'like', '%' . $params['name'] . '%']; + $isFilter = true; + } + $list = $this->dao->getList($query, 'id, uuid, pid, name, id_path'); + if ($isFilter) { + $idPaths = array_column($list, 'id_path'); + $uuids = []; + foreach ($idPaths as $idPath) { + $uuids = array_merge($uuids, explode(',', $idPath)); + } + $uuids = array_unique($uuids); + $parentIds = $this->dao->getColumn([ + ['uuid', 'in', $uuids] + ], 'id'); + $parents = $this->dao->getList([ + ['id', 'in', $parentIds] + ], 'id, uuid, pid, name, id_path'); + $list = array_merge($list, $parents); + } + return TreeUtils::toTree($list); + + } + + + // ============================================================ 私有方法 =============================================== + + + /** + * 获取部门 + * @param $id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + private function getDept($id): array + { + if ($id == 0) { + return [ + 'id' => 0, + 'uuid' => 0, + 'name' => '顶级部门', + 'status' => 1, + 'sort' => 1000, + 'pid' => 0, + 'id_path' => '0', + ]; + } + $dept = $this->dao->get(['id' => $id]); + if (empty($dept)) { + return []; + } + return $dept->toArray(); + } + + /** + * 更新所有子级的id_path + * @param $oldIdPath + * @param $newIdPath + * @return void + */ + private function updateAllChildrenIdPath($oldIdPath, $newIdPath) + { + // 获取所有子级 + $childrenIds = $this->dao->getColumn([ + ['id_path', 'like', $oldIdPath . ',%'] + ], 'id'); + $children = $this->dao->getList([ + ['id', 'in', $childrenIds] + ], 'id, id_path'); + foreach ($children as $child) { + $newIdPath = str_replace($oldIdPath, $newIdPath, $child['id_path']); + $this->dao->update(['id' => $child['id']], [ + 'id_path' => $newIdPath + ]); + } + + } + + public function getChildrenDeptIdsBatch(array $ids): array + { + $allDepts = $this->dao->getList([], 'id, pid, leader'); + $allChildrenIds = []; + + // 批量处理所有ID的子节点查找 + foreach ($ids as $id) { + $childIds = TreeUtils::getChildrenIds($allDepts, $id); + $allChildrenIds = array_merge($allChildrenIds, $childIds); + } + return array_values(array_unique($allChildrenIds)); // 重新索引并去重 + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/service/SystemRoleService.php b/plugin/piadmin/app/service/SystemRoleService.php new file mode 100644 index 0000000..d6b6c3b --- /dev/null +++ b/plugin/piadmin/app/service/SystemRoleService.php @@ -0,0 +1,245 @@ +dao = app()->make(SystemRoleDao::class); + $this->menuDao = app()->make(SystemMenuDao::class); + $this->roleMenuDao = app()->make(SystemRoleMenuDao::class); + + } + + /** + * 保存数据 + * @param array $params + * @return array + */ + public function saveData(array $params): array + { + // 检查code + $existCode = $this->dao->be(['code' => $params['code']]); + if ($existCode) { + throw new ApiException(4020009); + } + // 检查名称 + $existName = $this->dao->be(['name' => $params['name']]); + if ($existName) { + throw new ApiException(4020010); + } + // 要保存的菜单id + $menuIds = $this->getMenuIdsByRoleMenuId($params['menu_ids']) ?? []; +// if (empty($menuIds)) { +// throw new ApiException(4020012); +// } + + // 落库 + Db::startTrans(); + try { + $role = $this->dao->save($params); + // 关联数据 + $roleMenuData = []; + foreach ($menuIds as $menuId) { + $roleMenuData[] = [ + 'role_id' => $role['id'], + 'menu_id' => $menuId + ]; + } + $this->roleMenuDao->saveAll($roleMenuData, false); + Db::commit(); + } catch (\Exception $exception) { + Db::rollback(); + Log::error($exception->getTraceAsString()); + throw new ApiException($exception->getMessage()); + } + return $role->toArray(); + } + + /** + * 更新数据 + * @param array $params + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function updateData(array $params) + { + $role = $this->dao->get(['id' => $params['id']]); + if (empty($role)) { + throw new ApiException(4020011); + } + // 检查code + if (!empty($params['code']) && $params['code'] != $role['code']) { + $existCode = $this->dao->be(['code' => $params['code']]); + if ($existCode) { + throw new ApiException(4020009); + } + } + // 检查名称 + if (!empty($params['name']) && $params['name'] != $role['name']) { + $existName = $this->dao->be(['name' => $params['name']]); + if ($existName) { + throw new ApiException(4020010); + } + } + // 要保存的菜单id + $menuIds = empty($params['menu_ids']) ? [] : $this->getMenuIdsByRoleMenuId($params['menu_ids']); + // 落库 + Db::startTrans(); + try { + $this->dao->update(['id' => $role['id']], $params); + if (!empty($menuIds)) { + $this->roleMenuDao->delete(['role_id' => $role['id']]); + $roleMenuData = []; + foreach ($menuIds as $menuId) { + $roleMenuData[] = [ + 'role_id' => $role['id'], + 'menu_id' => $menuId + ]; + } + $this->roleMenuDao->saveAll($roleMenuData, false); + } + Db::commit(); + } catch (\Exception $exception) { + Db::rollback(); + Log::error($exception->getTraceAsString()); + throw new ApiException($exception->getMessage()); + } + return $role->toArray(); + } + + /** + * 列表数据 + * @param array $params + * @return array + */ + public function listData(array $params): array + { + [$page, $limit] = RequestUtils::getPageParameter(); + [$sortRule, $sortField] = RequestUtils::getSortParameter(); + + $query = []; + if (isNotBlank($params['name'])) { + $query[] = ['name', 'like', '%' . $params['name'] . '%']; + } + if (isNotBlank($params['code'])) { + $query[] = ['code', 'like', '%' . $params['code'] . '%']; + } + if (isNotBlank($params['status'])) { + $query[] = ['status', '=', $params['status']]; + } + $list = $this->dao->getList($query, '*', $page, $limit, "sort desc, create_time desc"); + $count = $this->dao->getCount($query); + return compact('list', 'count'); + } + + /** + * 详情数据 + * @param mixed $id + * @return array + */ + public function readData(mixed $id): array + { + $role = $this->dao->get(['id' => $id]); + if (empty($role)) { + return []; + } + $role = $role->toArray(); + $roleMenuIds = $this->roleMenuDao->getColumn([['role_id', '=', $id]], 'menu_id'); + $role['menu_ids'] = $roleMenuIds; + return $role; + } + + /** + * 删除数据 + * @param mixed $id + * @return array + */ + public function deleteData(mixed $id): array + { + $role = $this->dao->get(['id' => $id]); + if (empty($role)) { + return []; + } + // todo 检查该角色下是否有用户 + // 落库 + Db::startTrans(); + try { + $this->dao->softDel($id); + $this->roleMenuDao->delete(['role_id' => $id]); + // todo 删除用户角色关联 + Db::commit(); + } catch (\Exception $exception) { + Db::rollback(); + Log::error($exception->getTraceAsString()); + throw new ApiException($exception->getMessage()); + } + return []; + } + + /** + * 简单角色列表 + * @param array $params + * @return array + */ + public function listPureData(array $params): array + { + $query = [ + 'status' => 1 + ]; + if (isNotBlank($params['name'])) { + $query[] = ['name', 'like', '%' . $params['name'] . '%']; + } + $list = $this->dao->getList($query, 'id, name', 0, 0, 'sort desc, create_time desc'); + return compact('list'); + } + + + // ============================================================ 私有方法 =============================================== + + + /** + * 获取角色应包含的所有菜单id + * @param $roleMenuId + * @return array + */ + private function getMenuIdsByRoleMenuId($roleMenuId): array + { + // 查询菜单信息 + $roleMenuId = array_unique($roleMenuId); + $menuIds = $this->menuDao->getColumn([['id', 'in', $roleMenuId]], 'id'); + if (empty($menuIds)) { + return []; + } + // 查询菜单的所有父级权限 + $allIdPath = $this->menuDao->getColumn([['id', 'in', $menuIds]], 'id_path'); + $allUUid = []; + foreach ($allIdPath as $idPath) { + $allUUid = array_merge($allUUid, explode(',', $idPath)); + } + $allUUid = array_unique($allUUid); + $allMenuIds = $this->menuDao->getColumn([['uuid', 'in', $allUUid]], 'id'); + return array_unique($allMenuIds); + } + + + + +} \ No newline at end of file diff --git a/plugin/piadmin/app/utils/AdminTokenUtils.php b/plugin/piadmin/app/utils/AdminTokenUtils.php new file mode 100644 index 0000000..a7f2f93 --- /dev/null +++ b/plugin/piadmin/app/utils/AdminTokenUtils.php @@ -0,0 +1,89 @@ +where(['admin_id' => $adminId])->field('role_id')->select()->toArray(); + if (sizeof($role_ids) == 0) { + throw new ApiException('无权限'); + } + $role_ids = array_column($role_ids, 'role_id'); + if (in_array(SystemAdminRole::SUPER_ADMIN_ID, $role_ids)) { + //超级管理员,直接返回所有 + $menuModel = new SystemMenu(); + $menu_ids = $menuModel->field('id')->select()->toArray(); + $menu_ids = array_column($menu_ids, 'id'); + } else { + //其他角色,按权限返回 + $roleMenuModel = new SystemRoleMenu(); + $menu_ids = $roleMenuModel->where([['role_id', 'in', $role_ids]])->field('menu_id')->select()->toArray(); + $menu_ids = array_column($menu_ids, 'menu_id'); + } + return $menu_ids; + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/utils/ArrayUtils.php b/plugin/piadmin/app/utils/ArrayUtils.php new file mode 100644 index 0000000..44ac763 --- /dev/null +++ b/plugin/piadmin/app/utils/ArrayUtils.php @@ -0,0 +1,339 @@ +toArray(); + } + if (empty($data)) { + return []; + } + $cols = array_column($data, $key); + return $unique ? array_unique($cols) : $cols; + } + + /** + * 将二维数组按某一列的值转为map, + * @param mixed $data 二维数组 + * @param string $key map键的列名 + * @param string|null $hKey 可选,map值中的键名,为空则整个数组元素作为值 + * @return array + */ + public static function toMap(mixed $data, string $key, string $hKey = null): array + { + if ($data instanceof Collection) { + $data = $data->all(); + } + if (empty($data)) { + return []; + } + $result = []; + foreach ($data as $item) { + if ($hKey !== null && isset($item[$hKey])) { + // 如果提供了 $hKey 并且数组中存在该键,则使用该键的值作为map的值 + $result[$item[$key]] = $item[$hKey]; + } else { + // 否则,使用整个数组元素作为map的值 + $result[$item[$key]] = $item; + } + } + return $result; + } + + /** + * 将二维数组按某一列分组并提取结果中的某一列 + * @param mixed $data 二维数组 + * @param string $groupKey 分组列 + * @param string $mapKey 提取列 + * @param bool $unique 结果去重 默认去重 + * @return array + */ + public static function groupAndMap($data, string $groupKey, string $mapKey, bool $unique = true) + { + if ($data instanceof Collection) { + $data = $data->toArray(); + } + if (empty($data)) { + return []; + } + $result = []; + foreach ($data as $item) { + if (!isset($result[$item[$groupKey]])) { + $result[$item[$groupKey]] = []; + } + $result[$item[$groupKey]][] = $item[$mapKey]; + } + // 去重 + if ($unique) { + foreach ($result as &$values) { + $values = array_unique($values); + } + } + return $result; + } + + /** + * 将二维数组按某一列分组并提取结果中的某一列 + * @param mixed $data 二维数组 + * @param string $groupKey 分组列 + * @return array + */ + public static function group($data, string $groupKey) + { + if ($data instanceof Collection) { + if ($data->isEmpty()) { + return []; + } + } + if (empty($data)) { + return []; + } + $result = []; + foreach ($data as $item) { + if (!isset($result[$item[$groupKey]])) { + $result[$item[$groupKey]] = []; + } + $result[$item[$groupKey]][] = $item; + } + return $result; + } + + + /** + * 根据指定的键和值列表过滤数据数组 + * + * @param array $data 要过滤的数据数组 + * @param string $filterKey 用于过滤的键名 + * @param array $values 允许的值列表 + * + * @return array 过滤后的数据数组 + */ + public static function filter($data, string $filterKey, array $values): array + { + // 初始化结果数组 + $result = []; + + // 遍历数据数组 + foreach ($data as $d) { + // 如果当前项的指定键的值在允许的值列表中,则将该项添加到结果数组中 + if (in_array($d[$filterKey], $values)) { + $result[] = $d; + } + } + + // 返回过滤后的结果数组 + return $result; + } + + /** + * 二维关联数组根据某个int类型的字段排序 默认倒序 + * @param array $data + * @param string $sortKey + * @param bool $isDesc + * @return array + */ + public static function sortByInt(array $data, string $sortKey, bool $isDesc = true): array + { + if (empty($data)) { + return $data; + } + if ($isDesc) { + usort($data, function ($a, $b) use ($sortKey) { + if ($a[$sortKey] == $b[$sortKey]) { + return 0; + } + return ($a[$sortKey] > $b[$sortKey]) ? -1 : 1; + }); + } else { + usort($data, function ($a, $b) use ($sortKey) { + return $a['score'] <=> $b['score']; + }); + } + return $data; + } + + /** + * 拼装多对多关联数据 + * @param $mainData + * @param $subData + * @param $associationData + * @return array + */ + public static function combineManyToManyAssociationsData($mainData, $subData, $associationData): array + { + $main = $mainData['data']; + $sub = $subData['data']; + $association = $associationData['data']; + $result = []; + foreach ($main as $data) { + $data[$mainData['join_data_key']] = []; + $result[$data[$mainData['main_key']]] = $data; + } + + $sub = array_column($sub, null, $subData['sub_key']); + + foreach ($association as $item) { + if (isset($sub[$item[$associationData['sub_key']]]) && isset($result[$item[$associationData['main_key']]])) { + $result[$item[$associationData['main_key']]][$mainData['join_data_key']][] = $sub[$item[$associationData['sub_key']]]; + } + } + return $result; + } + + /** + * 是否为普通数组 true普通数组 false关联数组 + * @param $array + * @return bool + */ + public static function isArrayIndexed($array): bool + { + return array_diff(array_keys($array), range(0, count($array) - 1)) === array(); + } + + /** + * 从关联数组中获取多个value + * @param $data + * @param $keys + * @return array + */ + public static function getMany($data, $keys): array + { + if (empty($data) || empty($keys)) { + return []; + } + + // 获取多个键对应的值 + return array_values(array_intersect_key($data, array_flip($keys))); + } + + /** + * 使用第一个数组中的值替换第二个数组中相同键的值 + * + * @param array $array1 用于替换的数组 + * @param array $array2 被替换的数组 + * + * @return array 替换后的数组 + */ + public static function replaceValues(array $array1, array $array2): array + { + foreach ($array1 as $key => $value) { + if (array_key_exists($key, $array2)) { + $array2[$key] = $value; + } + } + + // 返回替换后的数组 + return $array2; + } + + /** + * 过滤数组中的非空元素(不包括0) + * + * 该方法用于接收一个数组,并返回其中不含空值的元素子集空值的定义遵循 PHP 的空值规则, + * 包括 NULL、空字符串、0、false 等 + * + * @param array $data 需要进行过滤的数组 + * @return array 过滤后的数组,仅包含非空元素 + */ + public static function filterNotEmpty(array $data): array + { + // 如果传入的数组为空,则直接返回原数组,不做处理 + if (empty($data)) { + return $data; + } + // 使用 array_filter 函数配合自定义回调函数来过滤数组中的非空元素 + // 回调函数判断每个元素是否不为空,并返回布尔值,array_filter 根据回调函数的返回值决定是否保留该元素 + return array_filter($data, function ($var) { + return $var !== '' && $var !== null && $var !== false && $var !== []; + }); + } + + /** + * 从数组中随机选取指定数量的元素 + * @param array $sourceArray 源数组 + * @param int $selectCount 需要选取的数量 + * @return array 随机选取的元素数组 + */ + public static function getRandomElements(array $sourceArray, int $selectCount): array + { + // 边界条件处理 + if ($selectCount <= 0 || empty($sourceArray)) { + return []; + } + $arrayLength = count($sourceArray); + $selectCount = min($selectCount, $arrayLength); // 确保不超过数组长度 + + // 随机打乱数组顺序 + shuffle($sourceArray); + // 截取前N个元素 + return array_slice($sourceArray, 0, $selectCount); + } + + public static function keysToSnake($param) + { + if (empty($param)) { + return $param; + } + $delKey = []; + foreach ($param as $key => &$value) { + $newKey = Str::snake($key); + if ($newKey != $key) { + $delKey[] = $key; + } + if (is_array($value)) { + $value = self::keysToSnake($value); + } + $param[$newKey] = $value; + } + foreach ($delKey as $key) { + unset($param[$key]); + } + return $param; + } +} diff --git a/plugin/piadmin/app/utils/BcUtils.php b/plugin/piadmin/app/utils/BcUtils.php new file mode 100644 index 0000000..802abcf --- /dev/null +++ b/plugin/piadmin/app/utils/BcUtils.php @@ -0,0 +1,61 @@ +setBackgroundColor(242, 243, 245); + $captcha->build(120, 36); + + $uuid = Uuid::uuid4(); + $key = $uuid->toString(); + $expire = config('plugin.piadmin.piadmin.captcha.expire', 300); + $code = strtolower($captcha->getPhrase()); + + CacheHelper::set($key, $code, $expire); + + $img_content = $captcha->get(); + return [ + 'captcha_key' => $key, + 'image' => 'data:image/png;base64,' . base64_encode($img_content) + ]; + } + + /** + * 数字验证码 + * @param string $key + * @param int $length + * @return array + */ + public static function numberCaptcha(string $key, int $length = 4): array + { + $code = str_pad(rand(0, 999999), $length, '0', STR_PAD_LEFT); + $expire = config('plugin.piadmin.piadmin.captcha.expire', 300); + CacheHelper::set($key, $code, $expire); + return [ + 'captcha_key' => $key, + 'code' => $code, + ]; + } + + /** + * 验证码验证 + * @param string $uuid + * @param string|int $captcha + * @return bool + */ + public static function checkCaptcha(string $uuid, string|int $captcha): bool + { + try { + $code = CacheHelper::get($uuid); + CacheHelper::delete($uuid); + } catch (\Exception $e) { + throw new ApiException($e->getMessage()); + } + if (strtolower($captcha) !== $code) { + return false; + } + return true; + } +} diff --git a/plugin/piadmin/app/utils/CaptchaUtils.php b/plugin/piadmin/app/utils/CaptchaUtils.php new file mode 100644 index 0000000..644bb39 --- /dev/null +++ b/plugin/piadmin/app/utils/CaptchaUtils.php @@ -0,0 +1,183 @@ + $from]); + } + $length = $captchaConfig['length'] ?? 6; + for ($i = 0; $i < $length; $i++) { + $str .= $strPol[rand(0, $max)]; + } + + // 存入缓存 + $cacheKey = "captcha:{$from}:{$type}:{$code}"; + CacheUtils::set($cacheKey, $str, $captchaConfig['expire'] ?? 300); + // 重新发送的时间 + $banCacheKey = "captcha:{$from}:{$type}:{$code}_ban"; + CacheUtils::set($banCacheKey, $str, $captchaConfig['ban_expire'] ?? 60); + return $str; + } + + /** + * 发送邮件 + * @param string $captcha + * @param string $email + * @return array|string + */ + private static function sendEmail(string $captcha, string $email): array|string + { + $lang = 'zh-cn'; + validate(CaptchaValidate::class)->scene('sendEmail')->check(['email' => $email]); + $url = config("plugin.piadmin.settings.captcha.base_url") . config("plugin.piadmin.settings.captcha.email_message_url"); + $url = $url . '?' . http_build_query(config("plugin.piadmin.settings.captcha.get_params")); + $param = [ + 'email' => $email, + 'template_id' => config("plugin.piadmin.settings.captcha.email.template_code.{$lang}"), + 'template_param' => json_encode([ + 'code' => $captcha, + 'expire' => config("plugin.piadmin.settings.captcha.email.expire") / 60 + ]), + ]; + return CurlUtils::post($url, [], $param); + } + + /** + * 发送短信 + * @param string $captcha + * @param string $mobile + * @return array|string + */ + private static function sendSms(string $captcha, string $prefix, string $mobile): array|string + { + $lang = 'zh-cn'; + validate(CaptchaValidate::class)->scene('sendSms')->check(['mobile' => $mobile]); + $url = config("settings.captcha.base_url") . config("settings.captcha.phone_message_url"); + $url = $url . '?' . http_build_query(config("settings.captcha.get_params")); + $param = [ + 'phone' => $mobile, + 'phone_prefix' => $prefix, + 'template_id' => config("settings.captcha.sms.template_code.{$lang}"), + 'template_param' => json_encode([ + 'code' => $captcha, + 'expire' => config("settings.captcha.sms.expire") / 60 + ]), + ]; + return CurlUtils::post($url, [], $param); + } + + +} \ No newline at end of file diff --git a/plugin/piadmin/app/utils/CurlUtils.php b/plugin/piadmin/app/utils/CurlUtils.php new file mode 100644 index 0000000..cbb7a61 --- /dev/null +++ b/plugin/piadmin/app/utils/CurlUtils.php @@ -0,0 +1,157 @@ += 400) { + Log::warning("HTTP error: [$url] - $httpCode"); + throw new RuntimeException("HTTP error: $httpCode"); + } + if (self::isValidJson($data)) { + return json_decode($data, true); + } + return $data; + } catch (\Exception $exception) { + Log::error("Curl request failed: [$url] - " . $exception->getMessage()); + throw $exception; + } finally { + curl_close($ch); + } + } + + /** + * 发起GET请求并获取响应结果 + * + * 该方法主要用于发起GET类型的HTTP请求,并接收请求的响应结果 + * 它允许通过传递URL、请求头、请求数据和超时设置来定制请求 + * + * @param string $url 请求的URL地址,不能为空 + * @param array $headers 请求头数组,用于发送额外的HTTP头信息,默认为空数组 + * @param array $data 请求数据数组,用于发送额外的数据,默认为空数组 + * @param int $timeout 请求超时时间,单位为秒,默认为10秒 + * + * @return string|array 返回请求的响应结果,可能是字符串或数组 + */ + public static function get(string $url, array $headers = [], array $data = [], int $timeout = 10): string|array + { + return self::request($url, 'GET', $headers, $data, $timeout); + } + + /** + * 发起POST请求并返回响应结果 + * + * 该方法主要用于向指定URL发送POST请求,并根据参数设置请求头、请求体和超时时间 + * 它是一个静态方法,可以在不实例化类的情况下直接调用 + * + * @param string $url 请求的URL地址,不能为空 + * @param array $headers 请求头数组,用于设置HTTP请求的头部信息,默认为空数组 + * @param array $data 请求体数据数组,用于设置POST请求的主体内容,默认为空数组 + * @param int $timeout 请求超时时间,单位为秒,默认为10秒 + * @param bool $isJson 指示请求体数据是否应被格式化为JSON,默认为true + * + * @return string|array 返回响应结果,可能是字符串或数组,具体类型取决于请求的响应 + */ + public static function post(string $url, array $headers = [], array|string $data = [], int $timeout = 10, bool $isJson = true): string|array + { + return self::request($url, 'POST', $headers, $data, $timeout, $isJson); + } + + /** + * 判断字符串是否是有效的JSON格式 + * + * @param string $string 要检查的字符串 + * + * @return bool 如果字符串是有效的JSON格式,返回true;否则返回false + */ + private static function isValidJson(string $string): bool + { + json_decode($string); + return json_last_error() === JSON_ERROR_NONE; + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/utils/DateTimeUtils.php b/plugin/piadmin/app/utils/DateTimeUtils.php new file mode 100644 index 0000000..cdcb272 --- /dev/null +++ b/plugin/piadmin/app/utils/DateTimeUtils.php @@ -0,0 +1,243 @@ +setTimezone(new DateTimeZone($timezone)); + } catch (\Exception $e) { + Log::error("时间日期解析失败: {$e->getMessage()} 参数: timestamp: {$timestamp} timezone: {$timezone}"); + throw new FrameWorkException(2000007); + } + + } + + /** + * 获取当前时区 + * @return string + */ + public static function getTimezone(): string + { + $date_default_timezone_get = date_default_timezone_get(); + return Request::param('timezone') ?? $date_default_timezone_get; + } + + /** + * 检查日期字符串是否为有效的日期时间字符串 + * @param $dateStr + * @param $format + * @return bool + */ + public static function isValidDateTime(string $dateStr, string $format = 'Y-m-d'): bool + { + $date = DateTime::createFromFormat($format, $dateStr); + return $date != false; + } + + public static function beginTimestamp(string $dateStr): ?int + { + if (empty($dateStr)) { + return null; + } + try { + $dateTime = new \DateTime($dateStr); + $dateTime->setTime(0, 0, 0); + return $dateTime->getTimestamp(); + } catch (\Exception $e) { + return null; + } + } + + public static function endTimestamp(string $dateStr): ?int + { + if (empty($dateStr)) { + return null; + } + try { + $dateTime = new \DateTime($dateStr); + $dateTime->setTime(23, 59, 59); + return $dateTime->getTimestamp(); + } catch (\Exception $e) { + return null; + } + } + + public static function beginTime(string|DateTime $dateStr): ?string + { + if (empty($dateStr)) { + return null; + } + try { + if (!($dateStr instanceof DateTime)) { + $dateTime = new \DateTime($dateStr); + } else { + $dateTime = $dateStr; + } + $dateTime->setTime(0, 0, 0); + return $dateTime->format('Y-m-d H:i:s'); + } catch (\Exception $e) { + return null; + } + } + + public static function endTime(string|DateTime $dateStr): ?string + { + if (empty($dateStr)) { + return null; + } + try { + if (!($dateStr instanceof DateTime)) { + $dateTime = new \DateTime($dateStr); + } else { + $dateTime = $dateStr; + } + $dateTime->setTime(23, 59, 59); + return $dateTime->format('Y-m-d H:i:s'); + } catch (\Exception $e) { + return null; + } + } + + public static function begin(string|DateTime $dateStr): ?DateTime + { + if (empty($dateStr)) { + return null; + } + try { + if (!($dateStr instanceof DateTime)) { + $dateTime = new \DateTime($dateStr); + } else { + $dateTime = $dateStr; + } + $dateTime->setTime(0, 0, 0); + return $dateTime; + } catch (\Exception $e) { + return null; + } + } + + public static function end(string|DateTime $dateStr): ?DateTime + { + if (empty($dateStr)) { + return null; + } + try { + if (!($dateStr instanceof DateTime)) { + $dateTime = new \DateTime($dateStr); + } else { + $dateTime = $dateStr; + } + $dateTime->setTime(23, 59, 59); + return $dateTime; + } catch (\Exception $e) { + return null; + } + } + + public static function generateDateRange(string $startDate, string $endDate, string $format = 'Y-m-d'): array { + $dates = []; + $current = new \DateTime($startDate); + $end = new \DateTime($endDate); + + // 确保结束日期 >= 开始日期 + if ($current > $end) { + return []; + } + + // 循环生成每一天 + while ($current <= $end) { + $dates[] = $current->format($format); + $current->add(new DateInterval('P1D')); // 增加 1 天 + } + + return $dates; + } + + /** + * 是否为秒级时间戳 + * @param $timestamp + * @return bool + */ + public static function isSecondTimestamp($timestamp): bool + { + // 10位数字通常是秒级时间戳 + return is_numeric($timestamp) && strlen((string)$timestamp) === 10; + } + + /** + * 是否为毫秒级时间戳 + * @param $timestamp + * @return bool + */ + public static function isMilliTimestamp($timestamp): bool + { + // 10位数字通常是秒级时间戳 + return is_numeric($timestamp) && strlen((string)$timestamp) === 13; + } + + public static function toDateStr(mixed $val, string $default='--', string $format = 'Y-m-d'): string + { + if (empty($val)) { + return $default; + } + $dateTime = null; + try { + if (!($val instanceof DateTime)) { + $dateTime = new \DateTime($val); + } else { + $dateTime = $val; + } + $year = $dateTime->format('Y'); + if ($year < 2010) { + return $default; + } + return $dateTime->format($format); + } catch (\Exception $e) { + return $default; + } + } + + public static function toDateTimeStr(mixed $val): string + { + return self::toDateStr($val, '--', 'Y-m-d H:i:s'); + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/utils/DbUtils.php b/plugin/piadmin/app/utils/DbUtils.php new file mode 100644 index 0000000..7165e70 --- /dev/null +++ b/plugin/piadmin/app/utils/DbUtils.php @@ -0,0 +1,17 @@ +make(SysDictItemService::class); + return $dictItemService->selectList([ + 'dict_code' => $dictCode, + 'delete_time' => 0, + 'status' => 1 + ], 'dict_code, code, value, label')->toArray(); + }, 7200); + } + + /** + * 清除指定字典的缓存 + * @param string|array $dictCode + * @return void + */ + public static function clearDictCache(string|array $dictCode): void + { + $lang = Lang::getLangSet(); + if (is_string($dictCode)) { + $dictCode = [$dictCode]; + } + foreach ($dictCode as $item) { + $cacheKey = "dict:{$item}_{$lang}"; + CacheUtils::delete($cacheKey); + } + } + + /** + * 校验字典值是否存在 + * @param string $dictCode + * @param string $itemValue + * @return bool + */ + public static function validateDictValue(string $dictCode, string $itemValue): bool + { + $dictItems = self::getDictItems($dictCode); + $dictValues = ArrayUtils::map($dictItems, 'value'); + return in_array($itemValue, $dictValues); + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/utils/IdPathUtils.php b/plugin/piadmin/app/utils/IdPathUtils.php new file mode 100644 index 0000000..5a0fb31 --- /dev/null +++ b/plugin/piadmin/app/utils/IdPathUtils.php @@ -0,0 +1,17 @@ +toString()); + } +} diff --git a/plugin/piadmin/app/utils/JwtUtils.php b/plugin/piadmin/app/utils/JwtUtils.php new file mode 100644 index 0000000..0ecd6c3 --- /dev/null +++ b/plugin/piadmin/app/utils/JwtUtils.php @@ -0,0 +1,98 @@ +host(); + // 获取当前时间戳 + $now = time(); + // 构造过期时间的字符串表示 + $expireTime = strtolower("+ {$expire} seconds"); + $uniId = uniqid(); + + // 向负载数据中添加JWT标准声明 + $payload += [ + 'iss' => $host, // 发布者 + 'iat' => $now, // 签发时间 + 'exp' => strtotime($expireTime), // 过期时间 + 'nbf' => $now, // 生效时间 + 'aud' => $host, // 接收方 + ]; + // 构造包含用户ID和类型的参数数组 + $payload['jti'] = compact('id', 'type', 'uniId'); + // 使用HS256算法编码JWT令牌 + $token = JWT::encode($payload, $key, 'HS256', $keyId); + // 返回包含生成的令牌和过期时间的信息 + return [ + 'token' => $token, + 'expire' => $expire + ]; + } + + /** + * 获取用户所有登录信息 + * todo 补全redis操作部分 + * @param $userId + * @return array + */ + public static function getAllTokenList($userId): array + { + $currentToken = self::getCurrentToken(); + $redisConn = Cache::store('redis')->handler(); + $prefix = 'geo:user:token:' . $userId . ':'; + $cursor = '0'; // 初始游标 + $data = []; + $keys = $redisConn->scan($cursor, $prefix . '*', 1000); + foreach ($keys as $key) { + $key = str_replace('geo:', '', $key); + $tmp = json_decode(CacheUtils::get($key), true); + $data[] = [ + 'id' => md5($tmp['token']), + 'login_time' => $tmp['login_time'], + 'os_type' => $tmp['os_type'], + // todo 补全其他操作系统图标 + 'os_icon' => 'https://luckin-east-test.oss.neicela.com/geo/sys_icon/windows.svg', + 'browser_name' => $tmp['browser_name'], + 'login_ip' => $tmp['login_ip'], + 'is_current' => $currentToken == $tmp['token'] ? 1 : 0, + ]; + } + return $data; + } + + /** + * 获取当前登录的token + * @param string $type + * @return string + */ + public static function getCurrentToken(string $type = 'user'): string + { + $authHeader = config("piadmin.jwt.{$type}.auth_header"); + $token = request()->header($authHeader, ''); + return trim(str_replace('Bearer ', '', $token)); + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/utils/LangUtils.php b/plugin/piadmin/app/utils/LangUtils.php new file mode 100644 index 0000000..a6cbfb3 --- /dev/null +++ b/plugin/piadmin/app/utils/LangUtils.php @@ -0,0 +1,65 @@ +make(LangPackService::class); + if (empty($lang)) { + $lang = Lang::getLangSet(); + } + // 缓存key + $cacheKey = 'lang_pack:' . $lang; + $langPackageData = CacheUtils::remember($cacheKey, function () use ($langPackService, $lang) { + return $langPackService->getColumn([ + 'lang' => $lang, + 'delete_time' => 0 + ], 'lang_value', 'lang_key'); + }, 3600); + $message = $langPackageData[$code] ?? 'Parse Code Error'; + + // 填充 + return StrUtils::format($message, $data); + } catch (\Throwable $e) { + Log::error('获取语言code:' . $code . '发成错误,错误原因是:' . json_encode([ + 'file' => $e->getFile(), + 'message' => $e->getMessage(), + 'line' => $e->getLine() + ])); + return $code; + } + } + + /** + * 清除指定语言的包缓存 + * + * 该方法用于删除缓存中指定语言的语言包信息如果未指定语言,将使用系统当前的语言设置 + * 主要用于在语言变更或更新语言包后,清除旧的缓存信息,以确保最新的语言信息被加载 + * + * @param string $lang 可选参数指定要清除缓存的语言如果未提供,默认使用当前系统语言 + * @return void + */ + public static function clearPackCache(string $lang = ''): void + { + // 如果未提供语言参数或提供的语言参数为空,则使用系统当前的语言设置 + if (empty($lang)) { + $lang = Lang::getLangSet(); + } + + // 删除指定语言的语言包缓存 + CacheUtils::delete('lang_pack:' . $lang); + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/utils/RandomUtils.php b/plugin/piadmin/app/utils/RandomUtils.php new file mode 100644 index 0000000..367aad0 --- /dev/null +++ b/plugin/piadmin/app/utils/RandomUtils.php @@ -0,0 +1,50 @@ +format("Y-m-d"); + $hashValue = md5($dateStr); + + // 使用哈希值作为随机种子 + $seed = hexdec(substr($hashValue, 0, 8)); + srand($seed); + + // Fisher-Yates 洗牌算法打乱数组 + $n = count($srcArray); + for ($i = $n - 1; $i > 0; $i--) { + $j = rand(0, $i); + $temp = $srcArray[$i]; + $srcArray[$i] = $srcArray[$j]; + $srcArray[$j] = $temp; + } + return $srcArray; + } + + /** + * 生成随机种子 + * @param string $seedStr + * @return string + */ + public static function generateRandomSeed(string $seedStr): string + { + // SHA256哈希计算 + $hash = hash('sha256', $seedStr); + // 取前8位十六进制并转换为整数 + $hex_part = substr($hash, 0, 8); + return hexdec($hex_part); + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/utils/RequestUtils.php b/plugin/piadmin/app/utils/RequestUtils.php new file mode 100644 index 0000000..2cbbe1a --- /dev/null +++ b/plugin/piadmin/app/utils/RequestUtils.php @@ -0,0 +1,76 @@ +input('page',1); + $limit = request()->input('limit',15); + }catch(\Throwable $e){ + $page = 1; + $limit = 15; + } +// $params = request()->only([ +// config('settings.paging.page.name') => config('settings.paging.page.default'), +// config('settings.paging.limit.name') => config('settings.paging.limit.default'), +// ]); + return [$page, $limit]; + } + + + // 获取本次请求管理员信息 + public static function getAdminInfo(): array{ + + $admininfo = [ + 'id'=>'', + 'name'=>'' + ]; + + try{ + $admin = request()->admin; + if(!empty($admin)){ + $admininfo = [ + 'id'=>$admin['id'] ?? '', + 'name'=>$admin['name'] ?? '' + ]; + } + + }catch(\Throwable $e){ + } + + return $admininfo; + } + + /** + * 获取本次请求的排序参数 + * @return array + */ + public static function getSortParameter(): array + { + $params = requestOnly([ + config('plugin.piadmin.piadmin.paging.sort_rule.name') => config('plugin.piadmin.piadmin.paging.sort_rule.default'), + config('plugin.piadmin.piadmin.paging.sort_field.name') => config('plugin.piadmin.piadmin.paging.sort_field.default'), + ]); + if (!in_array($params[config('plugin.piadmin.piadmin.paging.sort_rule.name')], config('plugin.piadmin.piadmin.paging.sort_rule.allow'))) { + $params[config('plugin.piadmin.piadmin.paging.sort_rule.name')] = config('plugin.piadmin.piadmin.paging.sort_rule.default'); + } + if (!in_array($params[config('plugin.piadmin.piadmin.paging.sort_field.name')], config('plugin.piadmin.piadmin.paging.sort_field.allow'))) { + $params[config('plugin.piadmin.piadmin.paging.sort_field.name')] = config('plugin.piadmin.piadmin.paging.sort_field.default'); + } + return array_values($params); + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/utils/SecretUtils.php b/plugin/piadmin/app/utils/SecretUtils.php new file mode 100644 index 0000000..3fda726 --- /dev/null +++ b/plugin/piadmin/app/utils/SecretUtils.php @@ -0,0 +1,25 @@ + $value) { + $template = str_replace('{' . $key . '}', $value, $template); + } + return $template; + } + + /** + * 安全截取字符串 + * @param string $string + * @param int $start + * @param int $length + * @param string $encoding + * @return string + */ + public static function safeSub(string $string, int $start, int $length, string $encoding = 'UTF-8'): string { + if ($encoding === 'UTF-8') { + $maxLength = mb_strlen($string, $encoding) - $start; + $safeLength = min($length, $maxLength); + return mb_substr($string, $start, $safeLength, $encoding); + } else { + $maxLength = strlen($string) - $start; + $safeLength = min($length, $maxLength); + return substr($string, $start, $safeLength); + } + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/utils/TreeUtils.php b/plugin/piadmin/app/utils/TreeUtils.php new file mode 100644 index 0000000..c77325b --- /dev/null +++ b/plugin/piadmin/app/utils/TreeUtils.php @@ -0,0 +1,60 @@ += 3 && in_array($parts[$count - 2], ['com', 'net', 'org', 'edu', 'gov'])) { + return $parts[$count - 3] . '.' . $parts[$count - 2] . '.' . $parts[$count - 1]; + } + return $parts[$count - 2] . '.' . $parts[$count - 1]; + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/utils/storage/AliOssStorage.php b/plugin/piadmin/app/utils/storage/AliOssStorage.php new file mode 100644 index 0000000..02113eb --- /dev/null +++ b/plugin/piadmin/app/utils/storage/AliOssStorage.php @@ -0,0 +1,275 @@ + $expiration, + 'conditions' => [ + ['bucket' => $bucket] + ] + ]; + $stringToSign = base64_encode(json_encode($policy)); + $signature = base64_encode(hash_hmac('sha1', $stringToSign, $accessKeySecret, true)); + + $host = "https://{$bucket}.{$endpoint}"; + return [ + 'expireTime' => $expiration, + 'host' => $host, + 'ossAccessKeyId' => $accessKeyId, + 'policy' => $stringToSign, + 'signature' => $signature, + ]; + + } + + /** + * 获取上传签名 + * + * @throws OssException + */ + public function getPutSign(string $object, string $method, string $contentType): array + { + // 检查文件 + $ossConfig = config('oss.ali'); + $disallowedExt = $ossConfig['disallowed_upload_ext']; + // 文件名称 + $objectName = parse_url($object, PHP_URL_PATH); + // 后缀 + $ext = pathinfo($objectName, PATHINFO_EXTENSION); + if (in_array(strtolower($ext), $disallowedExt)) { + throw new ApiException("禁止上传{$ext}文件"); + } + + // 读取配置 + $accessKeyId = $ossConfig['access_key_id']; + $accessKeySecret = $ossConfig['access_key_secret']; + $endpoint = $ossConfig['endpoint']; + $bucket = $ossConfig['bucket']; + $region = $ossConfig['region']; + $host = "https://{$bucket}.{$endpoint}"; + + // 构建签名参数 + $credentials = new Credentials($accessKeyId, $accessKeySecret); + $request = new RequestCore($host); + $request->set_method($method); + + $date = gmdate("D, d M Y H:i:s T"); + $request->add_header("Date", $date); + $request->add_header("x-oss-date", $date); + $request->add_header("content-type", $contentType); + + $signer = new SignerV1(); + $signingOpt = array( + 'bucket' => $bucket, + 'key' => $object, + 'region' => $region, + 'product' => 'oss', + ); + + // 签名并返回 + $signer->sign($request, $credentials, $signingOpt); + $returnData = $request->request_headers; + $returnData['host'] = "{$bucket}.{$endpoint}"; + $returnData['xOssContentSha256'] = 'UNSIGNED-PAYLOAD'; + $returnData['object'] = $object; + return $returnData; + } + + /** + * 切片上传完成 + * @throws \DOMException + * @throws OssException + */ + public function completeMultipartUpload(array $params): array + { + // 构建完成分片上传的xml参数 + $part = ''; + foreach ($params['body_arr'] as $value) { + $value['e_tag'] = str_replace('"', '', $value['e_tag']); + $part .= "{$value['part_number']}{$value['e_tag']}"; + } + $xmlParams = "{$part}"; + $requestParam = $this->getPutSign([ + 'object' => "{$params['object_name']}?uploadId={$params['upload_id']}", + 'method' => 'post', + 'content_type' => $params['mime'], + ]); + + // 构建请求参数 + $url = "https://{$requestParam['host']}/{$params['object_name']}?uploadId={$params['upload_id']}"; + $header = [ + 'Authorization: ' . $requestParam['Authorization'], + 'Host: ' . $requestParam['host'], + 'Date: ' . $requestParam['Date'], + 'x-oss-date: ' . $requestParam['Date'], + 'Content-type: ' . $requestParam['content-type'], + ]; + $postData = $xmlParams; + $response = CurlUtils::post($url, $header, $postData, 10, false); + + // 获取请求结果并验证 + $xml = simplexml_load_string($response['data']); + $resData = []; + foreach($xml->children() as $child) { + $resData[$child->getName()] = strval($child); + } + if (!empty($resData['Code'])) { + Log::error('切片上传完成接口失败:' . json_encode($resData)); + throw new ApiException('文件保存失败'); + } + return $this->complete($params); + } + + /** + * 获取文件元数据 + * @param string $object + * @return array|null + */ + public function getObjectMate(string $object): ?array + { + $ossConfig = config('filesystem.disks.oss'); + // 读取配置 + $accessKeyId = $ossConfig['access_key_id']; + $accessKeySecret = $ossConfig['access_key_secret']; + $endpoint = $ossConfig['endpoint']; + $bucket = $ossConfig['bucket']; + $region = $ossConfig['region']; + $host = "https://{$bucket}.{$endpoint}"; + + // 使用代码嵌入的RAM用户的访问密钥配置访问凭证。 + $provider = new StaticCredentialsProvider($accessKeyId, $accessKeySecret); + + $config = array( + "provider" => $provider, + "endpoint" => $endpoint, + ); + $ossClient = new OssClient($config); + + try { + // 获取文件的全部元数据。 + return $ossClient->getObjectMeta($bucket, $object); + } catch (\Exception $e) { + Log::error("获取ObjectMeta失败: {$object}"); + throw new ApiException('获取文件元数据失败'); + } + } + + /** + * oss上传落库 + * @param array $params + * @return array + */ + public function complete(array $params): array + { + // 获取文件mate + $mateData = $this->getObjectMate($params['object_name']); + if (empty($mateData)) { + Log::error('获取文件mate失败-保存附件失败!文件名:' . $params['object_name']); + throw new ApiException('文件保存失败'); + } + // 文件名称 + $objectName = pathinfo($params['object_name'], PATHINFO_FILENAME); + // 原名 + $originName = pathinfo($params['origin_name'], PATHINFO_FILENAME); + // 文件后缀 + $ext = strtolower(pathinfo($params['object_name'], PATHINFO_EXTENSION)); + $attachmentData = [ + 'seller_id' => session('user')['seller_id'], + 'title' => "$originName.$ext", + 'filesize' => $mateData['info']['download_content_length'], + 'source' => str_replace('http://', 'https://', $mateData['info']['url']), + 'sha1' => $params['hash'], + 'storage' => 2, + ]; + $attachmentData['uid'] = session('user')['id'] ?? ''; + $attachmentData['name'] = $attachmentData['origin_name']; + unset($attachmentData['oss_mate']); + return $attachmentData; + } + + /** + * 上传流到oss + * @param string $object + * @param mixed $content + * @return array + * @throws ApiException + */ + public function putObject(string $object, mixed $content): array + { + $ossConfig = config('oss.ali'); + $accessKeyId = $ossConfig['access_key_id']; + $accessKeySecret = $ossConfig['access_key_secret']; + $endpoint = $ossConfig['endpoint']; + $bucket = $ossConfig['bucket']; + $host = "https://{$bucket}.{$endpoint}"; + try { + $ossClient = new OssClient($accessKeyId, $accessKeySecret, $endpoint); + $result = $ossClient->putObject($bucket, $object, $content); + return $result; + } catch (\Exception $e) { + Log::error("上传文件失败:{$object} bucket: {$bucket}"); + throw new ApiException('文件保存失败'); + } + + } + + /** + * 上传文件到oss + * @param string $filePath + * @param string $originName + * @param string $objectPath + * @return array|null + */ + public function uploadObject(string $filePath, string $originName, string $objectPath = ''): ?array + { + $ossConfig = config('plugin.piadmin.oss.ali'); + // 读取配置 + $accessKeyId = $ossConfig['access_key_id']; + $accessKeySecret = $ossConfig['access_key_secret']; + $endpoint = $ossConfig['endpoint']; + $bucket = $ossConfig['bucket']; + $ext = strtolower(pathinfo($originName, PATHINFO_EXTENSION)); + $objectFileName = uniqid() . '.' . $ext; + // oss文件的保存位置 + $objectName = !empty($objectPath) ? "$objectPath/{$objectFileName}" : $objectFileName; + try { + $ossClient = new OssClient($accessKeyId, $accessKeySecret, $endpoint); + $result = $ossClient->uploadFile($bucket, $objectName, $filePath); + return $result; + } catch (\Exception $e) { + Log::error("上传文件失败:{$objectName} bucket: {$bucket}"); + throw new ApiException('上传文件失败'); + } + } +} \ No newline at end of file diff --git a/plugin/piadmin/app/validate/LoginValidate.php b/plugin/piadmin/app/validate/LoginValidate.php new file mode 100644 index 0000000..fdccc89 --- /dev/null +++ b/plugin/piadmin/app/validate/LoginValidate.php @@ -0,0 +1,28 @@ + 'require', + 'password' => 'require', + 'captcha' => 'require', + 'captcha_key' => 'require', + ]; + + protected $message = [ + 'username.require' => '400001', + 'password.require' => '400002', + 'captcha.require' => '400003', + 'captcha_key.require' => '400003', + ]; + + protected $scene = [ + 'loginByPassword' => ['username', 'password'] + ]; + +} \ No newline at end of file diff --git a/plugin/piadmin/app/validate/MenuValidate.php b/plugin/piadmin/app/validate/MenuValidate.php new file mode 100644 index 0000000..5a6dd77 --- /dev/null +++ b/plugin/piadmin/app/validate/MenuValidate.php @@ -0,0 +1,26 @@ + 'require', + 'pid' => 'require', + 'name' => 'require', + 'code' => 'require', + 'is_hidden' => 'require', + 'type' => 'require', + 'status' => 'require', + ]; + + + protected $scene = [ + 'save' => ['pid', 'name','code','type'], + 'update' => ['id'], + ]; + +} \ No newline at end of file diff --git a/plugin/piadmin/app/validate/SystemAdminValidate.php b/plugin/piadmin/app/validate/SystemAdminValidate.php new file mode 100644 index 0000000..3e873c4 --- /dev/null +++ b/plugin/piadmin/app/validate/SystemAdminValidate.php @@ -0,0 +1,44 @@ + [ + 'name' => 'require|max:100', + 'account' => 'require|max:100', + 'dept_id' => 'require', + 'password' => 'require|min:8|max:32', + 'phone' => 'mobile', + 'email' => 'email', + 'role_ids' => 'require|array' + ], + 'update' => [ + 'id' => 'require', + 'name' => 'max:100', + 'account' => 'max:100', + 'password' => 'min:8|max:32', + 'phone' => 'mobile', + 'email' => 'email', + 'role_ids' => 'array' + ] + ]; + protected $message = [ + 'name.require' => '4040001', + 'name.max' => '4040002', + 'account.require' => '4040003', + 'account.max' => '4040004', + 'dept_id.require' => '4040005', + 'password.require' => '4040006', + 'password.min' => '4040007', + 'password.max' => '4040008', + 'phone.mobile' => '4040009', + 'email.email' => '4040010', + 'role_ids.require' => '4040011', + 'role_ids.array' => '4040012', + 'id.require' => '4040015' + ]; +} \ No newline at end of file diff --git a/plugin/piadmin/app/validate/SystemDeptValidate.php b/plugin/piadmin/app/validate/SystemDeptValidate.php new file mode 100644 index 0000000..c71e434 --- /dev/null +++ b/plugin/piadmin/app/validate/SystemDeptValidate.php @@ -0,0 +1,39 @@ + [ + 'pid' => 'require', + 'name' => 'require|max:100', + 'sort' => 'integer', + 'leader' => 'integer', + 'phone' => 'mobile', + 'email' => 'email' + ], + 'update' => [ + 'id' => 'require', + 'pid' => 'require', + 'name' => 'max:100', + 'sort' => 'integer', + 'leader' => 'integer', + 'phone' => 'mobile', + 'email' => 'email' + ] + ]; + + protected $message = [ + 'name.require' => '4030001', + 'name.max' => '4030002', + 'sort.integer' => '4030003', + 'leader.integer' => '4030004', + 'phone.mobile' => '4030005', + 'email.email' => '4030006', + 'id.require' => '4030008', + 'pid.require' => '4030010', + ]; +} \ No newline at end of file diff --git a/plugin/piadmin/app/validate/SystemRoleValidate.php b/plugin/piadmin/app/validate/SystemRoleValidate.php new file mode 100644 index 0000000..1f7b523 --- /dev/null +++ b/plugin/piadmin/app/validate/SystemRoleValidate.php @@ -0,0 +1,41 @@ + [ + 'name' => 'require|max:100', + 'code' => 'require|max:100', + 'status' => 'require', + 'remark' => 'max:500', + 'menu_ids' => 'array', + 'sort' => 'integer' + ], + 'update' => [ + 'id' => 'require', + 'name' => 'max:100', + 'code' => 'max:100', + 'status' => 'require', + 'remark' => 'max:500', + 'menu_ids' => 'array', + 'sort' => 'integer' + ] + ]; + + protected $message = [ + 'name.require' => '4020001', + 'name.max' => '4020002', + 'code.require' => '4020003', + 'code.max' => '4020004', + 'status.require' => '4020005', + 'remark.max' => '4020006', + 'menu_ids.require' => '4020007', + 'menu_ids.array' => '4020008', + 'sort.integer' => '4020013', + 'id.require' => '4020014' + ]; +} \ No newline at end of file diff --git a/plugin/piadmin/app/validate/system/CaptchaValidate.php b/plugin/piadmin/app/validate/system/CaptchaValidate.php new file mode 100644 index 0000000..39489af --- /dev/null +++ b/plugin/piadmin/app/validate/system/CaptchaValidate.php @@ -0,0 +1,39 @@ + [ + 'to' => 'require|email', + 'type' => 'require' + ], + 'sms' => [ + 'to' => 'require|mobile', + 'type' => 'require' + ] + ]; + protected $message = [ + 'to.require' => '目标不能为空', + 'to.email' => '邮箱地址错误', + 'to.mobile' => '手机号错误', + 'type.require' => '验证码类型不能为空', + 'email.require' => '邮箱地址不能为空', + 'email.email' => '邮箱地址错误', + 'mobile.require' => '手机号不能为空', + 'mobile.mobile' => '手机号错误', + ]; + + protected $rule = [ + 'email' => 'require|email', + 'mobile' => 'require|mobile' + ]; + + protected $scene = [ + 'sendEmail' => ['email'], + 'sendSms' => ['mobile'] + ]; +} \ No newline at end of file diff --git a/plugin/piadmin/app/validate/system/LangValidate.php b/plugin/piadmin/app/validate/system/LangValidate.php new file mode 100644 index 0000000..0c3fca1 --- /dev/null +++ b/plugin/piadmin/app/validate/system/LangValidate.php @@ -0,0 +1,26 @@ + 'require', + 'lang_key' => 'require|max:255', + 'lang_value' => 'require|max:500', + ]; + + protected $scene = [ + 'savePackageData' => ['lang_key', 'lang_value'], + 'deletePackageData' => ['id'], + ]; + + protected $message = [ + 'id.require' => '400001', + 'lang_key.require' => '100001', + 'lang_value.require' => '401002', + ]; + +} \ No newline at end of file diff --git a/plugin/piadmin/app/validate/system/OssValidate.php b/plugin/piadmin/app/validate/system/OssValidate.php new file mode 100644 index 0000000..f9a747f --- /dev/null +++ b/plugin/piadmin/app/validate/system/OssValidate.php @@ -0,0 +1,26 @@ + 'require', + 'content_type' => 'require', + 'method' => 'require|in:post,put', + ]; + + protected $message = [ + 'file_name.require' => '4030004', + 'content_type.require' => '4030005', + 'method.require' => '4030006', + 'method.in' => '4030007', + ]; + + protected $scene = [ + 'ossSign' => ['file_name', 'content_type', 'method'], + ]; +} \ No newline at end of file diff --git a/plugin/piadmin/config/app.php b/plugin/piadmin/config/app.php new file mode 100644 index 0000000..addfc34 --- /dev/null +++ b/plugin/piadmin/config/app.php @@ -0,0 +1,10 @@ + true, + 'controller_suffix' => 'Controller', + 'controller_reuse' => false, + 'version' => '1.0.0' +]; diff --git a/plugin/piadmin/config/autoload.php b/plugin/piadmin/config/autoload.php new file mode 100644 index 0000000..dc8bff0 --- /dev/null +++ b/plugin/piadmin/config/autoload.php @@ -0,0 +1,6 @@ + [ + base_path() . '/plugin/piadmin/app/functions.php', + ] +]; \ No newline at end of file diff --git a/plugin/piadmin/config/container.php b/plugin/piadmin/config/container.php new file mode 100644 index 0000000..6d15a8c --- /dev/null +++ b/plugin/piadmin/config/container.php @@ -0,0 +1,6 @@ + support\exception\Handler::class, + '' => \plugin\piadmin\exception\ExceptionHandler::class +]; diff --git a/plugin/piadmin/config/log.php b/plugin/piadmin/config/log.php new file mode 100644 index 0000000..dc28285 --- /dev/null +++ b/plugin/piadmin/config/log.php @@ -0,0 +1,20 @@ + [ + 'handlers' => [ + [ + 'class' => Monolog\Handler\RotatingFileHandler::class, + 'constructor' => [ + runtime_path() . '/logs/piadmin.log', + 7, + Monolog\Logger::DEBUG, + ], + 'formatter' => [ + 'class' => Monolog\Formatter\LineFormatter::class, + 'constructor' => [null, 'Y-m-d H:i:s', true], + ], + ] + ], + ], +]; diff --git a/plugin/piadmin/config/menu.php b/plugin/piadmin/config/menu.php new file mode 100644 index 0000000..0b67a5f --- /dev/null +++ b/plugin/piadmin/config/menu.php @@ -0,0 +1,3 @@ + [ + CrossDomainMiddleware::class, + LanguageMiddleware::class, + ParameterConvertMiddleware::class + ] +]; diff --git a/plugin/piadmin/config/oss.php b/plugin/piadmin/config/oss.php new file mode 100644 index 0000000..4301ea0 --- /dev/null +++ b/plugin/piadmin/config/oss.php @@ -0,0 +1,15 @@ + [ + 'access_key_id' => env('ALIYUN_OSS_ACCESS_KEY_ID'), + 'access_key_secret' => env('ALIYUN_OSS_ACCESS_KEY_SECRET'), + 'bucket' => env('ALIYUN_OSS_BUCKET'), + 'endpoint' => env('ALIYUN_OSS_ENDPOINT'), + 'region' => env('ALIYUN_OSS_REGION'), + 'host' => 'https://' . env('ALIYUN_OSS_BUCKET') . '.' . env('ALIYUN_OSS_ENDPOINT'), + 'domain' => env('ALIYUN_OSS_DOMAIN'), + // 禁止上传的文件后缀 + 'disallowed_upload_ext' => [] + ] +]; diff --git a/plugin/piadmin/config/piadmin.php b/plugin/piadmin/config/piadmin.php new file mode 100644 index 0000000..dc5763c --- /dev/null +++ b/plugin/piadmin/config/piadmin.php @@ -0,0 +1,44 @@ + base_path(). '/plugin/piadmin', + + // 验证码配置 + 'captcha' => [ + // 验证码过期时间 (秒) + 'expire' => 300, + ], + + // jwt配置 + 'jwt' => [ + 'user' => [ + 'key' => 'ECA3Zfn09J9xWsiRCFNdjYRHXfPw5De5Pay7jCSX0cT3hhi3pjDS0Yn1RapNa0aK', + 'expire' => 600000, + 'auth_header' => 'Authorization', + 'key_id' => 'user' + ], + 'admin' => [ + 'key' => '13k8EPyxPe830cEnKe8pyx5G2hAdfmk8meWf5yHdNiYYYAbYH7eCEejmFTFf87pK', + 'expire' => 600000, + 'auth_header' => 'Authorization', + 'key_id' => 'admin' + ] + ], + + // 分页参数配置 + 'paging' => [ + 'page' => ['name' => 'page', 'default' => 1], + 'limit' => ['name' => 'limit', 'default' => 15], + 'sort_rule' => [ + 'name' => 'sort_rule', + 'default' => 'desc', + 'allow' => ['asc', 'desc'] + ], + 'sort_field' => [ + 'name' => 'sort_field', + 'default' => 'create_time', + 'allow' => ['create_time', 'update_time'] + ], + ], +]; diff --git a/plugin/piadmin/config/process.php b/plugin/piadmin/config/process.php new file mode 100644 index 0000000..881ab67 --- /dev/null +++ b/plugin/piadmin/config/process.php @@ -0,0 +1,2 @@ + '0.0.1-dev', + // 分布式节点配置 (主要用于实现雪花算法) + 'node' => [ + // 数据中心id + 'datacenter_id' => env('DC_ID'), + // 节点id + 'worker_id' => env('WORKER_ID'), + ], + // 分页参数配置 + 'paging' => [ + 'page' => ['name' => 'page', 'default' => 1], + 'limit' => ['name' => 'limit', 'default' => 15], + 'sort_rule' => [ + 'name' => 'sort_rule', + 'default' => 'desc', + 'allow' => ['asc', 'desc'] + ], + 'sort_field' => [ + 'name' => 'sort_field', + 'default' => 'create_time', + 'allow' => ['create_time', 'update_time'] + ], + ], + // 验证码参数配置 + 'captcha' => [ + 'base_url' => 'https://open-api.suq.cn', + 'phone_message_url' => '/api/v1/phone.sendMessage', + 'email_message_url' => '/api/v1/email.sendMessage', + 'get_params' => [ + 'app_id' => 'd8c1cf4d56', + 'identity' => '3511216babc7f57b72c9b7f62ea89bb6', + ], + 'email' => [ + 'expire' => 900, + 'length' => 6, + 'ban_expire' => 10, + 'template_code' => [ + 'zh-cn' => 9, + 'en-us' => 10, + ], + ], + 'sms' => [ + 'expire' => 900, + 'length' => 6, + 'ban_expire' => 10, + 'template_code' => [ + 'zh-cn' => 5, + 'en-us' => 5, + ], + ], + // 验证码类型 + 'types' => [ + 'email.user.register', + 'sms.user.register', + 'email.user.login', + 'sms.user.login', + 'email.user.forgot_password', + 'sms.user.forgot_password', + 'sms.user.update_account', + 'sms.user.new_account', + 'email.user.update_account', + 'email.user.new_account', + 'sms.user.verify_id_card' + ], + ], + // 默认设置 + 'default_settings' => [ + 'user_avatar' => env('ALIYUN_OSS_DOMAIN') . '/geo/sys_icon/default_user_avatar.svg', + 'article_cover' => env('ALIYUN_OSS_DOMAIN') . '/geo/sys_icon/default_article_cover.png', + 'project_logo' => env('ALIYUN_OSS_DOMAIN') . '/geo/sys_icon/default_project_logo_v2.svg', + ], + // 第三方api访问设置 + 'third_api_auth' => [ + 'app_id' => 'aa65700299848d6f21b969dbc9f6cf7c', + 'app_secret' => '5588071d36f0bc61af849c311a03f2c4' + ], + // 信源网站类型设置 + 'ref_site_types' => [ + 1 => [ + 'name' => '权威机构', + 'icon' => env('ALIYUN_OSS_DOMAIN'). '/geo/sys_icon/site_type_news.svg' + ], + 2 => [ + 'name' => '官方来源', + 'icon' => env('ALIYUN_OSS_DOMAIN'). '/geo/sys_icon/site_type_news.svg' + ], + 3 => [ + 'name' => '企业官方', + 'icon' => env('ALIYUN_OSS_DOMAIN'). '/geo/sys_icon/site_type_news.svg' + ], + 4 => [ + 'name' => '社交媒体', + 'icon' => env('ALIYUN_OSS_DOMAIN'). '/geo/sys_icon/site_type_news.svg' + ], + 5 => [ + 'name' => '其他', + 'icon' => env('ALIYUN_OSS_DOMAIN'). '/geo/sys_icon/site_type_other.svg' + ] + ], + // 信源网站媒体类型设置 + 'ref_site_media_types' => [ + 2 => [ + 'name' => '新闻资讯', + 'icon' => env('ALIYUN_OSS_DOMAIN'). '/geo/sys_icon/site_type_other.svg' + ], + 1 => [ + 'name' => '新媒体', + 'icon' => env('ALIYUN_OSS_DOMAIN'). '/geo/sys_icon/site_type_news.svg' + ], + ], + // 演示数据 + 'demo' => [ + 'project_id' => '0197d4ef4d25721187f690fd9215b443' + ] +]; diff --git a/plugin/piadmin/config/static.php b/plugin/piadmin/config/static.php new file mode 100644 index 0000000..1d5331f --- /dev/null +++ b/plugin/piadmin/config/static.php @@ -0,0 +1,6 @@ + true, + 'middleware' => [], // Static file Middleware +]; diff --git a/plugin/piadmin/config/translation.php b/plugin/piadmin/config/translation.php new file mode 100644 index 0000000..db60dbf --- /dev/null +++ b/plugin/piadmin/config/translation.php @@ -0,0 +1,10 @@ + 'zh_CN', + // Fallback language + 'fallback_locale' => ['zh_CN', 'en'], + // Folder where language files are stored + 'path' => base_path() . "/plugin/piadmin/resource/translations", +]; diff --git a/plugin/piadmin/config/view.php b/plugin/piadmin/config/view.php new file mode 100644 index 0000000..8acb91e --- /dev/null +++ b/plugin/piadmin/config/view.php @@ -0,0 +1,10 @@ + Raw::class +]; diff --git a/plugin/piadmin/exception/ExceptionHandler.php b/plugin/piadmin/exception/ExceptionHandler.php new file mode 100644 index 0000000..860d96a --- /dev/null +++ b/plugin/piadmin/exception/ExceptionHandler.php @@ -0,0 +1,59 @@ +_debug = config('plugin.piadmin.app.debug'); + } + + //日志记录 + public function report(Throwable $exception) + { + //TODO 记录日志 + Log::error("错误信息: " . $exception->getMessage()); + Log::error("文件: " . $exception->getFile() . "第" . $exception->getLine() . "行"); + Log::error("错误堆栈: \n" . $exception->getTraceAsString()); + } + + public function render(Request $request, Throwable $exception): Response + { + $code = $exception->getCode(); + $errorMessage = $exception->getMessage(); + if (is_numeric($errorMessage)) { + $errorMessage = trans($errorMessage); + } + $json = ['code' => $code ? $code : 500, 'msg' => $errorMessage,'data'=>[]]; + if($this->_debug){ + $json['request_url'] = $request->method() . ' ' . $request->uri(); + $json['timestamp'] = date('Y-m-d H:i:s'); + $json['client_ip'] = $request->getRealIp(); + $json['request_param'] = $request->all(); + $json['exception_handle'] = get_class($exception); + $json['exception_info'] = [ + 'code' => $exception->getCode(), + 'msg' => $exception->getMessage(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => explode("\n", $exception->getTraceAsString()) + ]; + } + + return new Response(200, ['Content-Type' => 'application/json'], + \json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + } + +} \ No newline at end of file diff --git a/plugin/piadmin/install.sql b/plugin/piadmin/install.sql new file mode 100644 index 0000000..423b558 --- /dev/null +++ b/plugin/piadmin/install.sql @@ -0,0 +1,230 @@ +/* + Navicat Premium Dump SQL + + Source Server : piadmin + Source Server Type : MySQL + Source Server Version : 50737 (5.7.37-log) + Source Host : 47.100.187.221:3306 + Source Schema : pi_admin_db + + Target Server Type : MySQL + Target Server Version : 50737 (5.7.37-log) + File Encoding : 65001 + + Date: 20/10/2025 16:58:57 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for pi_attachment +-- ---------------------------- +DROP TABLE IF EXISTS `pi_attachment`; +CREATE TABLE `pi_attachment` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `storage` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '1' COMMENT '附件引擎 1本地 2七牛 3腾讯 4阿里', + `type` int(11) NOT NULL DEFAULT 0 COMMENT '0 所有 1 压缩包 2 图片 3 视频 4 文件 5 音频', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '文件名', + `path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '文件路径', + `suffix` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '文件后缀', + `original_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '文件原名', + `size` float NOT NULL DEFAULT 0 COMMENT '文件大小', + `creator_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '创建者ID', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + `deleted_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '附件表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of pi_attachment +-- ---------------------------- + +-- ---------------------------- +-- Table structure for pi_system_admin +-- ---------------------------- +DROP TABLE IF EXISTS `pi_system_admin`; +CREATE TABLE `pi_system_admin` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '系统自增id', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '名称', + `account` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '登录账号', + `status` tinyint(2) NOT NULL DEFAULT 1 COMMENT '状态', + `sort` int(11) NOT NULL DEFAULT 1000 COMMENT '排序字段', + `dept_id` bigint(20) NOT NULL COMMENT '归属部门id', + `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '邮箱', + `phone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '手机号', + `gender` tinyint(2) NOT NULL DEFAULT 1 COMMENT '性别', + `avatar_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '头像', + `password` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '密码', + `description` varchar(900) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户自定义的描述信息', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `is_super_admin` tinyint(2) NOT NULL DEFAULT 2 COMMENT '是否为超级管理员', + `last_login_time` int(11) NOT NULL DEFAULT 0 COMMENT '最近登录时间', + `last_login_ip` varchar(90) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '最近登录ip', + `last_login_addr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '最近登录地点', + `create_time` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `update_time` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + `delete_time` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间;为0是未删除数据', + `create_by` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '创建人id', + `update_by` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '更新人id', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unique_id`(`id`) USING BTREE COMMENT '系统自增主键索引' +) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of pi_system_admin +-- ---------------------------- +INSERT INTO `pi_system_admin` VALUES (1, 'supderAdmin', 'supderAdmin@piadmin.com', 1, 1000, 0, 'supderAdmin@piadmin.com', '18888888888', 1, '', '$argon2i$v=19$m=65536,t=4,p=1$OGZHelVxd2cuUTBIMVlWUQ$CjibjWH2IAza6tXcsgQ9699P9QMOYt9d0ANtIOJz/7I', '超级管理员', NULL, 1, 1754013865, '127.0.0.1', '', 1753930543, 1754013865, 0, '', ''); +INSERT INTO `pi_system_admin` VALUES (6, '张三', 'zhangsan@piadmin.com', 1, 1000, 2, 'zhangsan@piadmin.com', '15500000001', 3, '', '$argon2i$v=19$m=65536,t=4,p=1$SFhKSjlrVG43aWx2LkJwRg$YQWIHt0/VmQTixacJT4ya7xyzkM+UvNWPvLQygQ6WyI', '', NULL, 2, 0, '', '', 1756451869, 1756451869, 0, '1', '1'); +INSERT INTO `pi_system_admin` VALUES (7, '测试管理员', 'admin', 1, 1000, 3, 'zhangf@suq.cn', '17606161011', 1, '', '$argon2i$v=19$m=65536,t=4,p=1$SEFWOVZoNjEyL2ViTEd6Yw$R8l6cogbj7IbHKAYwzTbLtwXz9NCN+GLCBEUBAzN3iY', '', NULL, 2, 0, '', '', 1760937653, 1760937653, 0, '', ''); + +-- ---------------------------- +-- Table structure for pi_system_admin_dept +-- ---------------------------- +DROP TABLE IF EXISTS `pi_system_admin_dept`; +CREATE TABLE `pi_system_admin_dept` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `admin_id` bigint(20) NOT NULL COMMENT '后台用户id', + `dept_id` bigint(20) NOT NULL COMMENT '部门id', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '后台用户与部门关联表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of pi_system_admin_dept +-- ---------------------------- +INSERT INTO `pi_system_admin_dept` VALUES (4, 6, 2); + +-- ---------------------------- +-- Table structure for pi_system_admin_role +-- ---------------------------- +DROP TABLE IF EXISTS `pi_system_admin_role`; +CREATE TABLE `pi_system_admin_role` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `admin_id` bigint(20) NOT NULL COMMENT '管理员', + `role_id` bigint(20) NOT NULL COMMENT '角色', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '管理员角色关联表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of pi_system_admin_role +-- ---------------------------- +INSERT INTO `pi_system_admin_role` VALUES (5, 6, 4); +INSERT INTO `pi_system_admin_role` VALUES (6, 1, 1); + +-- ---------------------------- +-- Table structure for pi_system_dept +-- ---------------------------- +DROP TABLE IF EXISTS `pi_system_dept`; +CREATE TABLE `pi_system_dept` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id', + `uuid` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '业务id', + `pid` bigint(32) NOT NULL COMMENT '上级id', + `id_path` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'id路径', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '部门名称', + `sort` int(11) NOT NULL DEFAULT 1000 COMMENT '排序字段', + `leader` bigint(20) NOT NULL DEFAULT 0 COMMENT '负责人id', + `phone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '联系电话', + `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '联系邮箱', + `status` tinyint(2) NOT NULL DEFAULT 1 COMMENT '状态', + `create_time` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `update_time` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + `delete_time` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间;为0是未删除数据', + `create_by` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '创建人id', + `update_by` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '更新人id', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '部门表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of pi_system_dept +-- ---------------------------- +INSERT INTO `pi_system_dept` VALUES (1, '0198f02e03e57037acad7541d1ca4f11', 0, '0,0198f02e03e57037acad7541d1ca4f11', '数字旗帜', 1000, 0, '', '', 1, 1756376204, 1756376204, 0, '1', '1'); +INSERT INTO `pi_system_dept` VALUES (2, '0198f02e2c24708ca22a31bf0fb284db', 1, '0,0198f02e03e57037acad7541d1ca4f11,0198f02e2c24708ca22a31bf0fb284db', '前端组', 1000, 0, '', '', 1, 1756376214, 1756376214, 0, '1', '1'); +INSERT INTO `pi_system_dept` VALUES (3, '0198f02e50db7346bca3dab08500e5a1', 1, '0,0198f02e03e57037acad7541d1ca4f11,0198f02e50db7346bca3dab08500e5a1', '后端组', 1000, 0, '', '', 1, 1756376224, 1756376224, 0, '1', '1'); +INSERT INTO `pi_system_dept` VALUES (4, '0198f02e674572e2a114bd750328afc8', 1, '0,0198f02e03e57037acad7541d1ca4f11,0198f02e674572e2a114bd750328afc8', '信息组', 1000, 0, '', '', 1, 1756376229, 1756376229, 0, '1', '1'); + +-- ---------------------------- +-- Table structure for pi_system_menu +-- ---------------------------- +DROP TABLE IF EXISTS `pi_system_menu`; +CREATE TABLE `pi_system_menu` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '系统自增id', + `uuid` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'uuid', + `pid` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '父id', + `id_path` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '组级集合 由uuid构成', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '菜单名称', + `code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '菜单标识代码', + `icon` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '菜单图标', + `route` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '路由地址', + `component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '组件路径', + `redirect` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '跳转地址', + `is_hidden` smallint(6) UNSIGNED NOT NULL DEFAULT 2 COMMENT '是否隐藏 (1是 2否)', + `is_layout` tinyint(1) UNSIGNED NOT NULL DEFAULT 2 COMMENT '继承layout', + `type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '菜单类型, (M菜单 B按钮 L链接 I iframe)', + `status` smallint(6) NOT NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)', + `sort` smallint(5) UNSIGNED NOT NULL DEFAULT 1000 COMMENT '排序', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注', + `create_by` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '创建人id', + `update_by` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '更新人id', + `create_time` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `update_time` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + `delete_time` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间;为0是未删除数据', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unique_id`(`id`) USING BTREE COMMENT '系统自增主键索引' +) ENGINE = InnoDB AUTO_INCREMENT = 22 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '菜单信息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of pi_system_menu +-- ---------------------------- +INSERT INTO `pi_system_menu` VALUES (15, '0198ee6f82d1731e9d651cb016f227ba', '0', '0,0198ee6f82d1731e9d651cb016f227ba', '控制台', 'dashboard', 'dashboard', 'dashboard', 'dashboard', '', 2, 2, 'M', 1, 1000, '控制台', '1', '1', 1756346942, 1756346942, 0); +INSERT INTO `pi_system_menu` VALUES (16, '0198ee718b4d72bfa9eb79ede4e2f921', '0', '0,0198ee718b4d72bfa9eb79ede4e2f921', '系统管理', 'system', 'system', 'system', 'system', '', 2, 2, 'M', 1, 1100, '系统管理', '1', '1', 1756347075, 1756347075, 0); +INSERT INTO `pi_system_menu` VALUES (17, '0198ee7230e2706b9de0bc2475a8d4b9', '16', '0,0198ee718b4d72bfa9eb79ede4e2f921,0198ee7230e2706b9de0bc2475a8d4b9', '用户管理', 'admin', 'admin', 'admin', 'admin', '', 2, 2, 'M', 1, 1100, '用户管理', '1', '1', 1756347117, 1756347117, 0); +INSERT INTO `pi_system_menu` VALUES (18, '0198ee729341712fa8d7caf14431c3ce', '16', '0,0198ee718b4d72bfa9eb79ede4e2f921,0198ee729341712fa8d7caf14431c3ce', '角色管理', 'role', 'role', 'role', 'role', '', 2, 2, 'M', 1, 1200, '角色管理', '1', '1', 1756347143, 1756347143, 0); +INSERT INTO `pi_system_menu` VALUES (19, '0198ee72fe1a719f8b20733b73d7c6e9', '16', '0,0198ee718b4d72bfa9eb79ede4e2f921,0198ee72fe1a719f8b20733b73d7c6e9', '菜单管理', 'menu', 'menu', 'menu', 'menu', '', 2, 2, 'M', 1, 1300, '菜单管理', '1', '1', 1756347170, 1756347170, 0); +INSERT INTO `pi_system_menu` VALUES (20, '0198ee741a2073fa924eaa010ea64651', '16', '0,0198ee718b4d72bfa9eb79ede4e2f921,0198ee741a2073fa924eaa010ea64651', '部门管理', 'dept', 'dept', 'dept', 'dept', '', 2, 2, 'M', 1, 1400, '部门管理', '1', '1', 1756347243, 1756347243, 0); +INSERT INTO `pi_system_menu` VALUES (21, '0198ee74740d700abc07b71a25e05cf8', '16', '0,0198ee718b4d72bfa9eb79ede4e2f921,0198ee74740d700abc07b71a25e05cf8', '字典管理', 'dict', 'dict', 'dict', 'dict', '', 2, 2, 'M', 1, 1500, '字典管理', '1', '1', 1756347266, 1756347266, 0); + +-- ---------------------------- +-- Table structure for pi_system_role +-- ---------------------------- +DROP TABLE IF EXISTS `pi_system_role`; +CREATE TABLE `pi_system_role` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '角色名称', + `code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色标识符', + `sort` int(11) NOT NULL DEFAULT 1000 COMMENT '排序字段', + `is_system` tinyint(2) NOT NULL DEFAULT 2 COMMENT '是否为系统角色', + `is_super_admin` tinyint(2) NOT NULL DEFAULT 2 COMMENT '是否为超级管理员', + `status` tinyint(2) NOT NULL DEFAULT 1 COMMENT '状态', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '备注', + `create_by` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `update_by` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `create_time` int(10) UNSIGNED NOT NULL DEFAULT 0, + `update_time` int(10) UNSIGNED NOT NULL DEFAULT 0, + `delete_time` int(10) UNSIGNED NOT NULL DEFAULT 0, + UNIQUE INDEX `uk_id`(`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of pi_system_role +-- ---------------------------- +INSERT INTO `pi_system_role` VALUES (1, '超级管理员', 'supder_admin', 1000, 1, 1, 1, '超级管理员', '', '', 1753926896, 1753926896, 0); +INSERT INTO `pi_system_role` VALUES (4, '普通角色', 'normal', 1000, 2, 2, 1, '这是一个普通角色', '1', '1', 1756352452, 1756352452, 0); + +-- ---------------------------- +-- Table structure for pi_system_role_menu +-- ---------------------------- +DROP TABLE IF EXISTS `pi_system_role_menu`; +CREATE TABLE `pi_system_role_menu` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `role_id` bigint(20) NOT NULL COMMENT '角色id', + `menu_id` bigint(20) NOT NULL COMMENT '菜单id', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色菜单关联表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of pi_system_role_menu +-- ---------------------------- +INSERT INTO `pi_system_role_menu` VALUES (4, 4, 15); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/plugin/piadmin/resource/translations/en/messages.php b/plugin/piadmin/resource/translations/en/messages.php new file mode 100644 index 0000000..454b0f2 --- /dev/null +++ b/plugin/piadmin/resource/translations/en/messages.php @@ -0,0 +1,6 @@ +'parameter error', + 403=>'Your login credentials are incorrect or have expired. Please log in again' +]; \ No newline at end of file diff --git a/plugin/piadmin/resource/translations/zh_CN/messages.php b/plugin/piadmin/resource/translations/zh_CN/messages.php new file mode 100644 index 0000000..20a3b46 --- /dev/null +++ b/plugin/piadmin/resource/translations/zh_CN/messages.php @@ -0,0 +1,64 @@ +'参数错误', + 501=>'操作失败', + 502=>'数据不存在', + 200=>'操作成功', + 403=>'您的登录凭证错误或者已过期,请重新登录', + 400001=>'用户名不能为空', + 400002=>'密码不能为空', + 400003=>'验证码不能为空', + 400004=>'验证码错误', + 400005=>'用户名或密码错误', + 400006=>'用户状态异常', + 401001=>'上级菜单不存在', + 401002=>'上级不能选择自己', + 401003=>'菜单标识已存在', + 401004=>'上级不能选择当前菜单的子级', + 401005=>'菜单不存在', + 4020001 => '角色名称不能为空', + 4020002 => '角色名称不能超过100个字符', + 4020003 => '角色编码不能为空', + 4020004 => '角色编码不能超过100个字符', + 4020005 => '状态不能为空', + 4020006 => '备注不能超过500个字符', + 4020007 => '菜单id不能为空', + 4020008 => '菜单格式错误', + 4020009 => '角色编码已存在', + 4020010 => '角色已存在', + 4020011 => '角色不存在', + 4020012 => '所选菜单不存在', + 4020013 => '排序字段只能是数字', + 4020014 => '角色id不能为空', + 4030001 => '部门名称不能为空', + 4030002 => '部门名称不能超过100个字符', + 4030003 => '排序字段只能是数字', + 4030004 => '负责人id只能是数字', + 4030005 => '手机号码格式错误', + 4030006 => '邮箱格式错误', + 4030007 => '父级部门不存在', + 4030008 => '部门id不能为空', + 4030009 => '部门不存在', + 4030010 => '父级部门id不能为空', + 4030011 => '父级部门不能选择自身', + 4040001 => '昵称不能为空', + 4040002 => '昵称不能超过100个字符', + 4040003 => '账号不能为空', + 4040004 => '账号不能超过100个字符', + 4040005 => '部门不能为空', + 4040006 => '密码不能为空', + 4040007 => '密码不能少于8个字符', + 4040008 => '密码不能超过32个字符', + 4040009 => '手机号码格式不正确', + 4040010 => '邮箱格式不正确', + 4040011 => '角色不能为空', + 4040012 => '角色必须是数组', + 4040013 => '用户帐号已存在', + 4040014 => '用户必须有一个角色', + 4040015 => '用户id不能为空', + 4040016 => '用户不存在', + 4040017 => '禁止删除超级管理员', + 4040018 => '禁止操作超级管理员', + +]; \ No newline at end of file diff --git a/plugin/piadmin/update.php b/plugin/piadmin/update.php new file mode 100644 index 0000000..cd9ada3 --- /dev/null +++ b/plugin/piadmin/update.php @@ -0,0 +1,4 @@ +R&9&1g0})ukRu+b zsI4iH+Tum=wxm?+g|1x7X%O&eYs0dFEXuJv?|~SRU%$V%yN_jIWzi)cX5Q?Z`F`g& z^PBfT2tWLcA20Z~I66XzQ9_8f0h+{zfZMeXzI$!B14qNqe+Ms>0t^d|k|#hpcn^q~ z;CVg3dWG2j%ifLf3af($0x zFqHZ@&|>{}K^tIw<{IEO5%LUi;PHV0JOSi}L_G^kF%-24L#d6mnNZLTT>4y=fJs32 z>T3n#v;Jb>u1$s#{kl4$sCDYsB{mVVOo|!b%Y>Or`)Ip-WMT~hhU&qo|1rvkx`)Dc z3eN*q@6-It4HR~%KIfv@I@xr$`J)wSwu_-_>;xY!T~{fgJ^dN=dHt(3eaw6QQ5(&Q zwbSe!_bDQ$p3rC#TC=DQ1t2=B$w95sS3oCG8?O+V{O^KwP2A z#JKe59cj_##AAmi*eL7j1FC3lq3dmSsf7^;4_WBvlMm=;#>pmNa_ zbN6mz6D^FlYx?YA-dafqFMD`^pBqx8l17ELWJ#L6Tv|SjloLN+GW0x%FxdnZY!{J* zO>bu3zE@n@Xrm(g1kiF6>)}|NZnR~9*Ma-cgVuwiY@)K+DvIix zgRsv#uD3~jt+NXh3XhXo&T-yFZlPoKYP(QS z*TQ4y`W)ZW)fHq&oMLL^ISVq=dQNX$&xlL$K1x# z*7(XDl{RE3ywB*nWQl#}H3ly4Cosk+Zyun@=e8?%w36eyYL}hLt={+9v0{aOIj?AY z&fVhxb&f+O2n7A&0BwGLmwI}bwM^2!bF$w)ca6_E*FOh3kc$sBYV94a`E&Ugq}JFv zUI2YO@bePdQ$U|M@LuPAo?E48`R&s6T0+lcHq#kXA^lZmqVmQj`0L|ALEl9{RJ+9? zBwEvF5AT3{ps$4w`&^&nANhHd5c>@!tuxTZHQ}@=WhEJJ-SqVEFZ8Fd2lo#&S)Z8* z_5ps5X+Ag(Ntt}s_vQln>$*J0l6s4#{qxQkI&ok-*hafkmQvcTMCbFO_kQ%<4_@^2 zzeWJ+2eNu`#%p8DK354;Lol7p6fud@%wulH<1{44HRY0Ks{v&TtXTdEpTWA|O34LUvWxqcXMUTb6W9?1XuHtqZ&nG)l^rKAmuHEs4) zB@AIt%@K#v30S=-d0QdDYxZT9iy zD*2z!VqR{nZf6e)`*;iRdk3Gh&V8v;dak0+*M06>|6-+l@VPb}Ttsel z@|n4OhceLL37RLIYdu32dP)cEJ5k+^SlHD-nud)91PZ{o+ zzq%#)Txl0+$CLm(L*M6p4y`kYcOBka9)liSH{d#+x;9dMR}dLv$B}VbAQ?vokWn}= z3x8s4pi9FKr63Hx5d?v8ATSXag$OVU;co-o_?H7Z`k|xHI-r3NX+lrJ=!7o`Ul1Dq E2mbpSrT_o{ literal 0 HcmV?d00001 diff --git a/publicupload/20250901/dffb25364449abaabb13f44535480044c8efd268.png b/publicupload/20250901/dffb25364449abaabb13f44535480044c8efd268.png new file mode 100644 index 0000000000000000000000000000000000000000..9b0c21a2d06facf6e4407e904ff0bcc743553128 GIT binary patch literal 42646 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 xG5`${HXaCR$JqF%jd4pt + * @copyright walkor + * @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 +{ + +} \ No newline at end of file diff --git a/support/Response.php b/support/Response.php new file mode 100644 index 0000000..9bc4e1e --- /dev/null +++ b/support/Response.php @@ -0,0 +1,24 @@ + + * @copyright walkor + * @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 +{ + +} \ No newline at end of file diff --git a/support/bootstrap.php b/support/bootstrap.php new file mode 100644 index 0000000..d913def --- /dev/null +++ b/support/bootstrap.php @@ -0,0 +1,139 @@ + + * @copyright walkor + * @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); + diff --git a/webman b/webman new file mode 100644 index 0000000..3f55062 --- /dev/null +++ b/webman @@ -0,0 +1,71 @@ +#!/usr/bin/env php +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(); diff --git a/windows.bat b/windows.bat new file mode 100644 index 0000000..f07ce53 --- /dev/null +++ b/windows.bat @@ -0,0 +1,3 @@ +CHCP 65001 +php windows.php +pause \ No newline at end of file diff --git a/windows.php b/windows.php new file mode 100644 index 0000000..f37a72c --- /dev/null +++ b/windows.php @@ -0,0 +1,136 @@ +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 = << 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); + } +}