Zory 3 週間 前
コミット
372abbc457
100 ファイル変更5579 行追加2 行削除
  1. 9 0
      .env
  2. 20 0
      .gitignore
  3. 8 0
      .idea/.gitignore
  4. 147 0
      .idea/cpa-api.iml
  5. 8 0
      .idea/modules.xml
  6. 161 0
      .idea/php.xml
  7. 6 0
      .idea/vcs.xml
  8. 18 0
      Dockerfile
  9. 21 0
      LICENSE
  10. 25 2
      README.md
  11. 133 0
      app/command/Model.php
  12. 27 0
      app/controller/IndexController.php
  13. 138 0
      app/controller/common/Login.php
  14. 76 0
      app/controller/common/Menu.php
  15. 208 0
      app/extra/basic/Base.php
  16. 29 0
      app/extra/basic/Model.php
  17. 136 0
      app/extra/basic/Service.php
  18. 25 0
      app/extra/ip2region/IpSearch.php
  19. BIN
      app/extra/ip2region/ip2region_v4.xdb
  20. 57 0
      app/extra/ip2region/xdb/IPv4.php
  21. 43 0
      app/extra/ip2region/xdb/IPv6.php
  22. 285 0
      app/extra/ip2region/xdb/SearchIp.php
  23. 196 0
      app/extra/ip2region/xdb/Searcher.php
  24. 137 0
      app/extra/tools/CodeExtend.php
  25. 72 0
      app/extra/tools/DataExtend.php
  26. 80 0
      app/extra/tools/OssRegionExtend.php
  27. 219 0
      app/extra/tools/SceneData.php
  28. 123 0
      app/extra/tools/UploadExtend.php
  29. 830 0
      app/functions.php
  30. 41 0
      app/middleware/AuthMiddleware.php
  31. 42 0
      app/middleware/StaticFile.php
  32. 47 0
      app/model/system/SystemConfig.php
  33. 47 0
      app/model/system/SystemGroup.php
  34. 52 0
      app/model/system/SystemMenu.php
  35. 63 0
      app/model/system/SystemUser.php
  36. 10 0
      app/process/Http.php
  37. 305 0
      app/process/Monitor.php
  38. 30 0
      app/service/system/MenuService.php
  39. 182 0
      app/service/system/SystemService.php
  40. 98 0
      app/service/system/UploadService.php
  41. 37 0
      app/service/system/UserService.php
  42. 14 0
      app/view/index/view.html
  43. 12 0
      app/view/protocol.html
  44. 0 0
      city.json
  45. 90 0
      composer.json
  46. 26 0
      config/app.php
  47. 21 0
      config/autoload.php
  48. 18 0
      config/bootstrap.php
  49. 31 0
      config/cache.php
  50. 6 0
      config/container.php
  51. 29 0
      config/database.php
  52. 15 0
      config/dependence.php
  53. 5 0
      config/event.php
  54. 17 0
      config/exception.php
  55. 32 0
      config/log.php
  56. 15 0
      config/middleware.php
  57. 32 0
      config/plugin/hhink/webman-sms/app.php
  58. 8 0
      config/plugin/hzdad/codecheck/app.php
  59. 26 0
      config/plugin/isszz/webman-hashids/app.php
  60. 48 0
      config/plugin/linfly/annotation/annotation.php
  61. 4 0
      config/plugin/linfly/annotation/app.php
  62. 19 0
      config/plugin/linfly/annotation/bootstrap.php
  63. 19 0
      config/plugin/linfly/annotation/middleware.php
  64. 17 0
      config/plugin/linfly/annotation/route.php
  65. 5 0
      config/plugin/luckycmc/webman-province-city-area/app.php
  66. 9 0
      config/plugin/luckycmc/webman-province-city-area/command.php
  67. 77 0
      config/plugin/shopwwi/auth/app.php
  68. 11 0
      config/plugin/tinywan/captcha/app.php
  69. 76 0
      config/plugin/tinywan/storage/app.php
  70. 28 0
      config/plugin/webman/console/app.php
  71. 4 0
      config/plugin/webman/event/app.php
  72. 17 0
      config/plugin/webman/event/bootstrap.php
  73. 7 0
      config/plugin/webman/event/command.php
  74. 10 0
      config/plugin/webman/push/app.php
  75. 21 0
      config/plugin/webman/push/process.php
  76. 87 0
      config/plugin/webman/push/route.php
  77. 4 0
      config/plugin/webman/redis-queue/app.php
  78. 7 0
      config/plugin/webman/redis-queue/command.php
  79. 32 0
      config/plugin/webman/redis-queue/log.php
  80. 19 0
      config/plugin/webman/redis-queue/process.php
  81. 21 0
      config/plugin/webman/redis-queue/redis.php
  82. 8 0
      config/plugin/webman/validation/app.php
  83. 7 0
      config/plugin/webman/validation/command.php
  84. 9 0
      config/plugin/webman/validation/middleware.php
  85. 5 0
      config/plugin/x2nx/webman-migrate/app.php
  86. 44 0
      config/plugin/x2nx/webman-migrate/command.php
  87. 24 0
      config/plugin/x2nx/webman-migrate/phinx.php
  88. 50 0
      config/process.php
  89. 29 0
      config/redis.php
  90. 21 0
      config/route.php
  91. 23 0
      config/server.php
  92. 65 0
      config/session.php
  93. 23 0
      config/static.php
  94. 38 0
      config/think-cache.php
  95. 42 0
      config/think-orm.php
  96. 25 0
      config/translation.php
  97. 22 0
      config/view.php
  98. 11 0
      docker-compose.yml
  99. BIN
      public/favicon.ico
  100. 3 0
      resource/translations/en/messages.php

+ 9 - 0
.env

@@ -0,0 +1,9 @@
+APP_KEY = '37dbd69c19c7addf439941b41e379563'
+APP_DEBUG = false
+
+DB_HOST = rm-7xvisi884oja3z51eio.mysql.cn-guangzhou.rds.aliyuncs.com
+DB_PORT = 3306
+DB_NAME = bluev_cpa
+DB_USER = cpa_user
+DB_PASSWORD = Lanwei@2026
+DB_DEFAULT = mysql

+ 20 - 0
.gitignore

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

+ 8 - 0
.idea/.gitignore

@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 147 - 0
.idea/cpa-api.iml

@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="WEB_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/." isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/./app" isTestSource="false" packagePrefix="app\" />
+      <sourceFolder url="file://$MODULE_DIR$/./app/view/components" isTestSource="false" packagePrefix="app\View\Components\" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/aliyuncs/oss-sdk-php" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/brick/math" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/cakephp/chronos" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/cakephp/core" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/cakephp/database" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/cakephp/datasource" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/cakephp/utility" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/carbonphp/carbon-doctrine-types" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/inflector" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/lexer" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/egulias/email-validator" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/fakerphp/faker" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/firebase/php-jwt" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/fruitcake/php-cors" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/graham-campbell/result-type" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/command" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/guzzle" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/guzzle-services" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/promises" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/psr7" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/uri-template" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/hashids/hashids" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/hhink/webman-sms" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/hzdad/codecheck" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/bus" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/collections" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/conditionable" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/console" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/container" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/contracts" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/database" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/events" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/filesystem" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/http" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/macroable" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/pagination" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/pipeline" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/redis" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/reflection" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/session" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/support" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/translation" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/validation" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/illuminate/view" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/isszz/webman-hashids" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/kkokk/poster" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/laravel/prompts" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/laravel/serializable-closure" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/league/container" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/linfly/annotation" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/luckycmc/webman-province-city-area" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/maennchen/zipstream-php" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/markbaker/complex" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/markbaker/matrix" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/monolog/monolog" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/myclabs/php-enum" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/nesbot/carbon" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/nikic/fast-route" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/nunomaduro/termwind" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/overtrue/easy-sms" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/php-di/invoker" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/php-di/php-di" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/phpoffice/phpspreadsheet" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/phpoption/phpoption" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/psr/cache" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/psr/clock" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/psr/container" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/psr/event-dispatcher" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-factory" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-message" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/psr/log" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/psr/simple-cache" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/qcloud/cos-sdk-v5" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/qiniu/php-sdk" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/ralouphie/getallheaders" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/robmorgan/phinx" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/shopwwi/webman-auth" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/cache" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/cache-contracts" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/clock" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/config" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/console" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/deprecation-contracts" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/error-handler" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher-contracts" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/filesystem" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/finder" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-foundation" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-kernel" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mime" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-ctype" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-grapheme" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-idn" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-normalizer" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-mbstring" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php80" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php83" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php84" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php85" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/process" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/service-contracts" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/string" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation-contracts" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-dumper" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-exporter" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/tinywan/captcha" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/tinywan/storage" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/topthink/think-container" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/topthink/think-helper" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/topthink/think-orm" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/topthink/think-validate" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/vlucas/phpdotenv" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/voku/portable-ascii" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/webman/cache" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/webman/console" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/webman/database" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/webman/event" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/webman/push" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/webman/redis" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/webman/redis-queue" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/webman/think-cache" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/webman/think-orm" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/webman/validation" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/workerman/coroutine" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/workerman/crontab" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/workerman/redis" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/workerman/redis-queue" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/workerman/webman-framework" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/workerman/workerman" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/x2nx/webman-migrate" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/yzh52521/easyhttp" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/cpa-api.iml" filepath="$PROJECT_DIR$/.idea/cpa-api.iml" />
+    </modules>
+  </component>
+</project>

+ 161 - 0
.idea/php.xml

@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="MessDetectorOptionsConfiguration">
+    <option name="transferred" value="true" />
+  </component>
+  <component name="PHPCSFixerOptionsConfiguration">
+    <option name="transferred" value="true" />
+  </component>
+  <component name="PHPCodeSnifferOptionsConfiguration">
+    <option name="highlightLevel" value="WARNING" />
+    <option name="transferred" value="true" />
+  </component>
+  <component name="PhpIncludePathManager">
+    <include_path>
+      <path value="$PROJECT_DIR$/vendor/monolog/monolog" />
+      <path value="$PROJECT_DIR$/vendor/nikic/fast-route" />
+      <path value="$PROJECT_DIR$/vendor/graham-campbell/result-type" />
+      <path value="$PROJECT_DIR$/vendor/psr/simple-cache" />
+      <path value="$PROJECT_DIR$/vendor/psr/clock" />
+      <path value="$PROJECT_DIR$/vendor/psr/http-factory" />
+      <path value="$PROJECT_DIR$/vendor/psr/container" />
+      <path value="$PROJECT_DIR$/vendor/psr/http-client" />
+      <path value="$PROJECT_DIR$/vendor/guzzlehttp/guzzle-services" />
+      <path value="$PROJECT_DIR$/vendor/psr/http-message" />
+      <path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
+      <path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" />
+      <path value="$PROJECT_DIR$/vendor/psr/cache" />
+      <path value="$PROJECT_DIR$/vendor/guzzlehttp/command" />
+      <path value="$PROJECT_DIR$/vendor/psr/log" />
+      <path value="$PROJECT_DIR$/vendor/webman/database" />
+      <path value="$PROJECT_DIR$/vendor/guzzlehttp/promises" />
+      <path value="$PROJECT_DIR$/vendor/guzzlehttp/guzzle" />
+      <path value="$PROJECT_DIR$/vendor/webman/think-cache" />
+      <path value="$PROJECT_DIR$/vendor/webman/console" />
+      <path value="$PROJECT_DIR$/vendor/guzzlehttp/uri-template" />
+      <path value="$PROJECT_DIR$/vendor/webman/redis" />
+      <path value="$PROJECT_DIR$/vendor/webman/push" />
+      <path value="$PROJECT_DIR$/vendor/webman/event" />
+      <path value="$PROJECT_DIR$/vendor/webman/cache" />
+      <path value="$PROJECT_DIR$/vendor/php-di/php-di" />
+      <path value="$PROJECT_DIR$/vendor/webman/think-orm" />
+      <path value="$PROJECT_DIR$/vendor/webman/validation" />
+      <path value="$PROJECT_DIR$/vendor/php-di/invoker" />
+      <path value="$PROJECT_DIR$/vendor/webman/redis-queue" />
+      <path value="$PROJECT_DIR$/vendor/egulias/email-validator" />
+      <path value="$PROJECT_DIR$/vendor/phpoffice/phpspreadsheet" />
+      <path value="$PROJECT_DIR$/vendor/x2nx/webman-migrate" />
+      <path value="$PROJECT_DIR$/vendor/qiniu/php-sdk" />
+      <path value="$PROJECT_DIR$/vendor/cakephp/database" />
+      <path value="$PROJECT_DIR$/vendor/cakephp/core" />
+      <path value="$PROJECT_DIR$/vendor/cakephp/utility" />
+      <path value="$PROJECT_DIR$/vendor/cakephp/datasource" />
+      <path value="$PROJECT_DIR$/vendor/cakephp/chronos" />
+      <path value="$PROJECT_DIR$/vendor/linfly/annotation" />
+      <path value="$PROJECT_DIR$/vendor/tinywan/captcha" />
+      <path value="$PROJECT_DIR$/vendor/tinywan/storage" />
+      <path value="$PROJECT_DIR$/vendor/firebase/php-jwt" />
+      <path value="$PROJECT_DIR$/vendor/workerman/crontab" />
+      <path value="$PROJECT_DIR$/vendor/workerman/workerman" />
+      <path value="$PROJECT_DIR$/vendor/workerman/coroutine" />
+      <path value="$PROJECT_DIR$/vendor/workerman/webman-framework" />
+      <path value="$PROJECT_DIR$/vendor/workerman/redis" />
+      <path value="$PROJECT_DIR$/vendor/workerman/redis-queue" />
+      <path value="$PROJECT_DIR$/vendor/luckycmc/webman-province-city-area" />
+      <path value="$PROJECT_DIR$/vendor/yzh52521/easyhttp" />
+      <path value="$PROJECT_DIR$/vendor/voku/portable-ascii" />
+      <path value="$PROJECT_DIR$/vendor/nesbot/carbon" />
+      <path value="$PROJECT_DIR$/vendor/brick/math" />
+      <path value="$PROJECT_DIR$/vendor/carbonphp/carbon-doctrine-types" />
+      <path value="$PROJECT_DIR$/vendor/overtrue/easy-sms" />
+      <path value="$PROJECT_DIR$/vendor/fruitcake/php-cors" />
+      <path value="$PROJECT_DIR$/vendor/qcloud/cos-sdk-v5" />
+      <path value="$PROJECT_DIR$/vendor/aliyuncs/oss-sdk-php" />
+      <path value="$PROJECT_DIR$/vendor/hashids/hashids" />
+      <path value="$PROJECT_DIR$/vendor/myclabs/php-enum" />
+      <path value="$PROJECT_DIR$/vendor/hhink/webman-sms" />
+      <path value="$PROJECT_DIR$/vendor/maennchen/zipstream-php" />
+      <path value="$PROJECT_DIR$/vendor/hzdad/codecheck" />
+      <path value="$PROJECT_DIR$/vendor/laravel/serializable-closure" />
+      <path value="$PROJECT_DIR$/vendor/laravel/prompts" />
+      <path value="$PROJECT_DIR$/vendor/shopwwi/webman-auth" />
+      <path value="$PROJECT_DIR$/vendor/isszz/webman-hashids" />
+      <path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
+      <path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
+      <path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
+      <path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
+      <path value="$PROJECT_DIR$/vendor/symfony/polyfill-php84" />
+      <path value="$PROJECT_DIR$/vendor/symfony/finder" />
+      <path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
+      <path value="$PROJECT_DIR$/vendor/symfony/polyfill-php83" />
+      <path value="$PROJECT_DIR$/vendor/symfony/cache" />
+      <path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
+      <path value="$PROJECT_DIR$/vendor/symfony/polyfill-php80" />
+      <path value="$PROJECT_DIR$/vendor/kkokk/poster" />
+      <path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
+      <path value="$PROJECT_DIR$/vendor/symfony/mime" />
+      <path value="$PROJECT_DIR$/vendor/symfony/config" />
+      <path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
+      <path value="$PROJECT_DIR$/vendor/symfony/polyfill-ctype" />
+      <path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
+      <path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
+      <path value="$PROJECT_DIR$/vendor/symfony/console" />
+      <path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-idn" />
+      <path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
+      <path value="$PROJECT_DIR$/vendor/symfony/polyfill-php85" />
+      <path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
+      <path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
+      <path value="$PROJECT_DIR$/vendor/phpoption/phpoption" />
+      <path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
+      <path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
+      <path value="$PROJECT_DIR$/vendor/symfony/string" />
+      <path value="$PROJECT_DIR$/vendor/topthink/think-validate" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/bus" />
+      <path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
+      <path value="$PROJECT_DIR$/vendor/symfony/clock" />
+      <path value="$PROJECT_DIR$/vendor/topthink/think-container" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/console" />
+      <path value="$PROJECT_DIR$/vendor/symfony/translation" />
+      <path value="$PROJECT_DIR$/vendor/topthink/think-helper" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/database" />
+      <path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
+      <path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/macroable" />
+      <path value="$PROJECT_DIR$/vendor/symfony/process" />
+      <path value="$PROJECT_DIR$/vendor/topthink/think-orm" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/pagination" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/redis" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/support" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/conditionable" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/validation" />
+      <path value="$PROJECT_DIR$/vendor/robmorgan/phinx" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/collections" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/reflection" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/container" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/contracts" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/http" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/view" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/filesystem" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/session" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/events" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/translation" />
+      <path value="$PROJECT_DIR$/vendor/illuminate/pipeline" />
+      <path value="$PROJECT_DIR$/vendor/nunomaduro/termwind" />
+      <path value="$PROJECT_DIR$/vendor/composer" />
+      <path value="$PROJECT_DIR$/vendor/vlucas/phpdotenv" />
+      <path value="$PROJECT_DIR$/vendor/fakerphp/faker" />
+      <path value="$PROJECT_DIR$/vendor/markbaker/matrix" />
+      <path value="$PROJECT_DIR$/vendor/markbaker/complex" />
+      <path value="$PROJECT_DIR$/vendor/league/container" />
+    </include_path>
+  </component>
+  <component name="PhpProjectSharedConfiguration" php_language_level="8.2">
+    <option name="suggestChangeDefaultLanguageLevel" value="false" />
+  </component>
+  <component name="PhpStanOptionsConfiguration">
+    <option name="transferred" value="true" />
+  </component>
+  <component name="PsalmOptionsConfiguration">
+    <option name="transferred" value="true" />
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>

+ 18 - 0
Dockerfile

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

+ 21 - 0
LICENSE

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

+ 25 - 2
README.md

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

+ 133 - 0
app/command/Model.php

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

+ 27 - 0
app/controller/IndexController.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace app\controller;
+
+use support\Request;
+
+class IndexController
+{
+    public function index(Request $request)
+    {
+        return view('index');
+    }
+
+    public function protocol()
+    {
+        $name = input("name","privacy");
+        if ($name == "privacy") {
+            $data = sConf('service.privacy');
+        } else {
+            $data = sConf('service.agreements');
+        }
+//        $ipAddress = (new IpSearch)->getIpArea("223.73.60.241");
+//        print_r($ipAddress);
+        return view('protocol',['data' => $data,'name' => $name]);
+    }
+
+}

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

@@ -0,0 +1,138 @@
+<?php
+
+namespace app\controller\common;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\system\SystemConfig;
+use app\model\system\SystemUser;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Attributes\Route\{GetMapping, Controller, Middleware, PostMapping};
+use support\{Request,Response};
+use Shopwwi\WebmanAuth\Auth;
+use think\facade\Db;
+use Tinywan\Captcha\Captcha;
+
+#[Controller("/api/login"),Middleware(AuthMiddleware::class)]
+class Login extends Base
+{
+
+    /**
+     * @var array|string[]
+     */
+    protected array $noNeedLogin = ["getSystemData","setLogin"];
+
+    #[Inject]
+    protected SystemConfig $config;
+
+    /**
+     * 登陆
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping('user')]
+    public function setLogin(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "username.require"  => trans("empty.user"),
+                "password.require"  => trans("empty.passwd"),
+                "code.require"      => trans("empty.code"),
+                "key.require"       => trans("empty.data"),
+            ],"post");
+            if (!is_array($param)) return error($param);
+            if (Captcha::check($param['code'],$param['key']) === false) return errorTrans("error.captcha");
+            $map = ["is_deleted" => 0,"username" => $param['username']];
+            [$state,$msg,$user] = $this->checkLogin($map,2,$param);
+            if (!$state) return error($msg);
+            return successTrans("success.login",get_object_vars((new Auth)->guard("admin")->login($user)));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 登录验证处理
+     * @param array $map
+     * @param int $type
+     * @param array $param
+     * @return array
+     */
+    protected function checkLogin(array $map = [],int $type = 1,array $param = []): array
+    {
+        $user = (new SystemUser)->where($map)->findOrEmpty();
+        if ($user->isEmpty()) return [0,trans("error.user-empty"),[]];
+        if ($user['status'] <> 1) return [0,trans("error.user-status"),[]];
+        if ($user['type'] > 1) {
+            $typeUser = $this->getTypeUser($user['store_id']);
+            if (empty($typeUser)) return [0,trans("empty.agent"),[]];
+            if ($typeUser['status'] <> 1) return [0,trans("error.agent"),[]];
+            if (time() > strtotime($typeUser['vip_end'])) return [0,trans("error.agent-out"),[]];
+            $user['shop_name'] = $typeUser['poi_name'];
+        }
+        if ($type == 2) {
+            if (md5($param['password'].$user['salt']) <> $user['password']) return [0,trans("error.passwd"),[]];
+        }
+        $user->login_at = getDateFull();
+        $user->login_ip = request()->getRealIp();
+        $user->login_num = Db::raw("login_num+1");
+        $user->save();
+        return [1,'success',$user->toArray()];
+    }
+
+
+    /**
+     * 获取代理信息
+     * @param int $agentId
+     * @return array
+     */
+    protected function getTypeUser(int $agentId = 0): array
+    {
+        return (new SaasStore)->where("poi_id",$agentId)->findOrEmpty()->toArray();
+    }
+
+    #[GetMapping('profile')]
+    public function getLoginUser(): Response
+    {
+        try {
+            $userData = (new Auth)->guard("admin")->user()->toArray();
+            if (isset($userData['password'])) unset($userData['password']);
+            $agent = (new SaasStore)->where("poi_id",$userData['store_id'])->findOrEmpty();
+            if (empty($agent['vip_end']))
+            {
+                $userData['vip_end'] = 0;
+            } else {
+                $userData['vip_end'] = strtotime($agent['vip_end']);
+            }
+            return successTrans("success.data",[
+                "username"  => $userData['username'],
+                "userId"    => $userData['id'],
+                "truename"  => $userData['truename'],
+                "vip_at"    => $userData['vip_end'],
+                "agent_id"  => $userData['agent_id'],
+                "store_id"  => $userData['store_id'],
+                "super"     => $userData['is_super'],
+                "shop"      => $agent['poi_name']??'',
+                "trade"     => $agent['trade_type']??0,
+                "type"      => $userData['type'],
+                "logo"      => $agent['poi_logo']??'-'
+            ]);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[GetMapping('data')]
+    public function getSystemData(): Response
+    {
+        try {
+            $captcha = Captcha::base64();
+            $service = $this->config->where("type","service")->column("value","name");
+            return successTrans("success.data",compact("captcha",'service'));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

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

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

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

@@ -0,0 +1,208 @@
+<?php
+
+namespace app\extra\basic;
+
+use app\model\saas\SaasStore;
+use support\Response;
+use think\contract\Arrayable;
+use think\Validate;
+
+class Base
+{
+
+
+
+    const DEFAULT_SUCCESS_MESSAGE = 'success';
+    const DEFAULT_FAIL_MESSAGE = 'fail';
+
+    const DEFAULT_SUCCESS_CODE = 200;
+    const DEFAULT_FAIL_CODE = 400;
+
+
+    public function getShopId()
+    {
+        return request()->header("shop",0);
+    }
+
+
+    /**
+     * 保证金检测
+     */
+    protected function checkDeposit(string $poiId = ""): array
+    {
+        if ($poiId == 0) return [1,'ok'];
+        $deposit = (new SaasStore)->where("poi_id",$poiId)->findOrEmpty();
+        if ($deposit->isEmpty()) return [0,"非法操作"];
+        if ($deposit['deposit'] == 0) return [0,'抖音来客保证金未缴纳,请联系渠道进行保证金缴纳,充值成功后自动开放权限'];
+        return [1,'ok'];
+    }
+
+    protected function getDyConfig(): array
+    {
+        return ["appid" => sConf('wechat.mini_appid'),'secret' => sConf('wechat.mini_secret')];
+    }
+
+    /**
+     * 短信通道
+     * @return array[]
+     */
+    protected function getSmsChannel(): array
+    {
+        return [
+            [
+                "name"  => "阿里云",
+                "type"  => "aliyun",
+                "url"   => "https://dysms.console.aliyun.com/dysms.htm"
+            ],
+            [
+                "name"  => "腾讯云",
+                "type"  => "qcloud",
+                "url"   => "https://console.cloud.tencent.com/smsv2"
+            ],
+            [
+                "name"  => "七牛云",
+                "type"  => "qiniu",
+                "url"   => "https://portal.qiniu.com/sms/dashboard"
+            ]
+        ];
+    }
+
+    /**
+     *  快捷输入并验证( 支持 规则 # 别名 )
+     * @param array $rules 验证规则( 验证信息数组 )
+     * @param array|string $input 输入方式 ( post. 或 get. )
+     * @param callable|null $callable 异常处理操作
+     */
+    protected function _valid(array $rules, array|string $input = '', ?callable $callable = null)
+    {
+        if (is_string($input)) {
+            $type = trim($input, '.') ?: 'get';
+            $input = request()->$type();
+        }
+        [$data, $rule, $info] = [[], [], []];
+        foreach ($rules as $name => $message) if (is_numeric($name)) {
+            [$name, $alias] = explode('#', $message . '#');
+            $data[$name] = $input[($alias ?: $name)] ?? null;
+        } elseif (!str_contains($name, '.')) {
+            $data[$name] = $message;
+        } elseif (preg_match('|^(.*?)\.(.*?)#(.*?)#?$|', $name . '#', $matches)) {
+            [, $_key, $_rule, $alias] = $matches;
+            if (in_array($_rule, ['value', 'default'])) {
+                if ($_rule === 'value') $data[$_key] = $message;
+                elseif ($_rule === 'default') $data[$_key] = $input[($alias ?: $_key)] ?? $message;
+            } else {
+                $info[explode(':', $name)[0]] = $message;
+                $data[$_key] = $data[$_key] ?? ($input[($alias ?: $_key)] ?? null);
+                $rule[$_key] = isset($rule[$_key]) ? ($rule[$_key] . '|' . $_rule) : $_rule;
+            }
+        }
+        $validate = new Validate();
+        if ($validate->rule($rule)->message($info)->check($data)) {
+            return $data;
+        } elseif (is_callable($callable)) {
+            return call_user_func($callable, $validate->getError(), $data);
+        } else {
+            return $validate->getError();
+        }
+    }
+    /**
+     * @param $data
+     * @return array|string|null
+     */
+    private function parseData($data)
+    {
+        if ($data instanceof Arrayable)
+            return $data->toArray();
+        else
+            return $data;
+    }
+
+    /**
+     * @param string $message
+     * @param array|null $data
+     * @return Response
+     */
+    public function success(string $message = self::DEFAULT_SUCCESS_MESSAGE, array $data = null): Response
+    {
+        $message = $this->parseData($message);
+        if (is_array($message)) {
+            $data = $message;
+            $message = self::DEFAULT_SUCCESS_MESSAGE;
+        } else {
+            $data = $this->parseData($data);
+        }
+        return $this->make(self::DEFAULT_SUCCESS_CODE, $message, $data);
+    }
+
+
+    /**
+     * @param int $status
+     * @param string $msg
+     * @param null $data
+     * @return Response
+     */
+    public function make(int $status, string $msg, $data = null)
+    {
+        $content = compact('status', 'msg');
+        if (!is_null($data))
+            $content['data'] = $this->parseData($data);
+        return json($content);
+    }
+
+    public function encode($msg = self::DEFAULT_SUCCESS_MESSAGE, $data = null)
+    {
+        $msg = $this->parseData($msg);
+        if (is_array($msg)) {
+            $data = $msg;
+            $msg = self::DEFAULT_SUCCESS_MESSAGE;
+        } else {
+            $data = $this->parseData($data);
+        }
+        $status = self::DEFAULT_SUCCESS_CODE;
+        $encode = true;
+        $content = compact('status','encode', 'msg');
+        if (is_null($data) || env('APP_DEBUG', false))
+            return $this->success($msg, $data['data'] ?? $data);
+        if (!is_array($data) || !isset($data['data'])) {
+            $res['data'] = $data;
+        } else {
+            $res = $data;
+        }
+        $res['status'] = $status;
+        $res['msg'] = $msg;
+        $content['data'] =  base64_encode(gzdeflate(json_encode($res,JSON_UNESCAPED_UNICODE),9));
+        return json($content);
+    }
+
+
+    /**
+     * 菜单信息格式化
+     * @param array $menus
+     * @param int $type
+     * @param int $mch
+     * @return array
+     */
+    protected function filterMenu(array $menus,int $type,int $mch = 0): array
+    {
+        foreach ($menus as &$menu) {
+            $menu['meta'] = [
+                "title"     => $menu['title'],
+                "icon"      => $menu['icon']??'',
+                "type"      => $menu['type'],
+                "hidden"    => !($menu['hide'] == 1),
+            ];
+            if (!empty($menu['children'])) {
+                $menu['children'] = $this->filterMenu($menu['children'],$type,$mch);
+            }
+            if ($mch > 0) {
+                $menu['component'] = ($mch==1?'merchant/':'store/').$menu['name'];
+            } else {
+                $menu['component'] = $menu['name'];
+            }
+            $menu['isMenu'] = $menu['status'];
+            if($type == 1) unset($menu['title'],$menu['icon'],$menu['type'],$menu['pid'],$menu['id']);
+        }
+        return $menus;
+    }
+
+}

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

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

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

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

+ 25 - 0
app/extra/ip2region/IpSearch.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace app\extra\ip2region;
+
+use app\extra\ip2region\xdb\IPv4;
+use app\extra\ip2region\xdb\Searcher;
+use app\extra\ip2region\xdb\SearchIp;
+
+class IpSearch
+{
+
+    /**
+     * @param string $ip
+     * @return array
+     */
+    public function getIpArea(string $ip = ""): array
+    {
+        $dbFile = __DIR__ . '/ip2region_v4.xdb';
+        $version = IPv4::default();
+        $searcher = Searcher::newWithFileOnly($version,$dbFile);
+        $region = $searcher->search($ip);
+        return explode("|",$region);
+    }
+
+}

BIN
app/extra/ip2region/ip2region_v4.xdb


+ 57 - 0
app/extra/ip2region/xdb/IPv4.php

@@ -0,0 +1,57 @@
+<?php
+
+namespace app\extra\ip2region\xdb;
+
+const IPv4VersionNo    = 4;
+
+class IPv4
+{
+    public $id;
+    public $name;
+    public $bytes;
+    public $segmentIndexSize;
+
+    private static $C = null;
+    public static function default() {
+        if (self::$C == null) {
+            // 14 = 4 + 4 + 2 + 4
+            self::$C = new self(IPv4VersionNo, 'IPv4', 4, 14);
+        }
+        return self::$C;
+    }
+
+    public function __construct($id, $name, $bytes, $segmentIndexSize) {
+        $this->id = $id;
+        $this->name = $name;
+        $this->bytes = $bytes;
+        $this->segmentIndexSize = $segmentIndexSize;
+    }
+
+    // compare the two ip bytes with the current version
+    public function ipSubCompare($ip1, $buff, $offset) {
+        // ip1: Little endian byte order encoded long from searcher.
+        // ip2: Little endian byte order read from xdb index.
+        $len  = strlen($ip1);
+        $eIdx = $offset + $len;
+        for ($i = 0, $j = $eIdx - 1; $i < $len; $i++, $j--) {
+            $i1 = ord($ip1[$i]) & 0xFF;
+            $i2 = ord($buff[$j]) & 0xFF;
+            // printf("i:%d, j:%d, i1:%d, i2:%d\n", $i, $j, $i1, $i2);
+            if ($i1 > $i2) {
+                return 1;
+            } else if ($i1 < $i2) {
+                return -1;
+            }
+        }
+
+        return 0;
+    }
+
+    public function __toString() {
+        return sprintf(
+            "{id:%d, name:%s, bytes:%d, segmentIndexSize:%d}",
+            $this->id, $this->name, $this->bytes, $this->segmentIndexSize
+        );
+    }
+
+}

+ 43 - 0
app/extra/ip2region/xdb/IPv6.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace app\extra\ip2region\xdb;
+
+const IPv6VersionNo    = 6;
+
+class IPv6
+{
+
+    public $id;
+    public $name;
+    public $bytes;
+    public $segmentIndexSize;
+
+    private static $C = null;
+    public static function default() {
+        if (self::$C == null) {
+            // 38 = 16 + 16 + 2 + 4
+            self::$C = new self(IPv6VersionNo, 'IPv6', 16, 38);
+        }
+
+        return self::$C;
+    }
+
+    public function __construct($id, $name, $bytes, $segmentIndexSize) {
+        $this->id = $id;
+        $this->name = $name;
+        $this->bytes = $bytes;
+        $this->segmentIndexSize = $segmentIndexSize;
+    }
+
+    public function ipSubCompare($ip, $buff, $offset) {
+        // return Util::ipCompare($ip, substr($buff, $offset, strlen($ip)));
+        return SearchIp::ipSubCompare($ip, $buff, $offset);
+    }
+
+    public function __toString() {
+        return sprintf(
+            "{id:%d, name:%s, bytes:%d, segmentIndexSize:%d}",
+            $this->id, $this->name, $this->bytes, $this->segmentIndexSize
+        );
+    }
+}

+ 285 - 0
app/extra/ip2region/xdb/SearchIp.php

@@ -0,0 +1,285 @@
+<?php
+
+namespace app\extra\ip2region\xdb;
+
+use \Exception;
+
+// global constants
+const Structure_20     = 2;
+const Structure_30     = 3;
+const HeaderInfoLength = 256;
+const VectorIndexRows  = 256;
+const VectorIndexCols  = 256;
+const VectorIndexSize  = 8;
+
+class SearchIp
+{
+    // parse the specified IP address and return its bytes.
+    // returns: NULL for failed or the packed bytes
+    public static function parseIP($ipString) {
+        $flag = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;
+        if (!filter_var($ipString, FILTER_VALIDATE_IP, $flag)) {
+            return null;
+        }
+
+        return inet_pton($ipString);
+    }
+
+    // IP bytes to string
+    public static function ipToString($ipBytes) {
+        $l = strlen($ipBytes);
+        return ($l == 4 || $l == 16) ? inet_ntop($ipBytes) : '<invalid-ip-bytes>';
+    }
+
+    // compare two ip bytes (packed string return by parsedIP)
+    // returns: -1 if ip1 < ip2, 0 if ip1 == ip2 or 1 if ip1 > ip2
+    public static function ipSubCompare($ip1, $buff, $offset) {
+        // $r = substr_compare($ip1, $buff, $offset, strlen($ip1));
+        // @Note: substr_compare is not working, use the substr + strcmp instead
+        $r = strcmp($ip1, substr($buff, $offset, strlen($ip1)));
+        if ($r < 0) {
+            return -1;
+        } else if ($r > 0) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+
+    // returns: -1 if ip1 < ip2, 0 if ip1 == ip2 or 1 if ip1 > ip2
+    public static function ipCompare($ip1, $ip2) {
+        $r = strcmp($ip1, $ip2);
+        if ($r < 0) {
+            return -1;
+        } else if ($r > 0) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+
+    // version parse
+    public static function versionFromName($ver_name) {
+        $name = strtoupper($ver_name);
+        if ($name == "V4" || $name == "IPv4") {
+            return IPv4::default();
+        } else if ($name == "V6" || $name == "IPv6") {
+            return IPv6::default();
+        } else {
+            throw new Exception("invalid verstion name `{$ver_name}`");
+        }
+    }
+
+    // version parse from header
+    public static function versionFromHeader($header) {
+        // Old structure 2.0 with IPv4 supports ONLY
+        if ($header['version'] == Structure_20) {
+            return IPv4::default();
+        }
+
+        // structure 3.0 after IPv6 supporting
+        if ($header['version'] != Structure_30) {
+            throw new Exception("invalid xdb structure version `{$header['version']}`");
+        }
+
+        if ($header['ipVersion'] == IPv4VersionNo) {
+            return IPv4::default();
+        } else if ($header['ipVersion'] == IPv6VersionNo) {
+            return IPv6::default();
+        } else {
+            throw new Exception("invalid ip version number `{$header['ipVersion']}`");
+        }
+    }
+
+    // binary string chars implode with space
+    public static function bytesToString($buff, $offset, $length) {
+        $sb = [];
+        for ($i = 0; $i < $length; $i++) {
+            $sb[] = ord($buff[$offset+$i]) & 0xFF;
+        }
+        return '['.implode(' ', $sb).']';
+    }
+
+    // decode a 4bytes long with Little endian byte order from a byte buffer
+    public static function le_getUint32($b, $idx) {
+        $val = (ord($b[$idx])) | (ord($b[$idx+1]) << 8)
+            | (ord($b[$idx+2]) << 16) | (ord($b[$idx+3]) << 24);
+
+        // convert signed int to unsigned int if on 32 bit operating system
+        if ($val < 0 && PHP_INT_SIZE == 4) {
+            $val = sprintf("%u", $val);
+        }
+
+        return $val;
+    }
+
+    // read a 2bytes int with litten endian byte order from a byte buffer
+    public static function le_getUint16($b, $idx) {
+        return ((ord($b[$idx])) | (ord($b[$idx+1]) << 8));
+    }
+
+    // Verify if the current Searcher could be used to search the specified xdb file.
+    // Why do we need this check ?
+    // The future features of the xdb impl may cause the current searcher not able to work properly.
+    //
+    // @Note: You Just need to check this ONCE when the service starts
+    // Or use another process (eg, A command) to check once Just to confirm the suitability.
+    // returns: null for everything is ok or the error string.
+    public static function verify($handle) {
+        // load the header
+        $header = self::loadHeader($handle);
+        if ($header == null) {
+            return 'failed to load the header';
+        }
+
+        // get the runtime ptr bytes
+        $runtimePtrBytes = 0;
+        if ($header['version'] == Structure_20) {
+            $runtimePtrBytes = 4;
+        } else if ($header['version'] == Structure_30) {
+            $runtimePtrBytes = $header['runtimePtrBytes'];
+        } else {
+            return "invalid structure version `{$header['version']}`";
+        }
+
+        // 1, confirm the xdb file size
+        // to ensure that the maximum file pointer does not overflow
+        $stat = fstat($handle);
+        if ($stat == false) {
+            return 'failed to stat the xdb file';
+        }
+
+        $maxFilePtr = (1 << ($runtimePtrBytes * 8)) - 1;
+        // print_r([$stat['size'], $maxFilePtr]);
+        if ($stat['size'] > $maxFilePtr) {
+            return "xdb file exceeds the maximum supported bytes: {$maxFilePtr}";
+        }
+
+        return null;
+    }
+
+    public static function verifyFromFile($dbFile) {
+        $handle = fopen($dbFile, 'r');
+        if ($handle === false) {
+            return null;
+        }
+
+        $r = self::verify($handle);
+        fclose($handle);
+        return $r;
+    }
+
+    // load header info from a specified file handle
+    public static function loadHeader($handle) {
+        if (fseek($handle, 0) == -1) {
+            return null;
+        }
+
+        $buff = fread($handle, HeaderInfoLength);
+        if ($buff === false) {
+            return null;
+        }
+
+        // read bytes length checking
+        if (strlen($buff) != HeaderInfoLength) {
+            return null;
+        }
+
+        // return the decoded header info
+        return array(
+            'version'         => self::le_getUint16($buff, 0),
+            'indexPolicy'     => self::le_getUint16($buff, 2),
+            'createdAt'       => self::le_getUint32($buff, 4),
+            'startIndexPtr'   => self::le_getUint32($buff, 8),
+            'endIndexPtr'     => self::le_getUint32($buff, 12),
+            'ipVersion'       => self::le_getUint16($buff, 16),
+            'runtimePtrBytes' => self::le_getUint16($buff, 18)
+        );
+    }
+
+    // load header info from the specified xdb file path
+    public static function loadHeaderFromFile($dbFile) {
+        $handle = fopen($dbFile, 'r');
+        if ($handle === false) {
+            return null;
+        }
+
+        $header = self::loadHeader($handle);
+        fclose($handle);
+        return $header;
+    }
+
+    // load vector index from a file handle
+    public static function loadVectorIndex($handle) {
+        if (fseek($handle, HeaderInfoLength) == -1) {
+            return null;
+        }
+
+        $rLen = VectorIndexRows * VectorIndexCols * VectorIndexSize;
+        $buff = fread($handle, $rLen);
+        if ($buff === false) {
+            return null;
+        }
+
+        if (strlen($buff) != $rLen) {
+            return null;
+        }
+
+        return $buff;
+    }
+
+    // load vector index from a specified xdb file path
+    public static function loadVectorIndexFromFile($dbFile) {
+        $handle = fopen($dbFile, 'r');
+        if ($handle === false) {
+            return null;
+        }
+
+        $vIndex = self::loadVectorIndex($handle);
+        fclose($handle);
+        return $vIndex;
+    }
+
+    // load the xdb content from a file handle
+    public static function loadContent($handle) {
+        if (fseek($handle, 0, SEEK_END) == -1) {
+            return null;
+        }
+
+        $size = ftell($handle);
+        if ($size === false) {
+            return null;
+        }
+
+        // seek to the head for reading
+        if (fseek($handle, 0) == -1) {
+            return null;
+        }
+
+        $buff = fread($handle, $size);
+        if ($buff === false) {
+            return null;
+        }
+
+        // read length checking
+        if (strlen($buff) != $size) {
+            return null;
+        }
+
+        return $buff;
+    }
+
+    // load the xdb content from a file path
+    public static function loadContentFromFile($dbFile) {
+        $str = file_get_contents($dbFile, false);
+        if ($str === false) {
+            return null;
+        } else {
+            return $str;
+        }
+    }
+
+    public static function now() {
+        return (microtime(true) * 1000);
+    }
+}

+ 196 - 0
app/extra/ip2region/xdb/Searcher.php

@@ -0,0 +1,196 @@
+<?php
+
+namespace app\extra\ip2region\xdb;
+
+class Searcher
+{
+    // ip version
+    private $version;
+
+    // xdb file handle
+    private $handle  = null;
+    private $ioCount = 0;
+
+    // vector index in binary string.
+    // string decode will be faster than the map based Array.
+    private $vectorIndex = null;
+
+    // xdb content buffer
+    private $contentBuff = null;
+
+    // ---
+    // static function to create searcher
+
+    /**
+     * @throws Exception
+     */
+    public static function newWithFileOnly($version, $dbFile) {
+        return new self($version, $dbFile, null, null);
+    }
+
+    /**
+     * @throws Exception
+     */
+    public static function newWithVectorIndex($version, $dbFile, $vIndex) {
+        return new self($version, $dbFile, $vIndex, null);
+    }
+
+    /**
+     * @throws Exception
+     */
+    public static function newWithBuffer($version, $cBuff) {
+        return new self($version, null, null, $cBuff);
+    }
+
+    // --- End of static creator
+
+    /**
+     * initialize the xdb searcher
+     * @throws Exception
+     */
+    function __construct($version, $dbFile, $vectorIndex=null, $cBuff=null) {
+        $this->version = $version;
+        // check the content buffer first
+        if ($cBuff != null) {
+            $this->vectorIndex = null;
+            $this->contentBuff = $cBuff;
+        } else {
+            // open the xdb binary file
+            $this->handle = fopen($dbFile, "r");
+            if ($this->handle === false) {
+                throw new \Exception("failed to open xdb file '%s'", $dbFile);
+            }
+
+            $this->vectorIndex = $vectorIndex;
+        }
+    }
+
+    public function close() {
+        if ($this->handle != null) {
+            fclose($this->handle);
+        }
+    }
+
+    public function getIPVersion() {
+        return $this->version;
+    }
+
+    public function getIOCount() {
+        return $this->ioCount;
+    }
+
+    /**
+     * find the region info for the specified ip address.
+     * @Note: the ip address couldO ONLY be a human-readable IP address string,
+     * DO not use the packed binary string returned by #parseIP
+     *
+     * @throws Exception
+     */
+    public function search($ip) {
+        $ipBytes = SearchIp::parseIP($ip);
+        if ($ipBytes == null) {
+            throw new \Exception("invalid ip address `{$ip}`");
+        }
+
+        return $this->searchByBytes($ipBytes);
+    }
+
+    /**
+     * find the region info for the specified binary ip bytes returned by #parseIP.
+     *
+     * @throws Exception
+     */
+    public function searchByBytes($ipBytes) {
+        // ip version check
+        if (strlen($ipBytes) != $this->version->bytes) {
+            throw new \Exception("invalid ip address ({$this->version->name} expected)");
+        }
+
+        // reset the global counter
+        $this->ioCount = 0;
+
+        // locate the segment index block based on the vector index
+        $il0 = ord($ipBytes[0]) & 0xFF;
+        $il1 = ord($ipBytes[1]) & 0xFF;
+        $idx = $il0 * VectorIndexCols * VectorIndexSize + $il1 * VectorIndexSize;
+        if ($this->vectorIndex != null) {
+            $sPtr = SearchIp::le_getUint32($this->vectorIndex, $idx);
+            $ePtr = SearchIp::le_getUint32($this->vectorIndex, $idx + 4);
+        } else if ($this->contentBuff != null) {
+            $sPtr = SearchIp::le_getUint32($this->contentBuff, HeaderInfoLength + $idx);
+            $ePtr = SearchIp::le_getUint32($this->contentBuff, HeaderInfoLength + $idx + 4);
+        } else {
+            // read the vector index block
+            $buff = $this->read(HeaderInfoLength + $idx, 8);
+            $sPtr = SearchIp::le_getUint32($buff, 0);
+            $ePtr = SearchIp::le_getUint32($buff, 4);
+        }
+
+        // printf("sPtr: %d, ePtr: %d\n", $sPtr, $ePtr);
+        // @Note: ptr validate, zero ptr means source data missing
+        // so we could just stop here and return an empty string.
+        if ($sPtr == 0 || $ePtr == 0) {
+            return "";
+        }
+
+        [$bytes, $dBytes] = [strlen($ipBytes), strlen($ipBytes) << 1];
+
+        // binary search the segment index to get the region info
+        $idxSize = $this->version->segmentIndexSize;
+        [$dataLen, $dataPtr, $l, $h] = [0, 0, 0, ($ePtr - $sPtr) / $idxSize];
+        while ($l <= $h) {
+            $m = ($l + $h) >> 1;
+            $p = $sPtr + $m * $idxSize;
+
+            // read the segment index
+            $buff = $this->read($p, $idxSize);
+
+            // compare the segment index
+            if ($this->version->ipSubCompare($ipBytes, $buff, 0) < 0) {
+                $h = $m - 1;
+            } else if ($this->version->ipSubCompare($ipBytes, $buff, $bytes) > 0) {
+                $l = $m + 1;
+            } else {
+                $dataLen = SearchIp::le_getUint16($buff, $dBytes);
+                $dataPtr = SearchIp::le_getUint32($buff, $dBytes + 2);
+                break;
+            }
+        }
+
+        // empty match interception.
+        // printf("dataLen: %d, dataPtr: %d\n", $dataLen, $dataPtr);
+        if ($dataLen == 0) {
+            return "";
+        }
+
+        // load and return the region data
+        return $this->read($dataPtr, $dataLen);
+    }
+
+    // read specified bytes from the specified index
+    private function read($offset, $len) {
+        // check the in-memory buffer first
+        if ($this->contentBuff != null) {
+            return substr($this->contentBuff, $offset, $len);
+        }
+
+        // read from the file
+        $r = fseek($this->handle, $offset);
+        if ($r == -1) {
+            throw new \Exception("failed to fseek to {$offset}");
+        }
+
+        $this->ioCount++;
+        $buff = fread($this->handle, $len);
+        if ($buff === false) {
+            throw new \Exception("failed to fread from {$len}");
+        }
+
+        if (strlen($buff) != $len) {
+            throw new \Exception("incomplete read: read bytes should be {$len}");
+        }
+
+        return $buff;
+    }
+
+}

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

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

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

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

+ 80 - 0
app/extra/tools/OssRegionExtend.php

@@ -0,0 +1,80 @@
+<?php
+
+namespace app\extra\tools;
+
+class OssRegionExtend
+{
+    /**
+     * 获取存储区域
+     * @return array
+     */
+    public static function OssRegion(): array
+    {
+        return [
+            'oss-cn-hangzhou.aliyuncs.com'       => '华东 1(杭州)',
+            'oss-cn-shanghai.aliyuncs.com'       => '华东 2(上海)',
+            'oss-cn-nanjing.aliyuncs.com'        => '华东 5(南京本地地域)',
+            'oss-cn-fuzhou.aliyuncs.com'         => '华东 6(福州本地地域)',
+            'oss-cn-qingdao.aliyuncs.com'        => '华北 1(青岛)',
+            'oss-cn-beijing.aliyuncs.com'        => '华北 2(北京)',
+            'oss-cn-zhangjiakou.aliyuncs.com'    => '华北 3(张家口)',
+            'oss-cn-huhehaote.aliyuncs.com'      => '华北 5(呼和浩特)',
+            'oss-cn-wulanchabu.aliyuncs.com'     => '华北 6(乌兰察布)',
+            'oss-cn-shenzhen.aliyuncs.com'       => '华南 1(深圳)',
+            'oss-cn-heyuan.aliyuncs.com'         => '华南 2(河源)',
+            'oss-cn-guangzhou.aliyuncs.com'      => '华南 3(广州)',
+            'oss-cn-chengdu.aliyuncs.com'        => '西南 1(成都)',
+            'oss-cn-hongkong.aliyuncs.com'       => '中国(香港)',
+            'oss-us-west-1.aliyuncs.com'         => '美国(硅谷)',
+            'oss-us-east-1.aliyuncs.com'         => '美国(弗吉尼亚)',
+            'oss-ap-northeast-1.aliyuncs.com'    => '日本(东京)',
+            'oss-ap-northeast-2.aliyuncs.com'    => '韩国(首尔)',
+            'oss-ap-southeast-1.aliyuncs.com'    => '新加坡',
+            'oss-ap-southeast-2.aliyuncs.com'    => '澳大利亚(悉尼)',
+            'oss-ap-southeast-3.aliyuncs.com'    => '马来西亚(吉隆坡)',
+            'oss-ap-southeast-5.aliyuncs.com'    => '印度尼西亚(雅加达)',
+            'oss-ap-southeast-6.aliyuncs.com'    => '菲律宾(马尼拉)',
+            'oss-ap-southeast-7.aliyuncs.com'    => '泰国(曼谷)',
+            'oss-ap-south-1.aliyuncs.com'        => '印度(孟买)',
+            'oss-eu-central-1.aliyuncs.com'      => '德国(法兰克福)',
+            'oss-eu-west-1.aliyuncs.com'         => '英国(伦敦)',
+            'oss-me-east-1.aliyuncs.com'         => '阿联酋(迪拜)',
+            'oss-rg-china-mainland.aliyuncs.com' => '无地域属性(中国内地)'
+        ];
+    }
+
+    /**
+     * 获取存储区域
+     * @return array
+     */
+    public static function CosRegion(): array
+    {
+        return [
+            'ap-guangzhou'                => '华南地区(广州)',
+            'up-cn-east-2.qiniup.com'      => '华东-浙江2',
+            'up-z1.qiniup.com'             => '华北-河北',
+            'up-z2.qiniup.com'             => '华南-广东',
+            'up-na0.qiniup.com'            => '北美-洛杉矶',
+            'up-as0.qiniup.com'            => '亚太-新加坡',
+            'up-ap-northeast-1.qiniup.com' => '亚太-首尔',
+        ];
+    }
+
+    /**
+     * 获取存储区域
+     * @return array
+     */
+    public static function QiniuRegion(): array
+    {
+        return [
+            'up.qiniup.com'                => '华东-浙江',
+            'up-cn-east-2.qiniup.com'      => '华东-浙江2',
+            'up-z1.qiniup.com'             => '华北-河北',
+            'up-z2.qiniup.com'             => '华南-广东',
+            'up-na0.qiniup.com'            => '北美-洛杉矶',
+            'up-as0.qiniup.com'            => '亚太-新加坡',
+            'up-ap-northeast-1.qiniup.com' => '亚太-首尔',
+        ];
+    }
+
+}

+ 219 - 0
app/extra/tools/SceneData.php

@@ -0,0 +1,219 @@
+<?php
+
+namespace app\extra\tools;
+
+class SceneData
+{
+
+    /**
+     * 场景值
+     * @var array
+     */
+    protected array $data = [
+        // ==================== 抖音场景值 ====================
+        // 固定入口 021-
+        '021001' => '我的-更多-小程序',
+        '021002' => '全局扫一扫',
+        '021003' => '我的-收藏 tab 入口',
+        '021008' => '业务固定入口',
+        '021009' => '小程序跳转小程序',
+        '021010' => '小程序返回小程序',
+        '021013' => '我的订单',
+        '021014' => '发布页-添加小程序',
+        '021017' => '发布页-已添加的小程序',
+        '021018' => '发布页-添加团购组件',
+        '021020' => '小程序在系统桌面的快捷方式',
+        '021027' => '钱包-其他服务',
+        '021028' => '钱包-银行卡管理',
+        '021031' => '福利中心-banner',
+        '021036' => '个人主页-小程序模块-banner位',
+        '021037' => '电影-立即购票',
+        '021039' => '抖音-钱包-福利专区',
+        '021045' => '金币游戏',
+
+        // 搜索入口 022-
+        '022001' => '综合搜索-小程序入口',
+        '022002' => '搜索-阿拉丁卡片',
+        '022003' => '搜索-西瓜视频小程序内容-作者头像',
+        '022004' => '推荐tab-搜索(综搜)-商品卡坑位',
+        '022005' => '搜索-相关背景',
+        '022006' => '搜索-商品列表搜索-进入店铺',
+
+        // 内容入口 023-
+        '023001' => '视频页-左下角小程序链接',
+        '023002' => '视频页-评论区顶部',
+        '023003' => '企业号主页',
+        '023004' => '话题页-小程序卡片',
+        '023005' => 'POI详情页',
+        '023006' => '视频页-左下角影视综艺链接',
+        '023009' => '直播间-主播侧-预览',
+        '023010' => '直播间-用户侧-右下角小程序卡片',
+        '023012' => '直播间-用户侧-底部互动玩法',
+        '023014' => '视频页-左下角小说链接',
+        '023015' => '视频-搜索页-底bar-相关阅读',
+        '023016' => '视频页-左下角影视综艺链接-小程序卡片形式',
+        '023020' => '视频页-左下角POI-弹起小程序六分屏',
+        '023028' => '城市POI相关入口',
+        '023029' => '同城-地图模式',
+        '023030' => '关注页挂件',
+        '023040' => '小程序异形卡-订阅卡',
+
+        // 分享 024-
+        '024001' => '群聊会话内底部action_bar进入小程序',
+        '024002' => '微信对话',
+        '024003' => '微信朋友圈',
+        '024004' => 'QQ对话',
+        '024005' => 'QQ空间',
+        '024007' => '免流服务toast提示',
+        '024008' => '口令分享',
+        '024010' => '群公告',
+        '024012' => '小程序zlink回流',
+
+        // 广告 025-
+        '025001' => '广告',
+
+        // 运营位置 026-
+        '026001' => '搜索页-banner',
+        '026002' => '话题页-小程序文字链接',
+        '026003' => '推送消息-push',
+        '026004' => '消息-消息通知',
+        '026006' => '企业号-消息对话框',
+        '026007' => '我的-banner',
+
+        // 电商 027-
+        '027001' => '商品种草页',
+        '027002' => '个人主页-橱窗',
+        '027003' => '橱窗-商品卡片',
+        '027004' => '直播间-购物车-商品列表',
+        '027005' => '直播间-购物车卡片',
+        '027006' => '订单列表页',
+        '027007' => '抖音商城-我的订单-足迹-进入店铺',
+        '027008' => '售后页-进入店铺',
+        '027009' => '购物车-进入店铺',
+        '027010' => '直播间-小房子组件',
+        '027011' => '直播间-小房子组件',
+        '027012' => '店铺官方页',
+        '027013' => '抖音商城-品牌馆-商品卡',
+        '027014' => '商家主页',
+
+        // ==================== 抖音极速版场景值 ====================
+        // 固定入口 101-
+        '101001' => '设置页面入口',
+        '101002' => '扫一扫',
+        '101003' => '个人页-小程序入口',
+        '101008' => '业务固定入口-活动',
+        '101009' => '小程序跳转小程序',
+        '101010' => '小程序返回小程序',
+        '101020' => '桌面快捷方式-图标',
+        '101036' => '首页侧边栏',
+
+        // 搜索入口 102-
+        '102005' => '综合搜索-热点-相关事件',
+
+        // 内容入口 103-
+        '103001' => '搜索视频-小程序锚点入口',
+        '103003' => '抖音个人页-小程序入口',
+        '103010' => '直播间-榜单',
+        '103021' => '同城频道顶部推荐位',
+        '103040' => '小程序异形卡-订阅卡',
+
+        // 分享 104-
+        '104001' => '私信分享',
+        '104007' => '小程序分享回流页',
+
+        // 广告 105-
+        '105001' => '抖音极速版信息流广告投放',
+
+        // 运营位置 108-
+        '108013' => '订单综合入口',
+        '108014' => '发布页锚点入口-最近使用',
+
+        // ==================== 今日头条场景值 ====================
+        // 固定入口 011-
+        '011002' => '搜索中间页-上方推荐',
+        '011004' => '小程序中心-最近使用',
+        '011005' => '小程序中心-收藏',
+        '011006' => '小程序中心-发现',
+        '011007' => '扫一扫',
+        '011008' => '业务固定入口',
+        '011009' => '小程序跳转小程序',
+        '011010' => '小程序返回小程序',
+        '011014' => '我的-发现 12 宫格',
+        '011018' => '音频入口浮窗',
+        '011019' => '音频入口通知栏',
+        '011020' => '小程序桌面快捷方式',
+        '011021' => '我的-订单入口',
+
+        // 搜索入口 012-
+        '012001' => '小程序卡片搜索结果,直接搜索名字出来的结果',
+        '012002' => '小程序中心搜索结果',
+        '012003' => '搜索阿拉丁',
+        '012004' => '自然搜索结果',
+        '012005' => '搜索出 feed 中小程序内部文章',
+
+        // 内容入口 013-
+        '013001' => '分享的微头条',
+        '013002' => '小视频详情页入口-右侧 icon 样式',
+        '013003' => '小视频详情页入口-左侧链接样式',
+        '013005' => '文章详情页入口',
+        '013007' => '话题详情页入口',
+        '013008' => '头条号个人主页入口',
+        '013010' => '发布的微头条',
+        '013013' => '推荐-feed 流小程序卡片',
+
+        // 分享 014-
+        '014001' => '微信对话',
+        '014002' => '微信朋友圈',
+        '014003' => 'QQ 对话',
+        '014004' => 'QQ空间',
+        '014005' => '钉钉',
+        '014006' => '系统分享',
+        '014007' => '复制链接',
+        '014009' => '口令分享',
+
+        // 推广或其他入口 015-
+        '015001' => '本地频道-频道顶部 widget',
+        '015002' => '钱包入口',
+
+        // 广告 016-
+        '016001' => '小程序广告入口',
+
+        // 消息触达 017-
+        '017001' => 'app 的推送',
+        '017002' => '系统消息通知',
+        '017003' => '客服能力;开发者与用户沟通',
+        '017004' => '短信链接跳转小程序',
+
+        // ==================== 今日头条极速版场景值 ====================
+        // 固定入口 061-
+        '061001' => '搜索页固定入口-上方最近使用',
+        '061002' => '搜索页固定入口-上方推荐',
+        '061003' => '搜索页固定入口-下方推荐',
+        '061004' => '我的-小程序列表最近使用',
+        '061008' => '业务固定入口',
+        '061009' => '小程序跳转小程序',
+        '061010' => '小程序返回小程序',
+        '061020' => '小程序桌面快捷方式',
+        '061021' => '我的-订单入口',
+
+        // 搜索入口 062-
+        '062001' => '自然搜索结果,直接搜索名字出来的结果',
+        '062002' => '小程序列表搜索结果 -> 小程序盒子搜索结果',
+        '062003' => '搜索阿拉丁',
+        '062004' => '站外搜索跳小程序',
+
+        // 内容入口 063-
+        '063012' => '生活频道-小程序卡片',
+        '063013' => 'feed 流小程序卡片',
+
+        // 推广或其他 065-
+        '065001' => '频道顶部 widget',
+        '065002' => '钱包入口',
+        '065003' => '任务页-banner',
+    ];
+
+    public function getScene($key = null)
+    {
+        return $this->data[$key]??'未知';
+    }
+}

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

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

+ 830 - 0
app/functions.php

@@ -0,0 +1,830 @@
+<?php
+
+
+use app\service\system\SystemService;
+use support\Response;
+use Webman\Event\Event;
+
+if (!function_exists("between_time"))
+{
+    /**
+     * 格式化起止时间(为了兼容前端RangePicker组件)
+     * 2020-04-01T08:15:08.891Z => 1585670400
+     * @param array $times
+     * @param bool $isWithTime 是否包含时间
+     * @return array
+     */
+    function between_time(array $times, bool $isWithTime = false): array
+    {
+        foreach ($times as &$time) {
+            $time = trim($time, '&quot;');
+            $time = str2date($time, $isWithTime);
+        }
+        return ['start_time' => current($times), 'end_time' => next($times)];
+    }
+}
+if (!function_exists("str2date"))
+{
+    /**
+     * 日期转换时间戳
+     * 例如: 2020-04-01 08:15:08 => 1585670400
+     * @param string $date
+     * @param bool $isWithTime 是否包含时间
+     * @return int
+     */
+    function str2date(string $date, bool $isWithTime = false): int
+    {
+        if (!$isWithTime) {
+            $date = date('Y-m-d', strtotime($date));
+        }
+        return strtotime($date);
+    }
+}
+
+if (!function_exists("isValidMobile"))
+{
+    /**
+     * 验证是否为有效的中国大陆手机号码
+     * @param string $mobile
+     * @return bool
+     */
+    function isValidMobile(string $mobile): bool
+    {
+        // 去除空格、换行等
+        $mobile = trim($mobile);
+        // 正则:1 开头,第二位 3-9,后面 9 位数字,总共 11 位
+        $pattern = '/^1[3-9]\d{9}$/';
+        return preg_match($pattern, $mobile) === 1;
+    }
+}
+
+if(!function_exists("hide_mobile")){
+
+    /**
+     * 手机号码脱敏
+     * @param string $mobile
+     * @param int $len 4 末尾4位 6 末尾2位
+     * @return string
+     */
+    function hide_mobile(string $mobile,int $len = 6): string
+    {
+        return substr_replace($mobile, ($len==4 ? '****' : '******'), 3, $len);
+    }
+}
+
+if (!function_exists('hide_str')) {
+    /**
+     * 将一个字符串部分字符用*替代隐藏
+     * @param string $string 待转换的字符串
+     * @param int $begin 起始位置,从0开始计数,当$type=4时,表示左侧保留长度
+     * @param int $len 需要转换成*的字符个数,当$type=4时,表示右侧保留长度
+     * @param int $type 转换类型:0,从左向右隐藏;1,从右向左隐藏;2,从指定字符位置分割前由右向左隐藏;3,从指定字符位置分割后由左向右隐藏;4,保留首末指定字符串中间用***代替
+     * @param string $glue 分割符
+     * @return string   处理后的字符串
+     */
+    function hide_str(string $string, int $begin = 3, int $len = 4, int $type = 0, string $glue = "@"): string
+    {
+        $array = array();
+        if ($type == 0 || $type == 1 || $type == 4) {
+            $strlen = $length = mb_strlen($string);
+            while ($strlen) {
+                $array[] = mb_substr($string, 0, 1, "utf8");
+                $string = mb_substr($string, 1, $strlen, "utf8");
+                $strlen = mb_strlen($string);
+            }
+        }
+        if ($type == 0) {
+            for ($i = $begin; $i < ($begin + $len); $i++) {
+                if (isset($array[$i])) {
+                    $array[$i] = "*";
+                }
+            }
+            $string = implode("", $array);
+        } elseif ($type == 1) {
+            $array = array_reverse($array);
+            for ($i = $begin; $i < ($begin + $len); $i++) {
+                if (isset($array[$i])) {
+                    $array[$i] = "*";
+                }
+            }
+            $string = implode("", array_reverse($array));
+        } elseif ($type == 2) {
+            $array = explode($glue, $string);
+            if (isset($array[0])) {
+                $array[0] = hide_str($array[0], $begin, $len, 1);
+            }
+            $string = implode($glue, $array);
+        } elseif ($type == 3) {
+            $array = explode($glue, $string);
+            if (isset($array[1])) {
+                $array[1] = hide_str($array[1], $begin, $len, 0);
+            }
+            $string = implode($glue, $array);
+        } elseif ($type == 4) {
+            $left = $begin;
+            $right = $len;
+            $tem = array();
+            for ($i = 0; $i < ($length - $right); $i++) {
+                if (isset($array[$i])) {
+                    $tem[] = $i >= $left ? "" : $array[$i];
+                }
+            }
+            $tem[] = '*****';
+            $array = array_chunk(array_reverse($array), $right);
+            $array = array_reverse($array[0]);
+            for ($i = 0; $i < $right; $i++) {
+                if (isset($array[$i])) {
+                    $tem[] = $array[$i];
+                }
+            }
+            $string = implode("", $tem);
+        }
+        return $string;
+    }
+}
+
+if (!function_exists('list_sort_by')) {
+    /**
+     *----------------------------------------------------------
+     * 对查询结果集进行排序
+     *----------------------------------------------------------
+     * @access public
+     *----------------------------------------------------------
+     * @param array $list 查询结果
+     * @param string $field 排序的字段名
+     * @param string $sortBy 排序类型
+     * @switch string  asc正向排序 desc逆向排序 nat自然排序
+     *----------------------------------------------------------
+     * @return array
+     *----------------------------------------------------------
+     */
+    function list_sort_by(array $list, string $field, string $sortBy = 'asc'): array
+    {
+        if (!empty($list)) {
+            $refer = $resultSet = array();
+            foreach ($list as $i => $data)
+                $refer[$i] = &$data[$field];
+            switch ($sortBy) {
+                case 'asc': // 正向排序
+                    asort($refer);
+                    break;
+                case 'desc':// 逆向排序
+                    arsort($refer);
+                    break;
+                case 'nat': // 自然排序
+                    natcasesort($refer);
+                    break;
+            }
+            foreach ($refer as $key => $val)
+                $resultSet[] = &$list[$key];
+            return $resultSet;
+        }
+        return [];
+    }
+}
+
+
+if (!function_exists('supplement_id')) {
+    /**
+     * 用户ID风格
+     * @param string $id
+     * @return string
+     */
+    function supplement_id(string $id): string
+    {
+        $len = strlen($id);
+        $buf = '000000';
+        return $len < 6 ? substr($buf, 0, (6 - $len)) . $id : $id;
+    }
+}
+if (!function_exists('createOrderId')) {
+    /**
+     * 生成订单号
+     * @param string $letter
+     * @param int $length
+     * @return string
+     */
+    function createOrderId(string $letter = '', int $length = 3): string
+    {
+        $gradual = 0;
+        $orderId = date('YmdHis') . mt_rand(10000000, 99999999);
+        $lengths = strlen($orderId);
+
+        // 循环处理随机数
+        for ($i = 0; $i < $lengths; $i++) {
+            $gradual += (int)(substr($orderId, $i, 1));
+        }
+
+        if (empty($letter)) {
+            $letter = get_order_letter($length);
+        }
+
+        $code = (100 - $gradual % 100) % 100;
+        return $letter . $orderId . str_pad((string)$code, 2, '0', STR_PAD_LEFT);
+    }
+}
+
+if (!function_exists('get_order_letter')) {
+    /**
+     * 生成订单短ID
+     * @param int $length
+     * @return string
+     */
+    function get_order_letter(int $length = 2): string
+    {
+        $letter_all = range('A', 'Z');
+        shuffle($letter_all);
+        $letter_array = array_diff($letter_all, ['I', 'O']);
+        $letter = array_rand(array_flip($letter_array), $length);
+        return implode('', $letter);
+    }
+}
+
+/**
+ * 过滤emoji表情
+ * @param string $str
+ * @return string
+ */
+if (!function_exists('removeEmoji')) {
+    function removeEmoji(string $str = '') : string
+    {
+        $str = preg_replace('/[\x{1F600}-\x{1F64F}]/u', '', $str);
+        $str = preg_replace('/[\x{1F300}-\x{1F5FF}]/u', '', $str);
+        $str = preg_replace('/[\x{1F680}-\x{1F6FF}]/u', '', $str);
+        $str = preg_replace('/[\x{2600}-\x{26FF}]/u', '', $str);
+        return preg_replace('/[\x{2700}-\x{27BF}]/u', '', $str);
+    }
+}
+
+if (!function_exists('object_array')) {
+    /**
+     * @param $array
+     * @return array
+     */
+    function object_array($array): array
+    {
+        if(is_object($array)) {
+            $array = (array)$array;
+        }
+        if(is_array($array)) {
+            foreach($array as $key=>$value) {
+                $array[$key] = object_array($value);
+            }
+        }
+        return $array;
+    }
+}
+
+if (!function_exists("getDateFull"))
+{
+    /**
+     * @param string $format
+     * @param string $time
+     * @return string
+     */
+    function getDateFull(string $format = "Y-m-d H:i:s",string $time = ""): string
+    {
+        if (empty($time)) $time = time();
+        return date($format??'Y-m-d H:i:s',$time);
+    }
+}
+
+if (!function_exists("events"))
+{
+    /**
+     * 事件发布
+     * @param $name
+     * @param $data
+     * @return array|mixed|null
+     */
+    function events($name,$data): mixed
+    {
+        return Event::dispatch($name,$data);
+    }
+}
+
+if (!function_exists("success")) {
+    /**
+     * @param string $msg
+     * @param array $data
+     * @param int $code
+     * @return Response
+     */
+    function success(string $msg = "", array $data = [], int $code = 1): Response
+    {
+        return json(compact('code', 'data', 'msg'));
+    }
+}
+
+if (!function_exists("successTrans")) {
+    /**
+     * 消息返回
+     * @param string $message
+     * @param array $data
+     * @param int $code
+     * @return Response
+     */
+    function successTrans(string $message, array $data = [], int $code = 1): Response
+    {
+        $msg = trans($message);
+        return json(compact("msg", "code", "data"));
+    }
+}
+
+if (!function_exists("error")) {
+    /**
+     * @param $msg
+     * @param array $data
+     * @param int $code
+     * @return Response
+     */
+    function error($msg, array $data = [], int $code = 0): Response
+    {
+        return json(compact('code', 'data', 'msg'));
+    }
+}
+
+if (!function_exists("errorTrans")) {
+    /**
+     * 消息返回
+     * @param string $message
+     * @param array $data
+     * @param int $code
+     * @return Response
+     */
+    function errorTrans(string $message = "", array $data = [], int $code = 0): Response
+    {
+        $msg = trans($message);
+        return json(compact("msg", "code", "data"));
+    }
+}
+
+if (!function_exists("pageFormat")) {
+    /**
+     * @param $data
+     * @param int $size
+     * @return array {page:"",pageSize:"",rows:[],total:""}
+     */
+    function pageFormat($data, int $size = 10): array
+    {
+        if (empty($data)) return [];
+        return [
+            'total'     => $data->total(),
+            'page'      => $data->currentPage(),
+            'pageSize'  => $size,
+            'rows'      => $data->items()
+        ];
+    }
+}
+
+
+if (!function_exists("pageFormatBills")) {
+    /**
+     * @param $data
+     * @param int $size
+     * @return array {page:"",pageSize:"",rows:[],total:""}
+     */
+    function pageFormatBills($data, int $size = 10): array
+    {
+        if (empty($data)) return [];
+        $billsData = [];
+        foreach ($data as $key=>$bill) {
+            $billsData[$key] = [
+                "title"             => $bill["title"],
+                "day"               => $bill["day"],
+                "end_day"           => $bill["end_day"],
+                "order_num"         => $bill["order_num"],
+                "order_money"       => $bill["order_money"],
+                "store_money"       => $bill["store_money"],
+                "star_fee_total"    => $bill["star_fee_total"],
+                "status"            => $bill["status"],
+                "sys_fee"           => $bill["life_fee_total"]+$bill["sys_fee_total"],
+            ];
+        }
+        return [
+            'total'     => $data->total(),
+            'page'      => $data->currentPage(),
+            'pageSize'  => $size,
+            'rows'      => $billsData
+        ];
+    }
+}
+
+if (!function_exists("pageFormatMsg")) {
+    /**
+     * @param $data
+     * @param int $size
+     * @return array {page:"",pageSize:"",rows:[],total:""}
+     */
+    function pageFormatMsg($data, int $size = 10): array
+    {
+        if (empty($data)) return [];
+        $rowsData = [];
+        if ($data->total() > 0) {
+            foreach ($data as $key=>$row) {
+                $rowsData[$key] = $row->toArray();
+            }
+        }
+        return [
+            'total'     => $data->total(),
+            'page'      => $data->currentPage(),
+            'pageSize'  => $size,
+            'rows'      => list_sort_by($rowsData,"id","asc")
+        ];
+    }
+}
+
+
+if(!function_exists('format_money')){
+    function format_money($str,$len = '2',$append = ""): string
+    {
+        if (empty($str)) {
+            return "0.00";
+        }
+        return number_format($str, $len, ".", $append);
+    }
+}
+
+
+if (!function_exists('sConf')) {
+
+    /**
+     * 获取或配置系统参数
+     * @param string $name 参数名称
+     * @param string|null $value 参数内容
+     */
+    function sConf(string $name = '', string $value = null)
+    {
+        if (is_null($value) && is_string($name)) {
+            return SystemService::get($name);
+        } else {
+            return SystemService::set($name, $value);
+        }
+    }
+}
+
+if (!function_exists('sData')) {
+
+    /**
+     * JSON 数据读取与存储
+     * @param string $name 数据名称
+     * @param mixed $value 数据内容
+     */
+    function sData(string $name,mixed $value = null)
+    {
+        if (is_null($value)) {
+            return SystemService::getData($name);
+        } else {
+            return SystemService::setData($name, $value);
+        }
+    }
+}
+
+
+if (!function_exists('sOplog')) {
+    /**
+     * 写入系统日志
+     * @param string $action 日志行为
+     * @param string $content 日志内容
+     * @param string $userName
+     * @return boolean
+     */
+    function sOplog(string $action, string $content, string $userName): bool
+    {
+        return SystemService::setOplog($action, $content,$userName);
+    }
+}
+
+
+
+if(!function_exists('store_region')){
+    /**
+     * 返回附近司机数据
+     * @param $latitude
+     * @param $longitude
+     * @param $type 1 返回查询影响记录数;2 返回数据数组
+     */
+    function store_region($latitude,$longitude,$type = 1,$where = 'status = 1',$field="*"){
+        $sql = "select ".$field." from(
+SELECT id,poi_id,poi_name,status,poi_address,longitude,latitude,
+ROUND(6378.138*2*ASIN(SQRT(POW(SIN(({$latitude}*PI()/180-latitude*PI()/180)/2),2)+COS({$latitude}*PI()/180)*COS(latitude*PI()/180)*POW(SIN(({$longitude}*PI()/180-longitude*PI()/180)/2),2)))*1000) AS juli
+FROM saas_store_shop where ".$where.") as tmp_table_name order by juli asc";
+        if($type == 1){
+            return \think\facade\Db::execute($sql);
+        }
+        return \think\facade\Db::query($sql);
+    }
+}
+
+if(!function_exists('getHourlyTimeSlots')){
+    function getHourlyTimeSlots($startHour = 9, $endHour = 22, $format = 'H:i'): array
+    {
+        $slots = [];
+
+        for ($hour = $startHour; $hour < $endHour; $hour++) {
+            $startTime = sprintf('%02d:00', $hour);
+            $endTime = sprintf('%02d:00', $hour + 1);
+
+            $slots[] = [
+                'start' => $startTime,
+                'end' => $endTime,
+                'display' => $startTime . ' - ' . $endTime
+            ];
+        }
+
+        return $slots;
+    }
+}
+
+if (!function_exists("strToUniqueNumberV4"))
+{
+    /**
+     * 名称加密
+     * @param string $str
+     * @param string $salt
+     * @return string
+     */
+    function strToUniqueNumberV4(string $str = "", string $salt = ''): string
+    {
+        // 添加盐值增加唯一性
+        $str = $str . $salt;
+        // 使用多种哈希组合
+        $crc = abs(crc32($str));
+        $md5 = hexdec(substr(md5($str), 0, 8));
+        $sha1 = hexdec(substr(sha1($str), 0, 8));
+        // 组合并取模
+        $number = ($crc + $md5 + $sha1) % 1000000000000;
+        // 使用时间戳微调确保唯一性(针对同一字符串)
+        static $lastStr = '';
+        static $counter = 0;
+        if ($str === $lastStr) {
+            $counter++;
+            $number = ($number + $counter) % 1000000000000;
+        } else {
+            $lastStr = $str;
+            $counter = 0;
+        }
+        return str_pad($number, 12, '0', STR_PAD_LEFT);
+    }
+}
+
+if (!function_exists("extractNodesClean"))
+{
+    /**
+     * 多维数组转N个一维
+     * @param $data
+     * @param array $result
+     * @return array
+     */
+    function extractNodesClean($data, array &$result = []): array
+    {
+        foreach ($data as $node) {
+            // 复制节点并删除 sub_tree_infos
+            $cleanNode = $node;
+            unset($cleanNode['sub_tree_infos']);
+            $result[] = $cleanNode;
+
+            // 递归处理子节点
+            if (isset($node['sub_tree_infos']) && is_array($node['sub_tree_infos'])) {
+                extractNodesClean($node['sub_tree_infos'], $result);
+            }
+        }
+        return $result;
+    }
+}
+if (!function_exists("formatTime"))
+{
+    /**
+     * 格式化时间:今天、昨天、前天、正常日期
+     * @param string $timestamp 时间戳 或 日期字符串
+     * @return string
+     */
+    function formatTime(string $timestamp): string
+    {
+        // 先统一转成时间戳
+        $time = is_numeric($timestamp) ? $timestamp : strtotime($timestamp);
+
+        $today = strtotime('today');       // 今天 00:00
+        $yesterday = strtotime('yesterday'); // 昨天 00:00
+        $beforeYesterday = strtotime('-2 days'); // 前天 00:00
+
+        if ($time >= $today) {
+            return '今天';
+        } elseif ($time >= $yesterday) {
+            return '昨天';
+        } elseif ($time >= $beforeYesterday) {
+            return '前天';
+        } else {
+            // 其他时间返回正常格式
+            return date('Y-m-d', $time);
+        }
+    }
+}
+if(! function_exists ('timeDiff') ) {
+    function timeDiff($begin_time, $end_time): array
+    {
+        if ($begin_time < $end_time) {
+            return ["day" => 0, "hour" => 0, "min" => 0, "sec" => 0];
+        } else {
+            $starttime = $end_time;
+            $endtime = $begin_time;
+        }
+
+        //计算天数
+        $timediff = $endtime - $starttime;
+        $days = intval($timediff / 86400);
+        //计算小时数
+        $remain = $timediff % 86400;
+        $hours = intval($remain / 3600);
+        //计算分钟数
+        $remain = $remain % 3600;
+        $mins = intval($remain / 60);
+        //计算秒数
+        $secs = $remain % 60;
+        return ["day" => $days, "hour" => $hours, "min" => $mins, "sec" => $secs];
+    }
+}
+
+if(! function_exists('filterTreeByTargets'))
+{
+
+    /**
+     * 过滤树,仅保留包含任何目标 category_id 的节点及其祖先
+     *
+     * @param array $tree       原始树
+     * @param array $targetIds  目标 category_id 列表
+     * @return array 过滤后的树(保持原结构)
+     */
+    function filterTreeByTargets($tree, $targetIds) {
+        $filtered = [];
+        foreach ($tree as $node) {
+            // 检查当前节点或子树中是否包含任一目标 ID
+            $contains = false;
+            if (in_array($node['category_id'], $targetIds)) {
+                $contains = true;
+            } else {
+                // 递归检查子节点
+                $children = [];
+                if (!empty($node['children']) && is_array($node['children'])) {
+                    $children = filterTreeByTargets($node['children'], $targetIds);
+                    if (!empty($children)) {
+                        $contains = true;
+                    }
+                }
+            }
+
+            if ($contains) {
+                // 保留当前节点,并合并过滤后的子节点
+                $newNode = $node;
+                if (!empty($node['children']) && is_array($node['children'])) {
+                    $newNode['children'] = filterTreeByTargets($node['children'], $targetIds);
+                } else {
+                    $newNode['children'] = [];
+                }
+                $filtered[] = $newNode;
+            }
+        }
+        return $filtered;
+    }
+}
+
+if(! function_exists('addTypesRecursive'))
+{
+    /**
+     * 为树中每个节点添加 types 字段
+     */
+    function addTypesRecursive(&$nodes, $typeMap) {
+        foreach ($nodes as &$node) {
+            $node['types'] = $typeMap[$node['category_id']] ?? '';
+            if (isset($node['children']) && is_array($node['children'])) {
+                addTypesRecursive($node['children'], $typeMap);
+            }
+        }
+    }
+}
+
+
+if(! function_exists('emojiToUnicode'))
+{
+    /**
+     * @param string $str
+     * @return string
+     */
+    function emojiToUnicode(string $str): string
+    {
+        return trim(json_encode($str, JSON_UNESCAPED_SLASHES), '"');
+    }
+}
+
+if(! function_exists('unicodeToEmoji'))
+{
+    function unicodeToEmoji(string $str): string
+    {
+        return json_decode("\"{$str}\"");
+    }
+}
+
+if(! function_exists('removeEmoji2'))
+{
+    /**
+     * 清除字符串中所有emoji表情
+     * @param string $str
+     * @return string
+     */
+    function removeEmoji2(string $str): string
+    {
+        // 匹配绝大部分emoji Unicode区间
+//        $pattern = '/[\x{1F600}-\x{1F64F}\x{1F300}-\x{1F5FF}\x{1F680}-\x{1F6FF}\x{1F1E0}-\x{1F1FF}\x{2600}-\x{26FF}\x{2700}-\x{27BF}\xFE00-\xFE0F\x{1F900}-\x{1F9FF}]/u';
+        $pattern = "/[^\p{L}\p{N}\s]/u";
+        return preg_replace($pattern, '', $str);
+    }
+}
+if(! function_exists('str_cut_ellipsis'))
+{
+    function str_cut_ellipsis(string $str, int $len = 20, string $dot = '...'): string
+    {
+        if (mb_strlen($str, 'UTF-8') <= $len) {
+            return $str;
+        }
+        return mb_substr($str, 0, $len, 'UTF-8') . $dot;
+    }
+}
+
+if (! function_exists('filterHtmlTags'))
+{
+    /**
+     * 批量修改 HTML 中指定标签的属性和样式
+     *
+     * @param string $html      原始 HTML 内容
+     * @param array  $rules     规则数组,键为标签名(小写),值为配置:
+     * [
+     *  'remove' => ['width', 'height'], // 要移除的属性名列表
+     *   'style'  => ['width' => '100%', 'margin' => '0'] // 要强制设置/覆盖的样式
+     * ]
+     * @return string 处理后的 HTML
+     */
+    function filterHtmlTags($html, $rules) {
+        // 匹配所有标签的开始部分:<标签名 属性...> 或 <标签名 属性.../> 或 <标签名>
+        $pattern = '/<(\w+)(?:\s+([^>]*?))?\/?>/i';
+        return preg_replace_callback($pattern, function($matches) use ($rules) {
+            $tag = strtolower($matches[1]);
+            // 如果该标签不在规则中,直接返回原匹配
+            if (!isset($rules[$tag])) {
+                return $matches[0];
+            }
+            $rule = $rules[$tag];
+            // 解析属性(如果有)
+            $attrStr = isset($matches[2]) ? $matches[2] : '';
+            $attrArray = [];
+            if ($attrStr) {
+                // 匹配 属性名="值" 或 属性名='值' 或 属性名=值
+                preg_match_all('/([a-zA-Z-]+)\s*=\s*("([^"]*)"|\'([^\']*)\'|([^\s>]+))/', $attrStr, $matchesAttr, PREG_SET_ORDER);
+                foreach ($matchesAttr as $m) {
+                    $name = strtolower($m[1]);
+                    $value = isset($m[3]) ? $m[3] : (isset($m[4]) ? $m[4] : $m[5]);
+                    $attrArray[$name] = $value;
+                }
+            }
+
+            // 1. 移除指定属性
+            if (isset($rule['remove'])) {
+                foreach ($rule['remove'] as $attr) {
+                    unset($attrArray[$attr]);
+                }
+            }
+
+            // 2. 处理样式
+            if (isset($rule['style']) && is_array($rule['style'])) {
+                $oldStyle = isset($attrArray['style']) ? $attrArray['style'] : '';
+                $styles = [];
+                if ($oldStyle) {
+                    // 简单解析,按分号分割
+                    $parts = array_filter(array_map('trim', explode(';', $oldStyle)));
+                    foreach ($parts as $part) {
+                        if (strpos($part, ':') !== false) {
+                            list($key, $value) = array_map('trim', explode(':', $part, 2));
+                            $styles[$key] = $value;
+                        }
+                    }
+                }
+                // 强制覆盖(或添加)样式
+                foreach ($rule['style'] as $key => $value) {
+                    $styles[$key] = $value;
+                }
+                // 重组 style
+                $newStyle = '';
+                foreach ($styles as $k => $v) {
+                    $newStyle .= $k . ':' . $v . ';';
+                }
+                $attrArray['style'] = $newStyle;
+            }
+
+            // 重建属性字符串
+            $newAttr = '';
+            foreach ($attrArray as $k => $v) {
+                $newAttr .= ' ' . $k . '="' . htmlspecialchars($v, ENT_QUOTES, 'UTF-8') . '"';
+            }
+            return '<' . $tag . $newAttr . '>';
+        }, $html);
+    }
+}

+ 41 - 0
app/middleware/AuthMiddleware.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace app\middleware;
+
+use Shopwwi\WebmanAuth\Auth;
+use Shopwwi\WebmanAuth\JWT;
+use Webman\Http\Request;
+use Webman\Http\Response;
+use Webman\MiddlewareInterface;
+
+class AuthMiddleware implements MiddlewareInterface
+{
+
+    public function process(Request $request, callable $handler): Response
+    {
+        try {
+            $controller = new \ReflectionClass($request->controller);
+            $noNeedLogin = $controller->getDefaultProperties()['noNeedLogin']??[];
+            if (empty($noNeedLogin) || !in_array($request->action, $noNeedLogin)) {
+                $type = $request->header('api-type','');
+                if (empty($type)) return json(['code'=> 0,'msg'=> trans("error.param")]);
+                $token =  $request->header("Authorization","");
+                if (empty($token)) return json(['code'=> 0,'msg'=> trans("error.request")]);
+                $guard = "admin";
+                if ($type == 'mini') {
+                    $guard = "user";
+                }
+                (new JWT)->guard($guard)->verify();
+                $user = (new Auth)->guard($guard)->user();
+                if (empty($user)) return json(['code'=>401,'msg'=> trans("error.login")]);
+                $request->user = $user->toArray();
+            }
+        } catch (\Throwable $throwable) {
+//            return response('',401);
+            return json(['code'=> 401,'msg'=> $throwable->getMessage()]);
+        }
+        $response = $request->method() == 'OPTIONS' ? response('',204) : $handler($request);
+        return $response;
+    }
+
+}

+ 42 - 0
app/middleware/StaticFile.php

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

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

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

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

@@ -0,0 +1,47 @@
+<?php
+
+namespace app\model\system;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property integer $parent_id 
+ * @property string $parent_path 
+ * @property integer $role_custom 额外权限
+ * @property string $name 
+ * @property mixed $create_at
+ */
+class SystemGroup extends Model
+{
+    /**
+     * The connection name for the model.
+     *
+     * @var string|null
+     */
+    protected $connection = 'mysql';
+    
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected string $table = "system_group";
+    
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected string $primaryKey = "id";
+    
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public bool $timestamps = false;
+
+
+}

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

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

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

@@ -0,0 +1,63 @@
+<?php
+
+namespace app\model\system;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property integer $role_id 权限ID
+ * @property string $role_path 
+ * @property string $fs_open_id 飞书OpenID
+ * @property integer $parent_id 
+ * @property string $username 用户名
+ * @property string $truename 真实姓名
+ * @property mixed $abbrname 姓名代号
+ * @property string $email 邮箱
+ * @property mixed $mobile 手机号码
+ * @property string $brith 生日
+ * @property string $password 密码
+ * @property mixed $salt 密钥串
+ * @property integer $status 状态
+ * @property integer $is_deleted 删除状态
+ * @property integer $is_super 
+ * @property string $remark 备注
+ * @property string $job_time 入职时间
+ * @property string $login_ip 登录IP
+ * @property mixed $login_at 登录时间
+ * @property mixed $updated_at 更新时间
+ * @property mixed $create_at 创建时间
+ */
+class SystemUser extends Model
+{
+    /**
+     * The connection name for the model.
+     *
+     * @var string|null
+     */
+    protected $connection = 'mysql';
+    
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected string $table = "system_user";
+    
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected string $primaryKey = "id";
+    
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public bool $timestamps = false;
+
+
+}

+ 10 - 0
app/process/Http.php

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

+ 305 - 0
app/process/Monitor.php

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

+ 30 - 0
app/service/system/MenuService.php

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

+ 182 - 0
app/service/system/SystemService.php

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

+ 98 - 0
app/service/system/UploadService.php

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

+ 37 - 0
app/service/system/UserService.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace app\service\system;
+
+use app\extra\basic\Service;
+use app\model\system\SystemUser;
+
+class UserService extends Service
+{
+
+    /**
+     *
+     * @return $this
+     */
+    public function setModel()
+    {
+        $this->mode = (new SystemUser);
+        return $this;
+    }
+
+    /**
+     *
+     * @param array $param
+     * @return array
+     */
+    public function searchFilter(array $param = []): array
+    {
+        $filter = [];
+        !empty($param['status']) && $filter[] = ["status", '=', $param['status']];
+        !empty($param['type']) && $filter[] = ["type", '=', $param['type']];
+        !empty($param['store_id']) && $filter[] = ["store_id", '=', $param['store_id']];
+        !empty($param['name']) && $filter[] = ["poi_name", 'like', "%{$param['name']}%"];
+        return $filter;
+    }
+
+
+}

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

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

+ 12 - 0
app/view/protocol.html

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title><?php echo ($name=='privacy'?'隐私协议':'服务协议');?></title>
+</head>
+<body>
+<div class="">
+    <?php echo htmlspecialchars_decode($data);?>
+</div>
+</body>
+</html>

ファイルの差分が大きいため隠しています
+ 0 - 0
city.json


+ 90 - 0
composer.json

@@ -0,0 +1,90 @@
+{
+  "name": "workerman/webman",
+  "type": "project",
+  "keywords": [
+    "high performance",
+    "http service"
+  ],
+  "homepage": "https://www.workerman.net",
+  "license": "MIT",
+  "description": "High performance HTTP Service Framework.",
+  "authors": [
+    {
+      "name": "walkor",
+      "email": "walkor@workerman.net",
+      "homepage": "https://www.workerman.net",
+      "role": "Developer"
+    }
+  ],
+  "support": {
+    "email": "walkor@workerman.net",
+    "issues": "https://github.com/walkor/webman/issues",
+    "forum": "https://wenda.workerman.net/",
+    "wiki": "https://workerman.net/doc/webman",
+    "source": "https://github.com/walkor/webman"
+  },
+  "require": {
+    "php": ">=8.1",
+    "workerman/webman-framework": "^2.1",
+    "monolog/monolog": "^2.0",
+    "webman/console": "^2.2",
+    "webman/database": "^2.1",
+    "webman/think-orm": "^2.1",
+    "illuminate/pagination": "^12.55",
+    "illuminate/events": "^12.55",
+    "symfony/var-dumper": "^7.4",
+    "webman/redis": "^2.1",
+    "webman/validation": "^2.2",
+    "yzh52521/easyhttp": "^1.1",
+    "webman/event": "^1.0",
+    "phpoffice/phpspreadsheet": "^5.5",
+    "workerman/crontab": "^1.0",
+    "vlucas/phpdotenv": "^5.6",
+    "webman/redis-queue": "^2.1",
+    "webman/think-cache": "^2.1",
+    "kkokk/poster": "^3.0",
+    "isszz/webman-hashids": "^0.0.2",
+    "linfly/annotation": "^2.1",
+    "hhink/webman-sms": "^1.0",
+    "hzdad/codecheck": "^1.0",
+    "php-di/php-di": "^7.1",
+    "shopwwi/webman-auth": "^2.0",
+    "tinywan/storage": "^1.1",
+    "aliyuncs/oss-sdk-php": "^2.7",
+    "qcloud/cos-sdk-v5": "^2.6",
+    "qiniu/php-sdk": "^7.14",
+    "luckycmc/webman-province-city-area": "^1.0",
+    "tinywan/captcha": "^0.0.4",
+    "webman/push": "^1.1"
+  },
+  "suggest": {
+    "ext-event": "For better performance. "
+  },
+  "autoload": {
+    "psr-4": {
+      "": "./",
+      "app\\": "./app",
+      "App\\": "./app",
+      "app\\View\\Components\\": "./app/view/components"
+    }
+  },
+  "scripts": {
+    "post-package-install": [
+      "support\\Plugin::install"
+    ],
+    "post-package-update": [
+      "support\\Plugin::install"
+    ],
+    "pre-package-uninstall": [
+      "support\\Plugin::uninstall"
+    ],
+    "post-create-project-cmd": [
+      "support\\Setup::run"
+    ],
+    "setup-webman": [
+      "support\\Setup::run"
+    ]
+  },
+  "minimum-stability": "dev",
+  "prefer-stable": true
+}

+ 26 - 0
config/app.php

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

+ 21 - 0
config/autoload.php

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

+ 18 - 0
config/bootstrap.php

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

+ 31 - 0
config/cache.php

@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+return [
+    'default' => 'file',
+    'stores' => [
+        'file' => [
+            'driver' => 'file',
+            'path' => runtime_path('cache')
+        ],
+        'redis' => [
+            'driver' => 'redis',
+            'connection' => 'default'
+        ],
+        'array' => [
+            'driver' => 'array'
+        ]
+    ]
+];

+ 6 - 0
config/container.php

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

+ 29 - 0
config/database.php

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

+ 15 - 0
config/dependence.php

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

+ 5 - 0
config/event.php

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

+ 17 - 0
config/exception.php

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

+ 32 - 0
config/log.php

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

+ 15 - 0
config/middleware.php

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ファイルの差分が大きいため隠しています
+ 11 - 0
config/plugin/tinywan/captcha/app.php


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

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

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

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

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

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

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

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

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

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

+ 10 - 0
config/plugin/webman/push/app.php

@@ -0,0 +1,10 @@
+<?php
+return [
+    'enable'       => true,
+    'websocket'    => 'websocket://0.0.0.0:8131',
+    'api'          => 'http://0.0.0.0:8232',
+    'app_key'      => '265c33b73d5c04f918978577df2c48d2',
+    'app_secret'   => '25774aa8461dcd54e5a318ba1e58f75b',
+    'channel_hook' => 'http://127.0.0.1:1881/plugin/webman/push/hook',
+    'auth'         => '/plugin/webman/push/auth'
+];

+ 21 - 0
config/plugin/webman/push/process.php

@@ -0,0 +1,21 @@
+<?php
+
+use Webman\Push\Server;
+
+return [
+    'server' => [
+        'handler'     => Server::class,
+        'listen'      => config('plugin.webman.push.app.websocket'),
+        'count'       => 1, // 必须是1
+        'reloadable'  => false, // 执行reload不重启
+        'constructor' => [
+            'api_listen' => config('plugin.webman.push.app.api'),
+            'app_info'   => [
+                config('plugin.webman.push.app.app_key') => [
+                    'channel_hook' => config('plugin.webman.push.app.channel_hook'),
+                    'app_secret'   => config('plugin.webman.push.app.app_secret'),
+                ],
+            ]
+        ]
+    ]
+];

+ 87 - 0
config/plugin/webman/push/route.php

@@ -0,0 +1,87 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+use support\Request;
+use Webman\Route;
+use Webman\Push\Api;
+
+/**
+ * 推送js客户端文件
+ */
+Route::get('/plugin/webman/push/push.js', function (Request $request) {
+    return response()->file(base_path().'/vendor/webman/push/src/push.js');
+});
+
+/**
+ * 私有频道鉴权,这里应该使用session辨别当前用户身份,然后确定该用户是否有权限监听channel_name
+ */
+Route::post(config('plugin.webman.push.app.auth'), function (Request $request) {
+    $pusher = new Api(str_replace('0.0.0.0', '127.0.0.1', config('plugin.webman.push.app.api')), config('plugin.webman.push.app.app_key'), config('plugin.webman.push.app.app_secret'));
+    $channel_name = $request->post('channel_name');
+    $session = $request->session();
+    // 这里应该通过session和channel_name判断当前用户是否有权限监听channel_name
+    $has_authority = true;
+    if ($has_authority) {
+        return response($pusher->socketAuth($channel_name, $request->post('socket_id')));
+    } else {
+        return response('Forbidden', 403);
+    }
+});
+
+/**
+ * 当频道上线以及下线时触发的回调
+ * 频道上线:是指某个频道从没有连接在线到有连接在线的事件
+ * 频道下线:是指某个频道的所有连接都断开触发的事件
+ */
+Route::post(parse_url(config('plugin.webman.push.app.channel_hook'), PHP_URL_PATH), function (Request $request) {
+
+    // 没有x-pusher-signature头视为伪造请求
+    if (!$webhook_signature = $request->header('x-pusher-signature')) {
+        return response('401 Not authenticated', 401);
+    }
+
+    $body = $request->rawBody();
+
+    // 计算签名,$app_secret 是双方使用的密钥,是保密的,外部无从得知
+    $expected_signature = hash_hmac('sha256', $body, config('plugin.webman.push.app.app_secret'), false);
+
+    // 安全校验,如果签名不一致可能是伪造的请求,返回401状态码
+    if ($webhook_signature !== $expected_signature) {
+        return response('401 Not authenticated', 401);
+    }
+
+    // 这里存储这上线 下线的channel数据
+    $payload = json_decode($body, true);
+
+    $channels_online = $channels_offline = [];
+
+    foreach ($payload['events'] as $event) {
+        if ($event['name'] === 'channel_added') {
+            $channels_online[] = $event['channel'];
+        } else if ($event['name'] === 'channel_removed') {
+            $channels_offline[] = $event['channel'];
+        }
+    }
+
+    // 业务根据需要处理上下线的channel,例如将在线状态写入数据库,通知其它channel等
+//    // 上线的所有channel
+//    echo 'online channels: ' . implode(',', $channels_online) . "\n";
+//    // 下线的所有channel
+//    echo 'offline channels: ' . implode(',', $channels_offline) . "\n";
+
+    return 'OK';
+});
+
+
+

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

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

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

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

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

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

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

@@ -0,0 +1,19 @@
+<?php
+return [
+    'redis_consumer_fast'  => [ // key是自定义的,没有格式限制,这里取名redis_consumer_fast
+        'handler'     => Webman\RedisQueue\Process\Consumer::class,
+        'count'       => 8,
+        'constructor' => [
+            // 消费者类目录
+            'consumer_dir' => app_path() . '/queue/redis/fast'
+        ]
+    ],
+    'redis_consumer_slow'  => [  // key是自定义的,没有格式限制,这里取名redis_consumer_slow
+        'handler'     => Webman\RedisQueue\Process\Consumer::class,
+        'count'       => 8,
+        'constructor' => [
+            // 消费者类目录
+            'consumer_dir' => app_path() . '/queue/redis/slow'
+        ]
+    ]
+];

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 50 - 0
config/process.php

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

+ 29 - 0
config/redis.php

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

+ 21 - 0
config/route.php

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

+ 23 - 0
config/server.php

@@ -0,0 +1,23 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+return [
+    'event_loop' => '',
+    'stop_timeout' => 2,
+    'pid_file' => runtime_path() . '/webman.pid',
+    'status_file' => runtime_path() . '/webman.status',
+    'stdout_file' => runtime_path() . '/logs/stdout.log',
+    'log_file' => runtime_path() . '/logs/workerman.log',
+    'max_package_size' => 10 * 1024 * 1024
+];

+ 65 - 0
config/session.php

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

+ 23 - 0
config/static.php

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

+ 38 - 0
config/think-cache.php

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

+ 42 - 0
config/think-orm.php

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

+ 25 - 0
config/translation.php

@@ -0,0 +1,25 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+/**
+ * Multilingual configuration
+ */
+return [
+    // Default language
+    'locale' => 'zh_CN',
+    // Fallback language
+    'fallback_locale' => ['zh_CN', 'en'],
+    // Folder where language files are stored
+    'path' => base_path() . '/resource/translations',
+];

+ 22 - 0
config/view.php

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

+ 11 - 0
docker-compose.yml

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

BIN
public/favicon.ico


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

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

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません