Browse Source

feat:init

master
zhangf@suq.cn 1 week ago
commit
8da7b79a05
  1. 8
      .gitignore
  2. 21
      LICENSE
  3. 70
      README.md
  4. 23
      app/controller/DistillationWordController.php
  5. 17
      app/dao/DistillationWordDao.php
  6. 4
      app/functions.php
  7. 42
      app/middleware/StaticFile.php
  8. 33
      app/model/DistillationExpandWord.php
  9. 33
      app/model/DistillationTransWord.php
  10. 33
      app/model/DistillationWord.php
  11. 10
      app/process/Http.php
  12. 305
      app/process/Monitor.php
  13. 32
      app/route/route.php
  14. 54
      app/service/DistillationWordService.php
  15. 28
      app/validate/UserGroupValidate.php
  16. 14
      app/view/index/view.html
  17. 65
      composer.json
  18. 7563
      composer.lock
  19. 26
      config/app.php
  20. 21
      config/autoload.php
  21. 18
      config/bootstrap.php
  22. 18
      config/cache.php
  23. 22
      config/container.php
  24. 40
      config/database.php
  25. 15
      config/dependence.php
  26. 5
      config/event.php
  27. 17
      config/exception.php
  28. 32
      config/log.php
  29. 15
      config/middleware.php
  30. 83
      config/plugin/tinywan/jwt/app.php
  31. 81
      config/plugin/tinywan/storage/app.php
  32. 24
      config/plugin/webman/console/app.php
  33. 4
      config/plugin/webman/event/app.php
  34. 17
      config/plugin/webman/event/bootstrap.php
  35. 7
      config/plugin/webman/event/command.php
  36. 5
      config/plugin/x2nx/webman-migrate/app.php
  37. 44
      config/plugin/x2nx/webman-migrate/command.php
  38. 24
      config/plugin/x2nx/webman-migrate/phinx.php
  39. 62
      config/process.php
  40. 18
      config/redis.php
  41. 24
      config/route.php
  42. 23
      config/server.php
  43. 65
      config/session.php
  44. 23
      config/static.php
  45. 42
      config/think-orm.php
  46. 25
      config/translation.php
  47. 22
      config/view.php
  48. 37
      database/factories/UsersFactory.php
  49. 37
      database/migrations/2025_09_01_145446_create_attachment_table.php
  50. 33
      database/migrations/2025_10_20_141905_create_platform_table.php
  51. 40
      database/seeders/DatabaseSeeder.php
  52. 8
      plugin/piadmin/.idea/.gitignore
  53. 8
      plugin/piadmin/.idea/modules.xml
  54. 22
      plugin/piadmin/.idea/php.xml
  55. 6
      plugin/piadmin/.idea/vcs.xml
  56. 437
      plugin/piadmin/api/Install.php
  57. 40
      plugin/piadmin/app/base/BaseController.php
  58. 796
      plugin/piadmin/app/base/BaseDao.php
  59. 27
      plugin/piadmin/app/base/BaseModel.php
  60. 62
      plugin/piadmin/app/base/BaseService.php
  61. 16
      plugin/piadmin/app/base/BaseValidate.php
  62. 53
      plugin/piadmin/app/common/ContainerProxy.php
  63. 51
      plugin/piadmin/app/controller/v1/AttachmentController.php
  64. 16
      plugin/piadmin/app/controller/v1/DataDictionaryController.php
  65. 37
      plugin/piadmin/app/controller/v1/IndexController.php
  66. 50
      plugin/piadmin/app/controller/v1/LoginController.php
  67. 96
      plugin/piadmin/app/controller/v1/MenuController.php
  68. 83
      plugin/piadmin/app/controller/v1/SystemAdminController.php
  69. 76
      plugin/piadmin/app/controller/v1/SystemDeptController.php
  70. 99
      plugin/piadmin/app/controller/v1/SystemRoleController.php
  71. 16
      plugin/piadmin/app/dao/AttachmenatDao.php
  72. 46
      plugin/piadmin/app/dao/InformationSchemaDao.php
  73. 19
      plugin/piadmin/app/dao/SystemAdminDao.php
  74. 15
      plugin/piadmin/app/dao/SystemAdminDeptDao.php
  75. 15
      plugin/piadmin/app/dao/SystemAdminRoleDao.php
  76. 15
      plugin/piadmin/app/dao/SystemDeptDao.php
  77. 17
      plugin/piadmin/app/dao/SystemMenuDao.php
  78. 18
      plugin/piadmin/app/dao/SystemRoleDao.php
  79. 19
      plugin/piadmin/app/dao/SystemRoleMenuDao.php
  80. 17
      plugin/piadmin/app/exception/ApiException.php
  81. 17
      plugin/piadmin/app/exception/FrameworkException.php
  82. 162
      plugin/piadmin/app/functions.php
  83. 81
      plugin/piadmin/app/middleware/AdminAuthorizationMiddleware.php
  84. 28
      plugin/piadmin/app/middleware/CrossDomainMiddleware.php
  85. 26
      plugin/piadmin/app/middleware/LanguageMiddleware.php
  86. 145
      plugin/piadmin/app/middleware/ParameterConvertMiddleware.php
  87. 40
      plugin/piadmin/app/middleware/PermissionsMiddleware.php
  88. 44
      plugin/piadmin/app/model/Attachment.php
  89. 20
      plugin/piadmin/app/model/InformationSchema.php
  90. 45
      plugin/piadmin/app/model/SystemAdmin.php
  91. 11
      plugin/piadmin/app/model/SystemAdminDept.php
  92. 27
      plugin/piadmin/app/model/SystemAdminRole.php
  93. 26
      plugin/piadmin/app/model/SystemDept.php
  94. 16
      plugin/piadmin/app/model/SystemMenu.php
  95. 30
      plugin/piadmin/app/model/SystemRole.php
  96. 25
      plugin/piadmin/app/model/SystemRoleMenu.php
  97. 16
      plugin/piadmin/app/route/v1/attachment.php
  98. 71
      plugin/piadmin/app/route/v1/route.php
  99. 98
      plugin/piadmin/app/service/AttachmentService.php
  100. 24
      plugin/piadmin/app/service/DataDictionaryService.php

8
.gitignore

@ -0,0 +1,8 @@
/runtime
/.idea
/.vscode
/vendor
*.log
.env
/tests/tmp
/tests/.phpunit.result.cache

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

70
README.md

@ -0,0 +1,70 @@
<div style="padding:18px;max-width: 1024px;margin:0 auto;background-color:#fff;color:#333">
<h1>webman</h1>
基于<a href="https://www.workerman.net" target="__blank">workerman</a>开发的超高性能PHP框架
<h1>学习</h1>
<ul>
<li>
<a href="https://www.workerman.net/webman" target="__blank">主页 / Home page</a>
</li>
<li>
<a href="https://webman.workerman.net" target="__blank">文档 / Document</a>
</li>
<li>
<a href="https://www.workerman.net/doc/webman/install.html" target="__blank">安装 / Install</a>
</li>
<li>
<a href="https://www.workerman.net/questions" target="__blank">问答 / Questions</a>
</li>
<li>
<a href="https://www.workerman.net/apps" target="__blank">市场 / Apps</a>
</li>
<li>
<a href="https://www.workerman.net/sponsor" target="__blank">赞助 / Sponsors</a>
</li>
<li>
<a href="https://www.workerman.net/doc/webman/thanks.html" target="__blank">致谢 / Thanks</a>
</li>
</ul>
<div style="float:left;padding-bottom:30px;">
<h1>赞助商</h1>
<h4>特别赞助</h4>
<a href="https://www.crmeb.com/?form=workerman" target="__blank">
<img src="https://www.workerman.net/img/sponsors/6429/20230719111500.svg" width="200">
</a>
<h4>铂金赞助</h4>
<a href="https://www.fadetask.com/?from=workerman" target="__blank"><img src="https://www.workerman.net/img/sponsors/1/20230719084316.png" width="200"></a>
<a href="https://www.yilianyun.net/?from=workerman" target="__blank" style="margin-left:20px;"><img src="https://www.workerman.net/img/sponsors/6218/20230720114049.png" width="200"></a>
</div>
<div style="float:left;padding-bottom:30px;clear:both">
<h1>请作者喝咖啡</h1>
<img src="https://www.workerman.net/img/wx_donate.png" width="200">
<img src="https://www.workerman.net/img/ali_donate.png" width="200">
<br>
<b>如果您觉得webman对您有所帮助,欢迎捐赠。</b>
</div>
<div style="clear: both">
<h1>LICENSE</h1>
The webman is open-sourced software licensed under the MIT.
</div>
</div>

23
app/controller/DistillationWordController.php

@ -0,0 +1,23 @@
<?php
namespace app\controller;
use app\service\DistillationWordService;
use support\Response;
class UserController
{
public function index(DistillationWordService $service): Response
{
$params = requestOnly([
'name' => '',
]);
return success($service->listData($params));
}
public function read(DistillationWordService $service): Response
{
$id = input('id');
return success($service->readData($id));
}
}

17
app/dao/DistillationWordDao.php

@ -0,0 +1,17 @@
<?php
namespace app\dao;
use app\model\DistillationWord;
use plugin\piadmin\app\base\BaseDao;
class DistillationWordDao extends BaseDao
{
protected function setModel(): string
{
return DistillationWord::class;
}
}

4
app/functions.php

@ -0,0 +1,4 @@
<?php
/**
* Here is your custom functions.
*/

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

33
app/model/DistillationExpandWord.php

@ -0,0 +1,33 @@
<?php
namespace app\model;
use plugin\piadmin\app\base\BaseModel;
/**
* 蒸馏词关联扩展词模型
*/
class DistillationExpandWord extends BaseModel
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'geo_distillation_expand_word';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
}

33
app/model/DistillationTransWord.php

@ -0,0 +1,33 @@
<?php
namespace app\model;
use plugin\piadmin\app\base\BaseModel;
/**
* 蒸馏词关联目标转化词模型
*/
class DistillationTransWord extends BaseModel
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'geo_distillation_trans_word';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
}

33
app/model/DistillationWord.php

@ -0,0 +1,33 @@
<?php
namespace app\model;
use plugin\piadmin\app\base\BaseModel;
/**
* 蒸馏词模型
*/
class DistillationWord extends BaseModel
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'geo_distillation_word';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
}

10
app/process/Http.php

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

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

32
app/route/route.php

@ -0,0 +1,32 @@
<?php
use app\controller\ActionArticleController;
use app\controller\ActionOptimizeController;
use app\controller\BrandLibraryController;
use app\controller\OrderController;
use app\controller\PlatformController;
use app\controller\ProjectBrandController;
use app\controller\ProjectController;
use app\controller\ProjectKeywordController;
use app\controller\ProjectResultBrandController;
use app\controller\ProjectResultContentController;
use app\controller\ProjectResultController;
use app\controller\ProjectResultReferenceSiteController;
use app\controller\ProjectTaskController;
use app\controller\ProjectWordCloudController;
use app\controller\SiteController;
use app\controller\ProjectTopicController;
use app\controller\ProjectUserController;
use app\controller\UserController;
use app\controller\UserGroupController;
use app\controller\VipController;
use plugin\piadmin\app\middleware\AdminAuthorizationMiddleware;
use plugin\piadmin\app\middleware\PermissionsMiddleware;
use Webman\Route;
Route::group('/service/v1', function () {
})->middleware([
AdminAuthorizationMiddleware::class,
PermissionsMiddleware::class
]);

54
app/service/DistillationWordService.php

@ -0,0 +1,54 @@
<?php
namespace app\service;
use app\dao\DistillationWordDao;
use plugin\piadmin\app\base\BaseService;
use plugin\piadmin\app\exception\ApiException;
use plugin\piadmin\app\utils\RequestUtils;
class DistillationWordService extends BaseService
{
protected $dao;
public function __construct(DistillationWordDao $dao)
{
$this->dao = $dao;
}
/**
* 获取前台用户列表
* @param array $params
* @return array
*/
public function listData(array $params): array
{
[$page, $limit] = RequestUtils::getPageParameter();
[$sortRule, $sortField] = RequestUtils::getSortParameter();
$query = [
'delete_time' => 0
];
if (isNotBlank($params['name'])) {
$query[] = ['name', 'like', '%' . $params['name'] . '%'];
}
$list = $this->dao->getList($query, '*', $page, $limit, "$sortField $sortRule");
$count = $this->dao->getCount($query);
return compact('list', 'count');
}
/**
* 获取用户信息
* @param mixed $id
* @return array
*/
public function readData(mixed $id): array
{
$user = $this->dao->get(['id' => $id]);
if (empty($user)) {
throw new ApiException('数据不存在');
}
return $user->toArray();
}
}

28
app/validate/UserGroupValidate.php

@ -0,0 +1,28 @@
<?php
namespace app\validate;
use plugin\piadmin\app\base\BaseValidate;
class UserGroupValidate extends BaseValidate
{
protected $group = [
'save' => [
'name' => 'require|max:90',
'trans_words' => 'require|in:1,0',
'expand_words' => 'require|in:1,2',
],
'update' => [
'id' => 'require',
'name' => 'require|max:90',
'trans_words' => 'require|in:1,0',
'expand_words' => 'require|in:1,2',
]
];
protected $message = [
'id.require' => 'ID不能为空',
'name.require' => '主关键词不能为空',
'trans_words.require' => '目标转化词不能为空',
'expand_words.require' => '扩展词不能为空',
];
}

14
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>

65
composer.json

@ -0,0 +1,65 @@
{
"name": "workerman/webman",
"type": "project",
"keywords": [
"high performance",
"http service"
],
"homepage": "https://www.workerman.net",
"license": "MIT",
"description": "High performance HTTP Service Framework.",
"authors": [
{
"name": "walkor",
"email": "walkor@workerman.net",
"homepage": "https://www.workerman.net",
"role": "Developer"
}
],
"support": {
"email": "walkor@workerman.net",
"issues": "https://github.com/walkor/webman/issues",
"forum": "https://wenda.workerman.net/",
"wiki": "https://workerman.net/doc/webman",
"source": "https://github.com/walkor/webman"
},
"require": {
"php": ">=8.1",
"workerman/webman-framework": "^2.1",
"monolog/monolog": "^2.0",
"suqflash/piadmin": "^0.0.5",
"webman/database": "^2.1",
"illuminate/pagination": "^12.26",
"illuminate/events": "^12.26",
"symfony/var-dumper": "^7.3",
"x2nx/webman-migrate": "^0.0.8",
"tinywan/storage": "^1.1",
"aliyuncs/oss-sdk-php": "^2.7",
"ext-simplexml": "*",
"ext-dom": "*"
},
"suggest": {
"ext-event": "For better performance. "
},
"autoload": {
"psr-4": {
"": "./",
"app\\": "./app",
"App\\": "./app",
"app\\View\\Components\\": "./app/view/components"
}
},
"scripts": {
"post-package-install": [
"support\\Plugin::install"
],
"post-package-update": [
"support\\Plugin::install"
],
"pre-package-uninstall": [
"support\\Plugin::uninstall"
]
},
"minimum-stability": "dev",
"prefer-stable": true
}

7563
composer.lock
File diff suppressed because it is too large
View File

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

18
config/cache.php

@ -0,0 +1,18 @@
<?php
return [
'default' => getenv('CACHE_MODE'),
'stores' => [
'file' => [
'driver' => 'file',
'path' => runtime_path('cache')
],
'redis' => [
'driver' => 'redis',
'connection' => 'default'
],
'array' => [
'driver' => 'array'
]
]
];

22
config/container.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
*/
/**
* 初始化DI容器
*/
$builder = new \DI\ContainerBuilder();
$builder->addDefinitions(config('dependence', []));
$builder->useAutowiring(true);
$builder->useAttributes(true);
return $builder->build();

40
config/database.php

@ -0,0 +1,40 @@
<?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' => 'default',
// 各种数据库配置
'connections' => [
'default' => [
'driver' => env('DB_CONNECTION', 'mysql'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', 3306),
'database' => env('DB_NAME', ''),
'username' => env('DB_USER', ''),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => env('DB_PREFIX', ''),
'suffix' => env('DB_SUFFIX', ''),
'strict' => env('DB_STRICT', false),
'engine' => env('DB_ENGINE', null),
'options' => [
\PDO::ATTR_TIMEOUT => env('DB_TIMEOUT', 5),
]
],
],
];

15
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
config/event.php

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

17
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
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
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 [];

83
config/plugin/tinywan/jwt/app.php

@ -0,0 +1,83 @@
<?php
return [
'enable' => true,
'jwt' => [
/** 算法类型 HS256、HS384、HS512、RS256、RS384、RS512、ES256、ES384、ES512、PS256、PS384、PS512 */
'algorithms' => 'HS256',
/** access令牌秘钥 */
'access_secret_key' => '2022d3d3LmJq',
/** access令牌过期时间,单位:秒。默认 2 小时 */
'access_exp' => 7200,
/** refresh令牌秘钥 */
'refresh_secret_key' => '2022KTxigxc9o50c',
/** refresh令牌过期时间,单位:秒。默认 7 天 */
'refresh_exp' => 604800,
/** refresh 令牌是否禁用,默认不禁用 false */
'refresh_disable' => false,
/** 令牌签发者 */
'iss' => 'webman.tinywan.cn',
/** 某个时间点后才能访问,单位秒。(如:30 表示当前时间30秒后才能使用) */
'nbf' => 0,
/** 时钟偏差冗余时间,单位秒。建议这个余地应该不大于几分钟 */
'leeway' => 60,
/** 是否允许单设备登录,默认不允许 false */
'is_single_device' => false,
/** 缓存令牌时间,单位:秒。默认 7 天 */
'cache_token_ttl' => 604800,
/** 缓存令牌前缀,默认 JWT:TOKEN: */
'cache_token_pre' => 'JWT:TOKEN:',
/** 缓存刷新令牌前缀,默认 JWT:REFRESH_TOKEN: */
'cache_refresh_token_pre' => 'JWT:REFRESH_TOKEN:',
/** 用户信息模型 */
'user_model' => function ($uid) {
return [];
},
/** 是否支持 get 请求获取令牌 */
'is_support_get_token' => false,
/** GET 请求获取令牌请求key */
'is_support_get_token_key' => 'authorization',
/** access令牌私钥 */
'access_private_key' => <<<EOD
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
EOD,
/** access令牌公钥 */
'access_public_key' => <<<EOD
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
EOD,
/** refresh令牌私钥 */
'refresh_private_key' => <<<EOD
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
EOD,
/** refresh令牌公钥 */
'refresh_public_key' => <<<EOD
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
EOD,
],
];

81
config/plugin/tinywan/storage/app.php

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

24
config/plugin/webman/console/app.php

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

4
config/plugin/webman/event/app.php

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

17
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
config/plugin/webman/event/command.php

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

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

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

44
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
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" => getenv('DB_TYPE'),
"host" => getenv('DB_HOST', '127.0.0.1'),
"name" => getenv('DB_NAME'),
"user" => getenv('DB_USER'),
"pass" => getenv('DB_PASSWORD'),
"port" => getenv('DB_PORT'),
"charset" => 'utf8',
"table_prefix" => 'pi_',
"table_suffix" => '',
],
'version_order' => 'creation'
]
];

62
config/process.php

@ -0,0 +1,62 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use support\Log;
use support\Request;
use app\process\Http;
global $argv;
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8788',
'count' => cpu_count() * 4,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '',
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
// File update detection and automatic reload
'monitor' => [
'handler' => app\process\Monitor::class,
'reloadable' => false,
'constructor' => [
// Monitor these directories
'monitorDir' => array_merge([
app_path(),
config_path(),
base_path() . '/process',
base_path() . '/support',
base_path() . '/resource',
base_path() . '/.env',
], glob(base_path() . '/plugin/*/app'), glob(base_path() . '/plugin/*/config'), glob(base_path() . '/plugin/*/api')),
// Files with these suffixes will be monitored
'monitorExtensions' => [
'php', 'html', 'htm', 'env'
],
'options' => [
'enable_file_monitor' => !in_array('-d', $argv) && DIRECTORY_SEPARATOR === '/',
'enable_memory_monitor' => DIRECTORY_SEPARATOR === '/',
]
]
]
];

18
config/redis.php

@ -0,0 +1,18 @@
<?php
return [
'default' => [
'password' => getenv('REDIS_PASSWORD'),
'host' => getenv('REDIS_HOST'),
'port' => getenv('REDIS_PORT'),
'database' => getenv('REDIS_DB'),
'prefix' => getenv('REDIS_PREFIX'),
'pool' => [
'max_connections' => 5,
'min_connections' => 1,
'wait_timeout' => 3,
'idle_timeout' => 60,
'heartbeat_interval' => 50,
],
]
];

24
config/route.php

@ -0,0 +1,24 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use Webman\Route;
// 引入业务
require_once base_path() . '/app/route/route.php';
// 处理404路由
Route::fallback(function(){
return error([],'Route not found');
});

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

42
config/think-orm.php

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

25
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
config/view.php

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

37
database/factories/UsersFactory.php

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

37
database/migrations/2025_09_01_145446_create_attachment_table.php

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use support\Db;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Db::schema()->create('attachment', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('storage')->default('1')->comment('附件引擎 1本地 2七牛 3腾讯 4阿里');
$table->integer('type')->default(0)->comment('0 所有 1 压缩包 2 图片 3 视频 4 文件 5 音频');
$table->string('name')->default('')->comment('文件名');
$table->string('path')->default('')->comment('文件路径');
$table->string('suffix')->default('')->comment('文件后缀');
$table->string('original_name')->default('')->comment('文件原名');
$table->float('size', 2)->default(0)->comment('文件大小');
$table->bigInteger('creator_id')->default(0)->comment('创建者ID');
$table->timestamps();
$table->softDeletes();
$table->comment('附件表');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Db::schema()->dropIfExists('attachment');
}
};

33
database/migrations/2025_10_20_141905_create_platform_table.php

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use support\Db;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Db::schema()->create('platform', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name')->default('')->comment('名称');
$table->integer('status')->default(1)->comment('状态 1启用 0禁用');
$table->bigInteger('create_by')->default(0)->comment('创建者ID');
$table->bigInteger('update_by')->default(0)->comment('编辑者ID');
$table->timestamps();
$table->softDeletes();
$table->comment('平台表');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Db::schema()->dropIfExists('platform');
}
};

40
database/seeders/DatabaseSeeder.php

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

8
plugin/piadmin/.idea/.gitignore

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
plugin/piadmin/.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/piadmin.iml" filepath="$PROJECT_DIR$/.idea/piadmin.iml" />
</modules>
</component>
</project>

22
plugin/piadmin/.idea/php.xml

@ -0,0 +1,22 @@
<?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="PhpProjectSharedConfiguration" php_language_level="8.0">
<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
plugin/piadmin/.idea/vcs.xml

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

437
plugin/piadmin/api/Install.php

@ -0,0 +1,437 @@
<?php
namespace plugin\piadmin\api;
use plugin\admin\api\Menu;
use support\Db;
use Throwable;
class Install
{
/**
* 数据库连接
*/
protected static $connection = 'plugin.admin.mysql';
/**
* 安装
*
* @param $version
* @return void
*/
public static function install($version)
{
var_dump('piadmin install.....');
//处理env文件,修改config/think-orm.php,redis.php,cache.php配置从env获取
$env = base_path() . DIRECTORY_SEPARATOR .'.env';
clearstatcache();
if (is_file($env)) {
var_dump('env文件存在,请勿重复安装');
return;
}
if (!is_writable(base_path() . DIRECTORY_SEPARATOR . 'config')) {
var_dump('文件权限验证失败');
return;
}
// 重写配置文件
self::generateConfig();
// 写入env文件
$env_config = <<<EOF
# 数据库配置
DB_TYPE = mysql
DB_HOST = 127.0.0.1
DB_PORT = 3306
DB_NAME = piadmin
DB_USER = piadmin
DB_PASSWORD = 123456
DB_PREFIX =
# 缓存方式
CACHE_MODE = file
# Redis配置
REDIS_HOST = 127.0.0.1
REDIS_PORT = 6379
REDIS_PASSWORD = ''
REDIS_DB = 0
REDIS_PREFIX = pi_adm_
EOF;
file_put_contents(base_path() . DIRECTORY_SEPARATOR . '.env', $env_config);
var_dump('安装成功,请修改env配置文件后,导入/plugin/piadmin/install.sql文件');
// 安装数据库
// static::installSql();
// // 导入菜单
// if($menus = static::getMenus()) {
// Menu::import($menus);
// }
}
/**
* 生成配置文件
*/
protected static function generateConfig()
{
// 1、think-orm配置文件
$think_orm_config = <<<EOF
<?php
return [
'default' => 'mysql',
'connections' => [
'mysql' => [
// 数据库类型
'type' => getenv('DB_TYPE'),
// 服务器地址
'hostname' => getenv('DB_HOST'),
// 数据库名
'database' => getenv('DB_NAME'),
// 数据库用户名
'username' => getenv('DB_USER'),
// 数据库密码
'password' => getenv('DB_PASSWORD'),
// 数据库连接端口
'hostport' => getenv('DB_PORT'),
// 数据库连接参数
'params' => [
// 连接超时3秒
\PDO::ATTR_TIMEOUT => 3,
],
// 数据库编码默认采用utf8
'charset' => 'utf8',
// 数据库表前缀
'prefix' => getenv('DB_PREFIX'),
// 断线重连
'break_reconnect' => true,
// 自定义分页类
'bootstrap' => '',
// 连接池配置
'pool' => [
'max_connections' => 5, // 最大连接数
'min_connections' => 1, // 最小连接数
'wait_timeout' => 3, // 从连接池获取连接等待超时时间
'idle_timeout' => 60, // 连接最大空闲时间,超过该时间会被回收
'heartbeat_interval' => 50, // 心跳检测间隔,需要小于60秒
],
],
],
];
EOF;
file_put_contents(base_path() . '/config/think-orm.php', $think_orm_config);
// 2、chache配置文件
$cache_config = <<<EOF
<?php
return [
'default' => getenv('CACHE_MODE'),
'stores' => [
'file' => [
'driver' => 'file',
'path' => runtime_path('cache')
],
'redis' => [
'driver' => 'redis',
'connection' => 'default'
],
'array' => [
'driver' => 'array'
]
]
];
EOF;
file_put_contents(base_path() . '/config/cache.php', $cache_config);
// 3、redis配置文件
$redis_config = <<<EOF
<?php
return [
'default' => [
'password' => getenv('REDIS_PASSWORD'),
'host' => getenv('REDIS_HOST'),
'port' => getenv('REDIS_PORT'),
'database' => getenv('REDIS_DB'),
'prefix' => getenv('REDIS_PREFIX'),
'pool' => [
'max_connections' => 5,
'min_connections' => 1,
'wait_timeout' => 3,
'idle_timeout' => 60,
'heartbeat_interval' => 50,
],
]
];
EOF;
file_put_contents(base_path() . '/config/redis.php', $redis_config);
// 4、依赖注入配置
$dependence_config = <<<EOF
<?php
/**
* 初始化DI容器
*/
$builder = new \DI\ContainerBuilder();
$builder->addDefinitions(config('dependence', []));
$builder->useAutowiring(true);
$builder->useAttributes(true);
return $builder->build();
EOF;
file_put_contents(base_path() . '/config/container.php', $dependence_config);
self::tinywanConfig();
}
/**
* @desc 文件上传配置文件
* @author zhangf@suq.cn
* @date 2025.9.1
* @return void
*/
private static function tinywanConfig()
{
// 4、依赖注入配置
$tinywan_config = <<<EOF
<?php
/**
* @desc app.php 描述信息
*
* @author Tinywan(ShaoBo Wan)
* @date 2022/3/10 19:46
*/
return [
'enable' => true,
'storage' => [
'default' => 'oss', // local:本地 oss:阿里云 cos:腾讯云 qos:七牛云
'single_limit' => 1024 * 1024 * 200, // 单个文件的大小限制,默认200M 1024 * 1024 * 200
'total_limit' => 1024 * 1024 * 200, // 所有文件的大小限制,默认200M 1024 * 1024 * 200
'nums' => 10, // 文件数量限制,默认10
'include' => [
'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'pdf',
'png', 'jpg', 'jpeg', 'gif', 'bmp'
], // 被允许的文件类型列表
'exclude' => [
'php', 'py', 'js', 'html', 'css', 'class'
], // 不被允许的文件类型列表
// 本地对象存储
'local' => [
'adapter' => \Tinywan\Storage\Adapter\LocalAdapter::class,
'root' => public_path().'/uploads/',
'dirname' => function () {
return date('Ymd');
},
'domain' => 'http://127.0.0.1:8787',
'uri' => '/uploads/', // 如果 domain + uri 不在 public 目录下,请做好软链接,否则生成的url无法访问
'algo' => 'sha1',
],
// 阿里云对象存储
'oss' => [
'adapter' => \Tinywan\Storage\Adapter\OssAdapter::class,
'accessKeyId' => 'xxxxxxxxxxxx',
'accessKeySecret' => 'xxxxxxxxxxxx',
'bucket' => 'resty-webman',
'dirname' => function () {
return 'storage';
},
'domain' => 'http://webman.oss.tinywan.com',
'endpoint' => 'oss-cn-hangzhou.aliyuncs.com',
'algo' => 'sha1',
],
// 腾讯云对象存储
'cos' => [
'adapter' => \Tinywan\Storage\Adapter\CosAdapter::class,
'secretId' => 'xxxxxxxxxxxxx',
'secretKey' => 'xxxxxxxxxxxx',
'bucket' => 'resty-webman-xxxxxxxxx',
'dirname' => 'storage',
'domain' => 'http://webman.oss.tinywan.com',
'region' => 'ap-shanghai',
],
// 七牛云对象存储
'qiniu' => [
'adapter' => \Tinywan\Storage\Adapter\QiniuAdapter::class,
'accessKey' => 'xxxxxxxxxxxxx',
'secretKey' => 'xxxxxxxxxxxxx',
'bucket' => 'resty-webman',
'dirname' => 'storage',
'domain' => 'http://webman.oss.tinywan.com',
],
// aws
's3' => [
'adapter' => \Tinywan\Storage\Adapter\S3Adapter::class,
'key' => 'xxxxxxxxxxxxx',
'secret' => 'xxxxxxxxxxxxx',
'bucket' => 'resty-webman',
'dirname' => 'storage',
'domain' => 'http://webman.oss.tinywan.com',
'region' => 'S3_REGION',
'version' => 'latest',
'use_path_style_endpoint' => true,
'endpoint' => 'S3_ENDPOINT',
'acl' => 'public-read',
],
],
];
EOF;
file_put_contents(base_path() . '/config/plugin/tinywan/storage/app.php', $tinywan_config);
}
/**
* 卸载
*
* @param $version
* @return void
*/
public static function uninstall($version)
{
// 删除菜单
foreach (static::getMenus() as $menu) {
Menu::delete($menu['key']);
}
// 卸载数据库
static::uninstallSql();
}
/**
* 更新
*
* @param $from_version
* @param $to_version
* @param $context
* @return void
*/
public static function update($from_version, $to_version, $context = null)
{
// 删除不用的菜单
if (isset($context['previous_menus'])) {
static::removeUnnecessaryMenus($context['previous_menus']);
}
// 安装数据库
static::installSql();
// 导入新菜单
if ($menus = static::getMenus()) {
Menu::import($menus);
}
// 执行更新操作
$update_file = __DIR__ . '/../update.php';
if (is_file($update_file)) {
include $update_file;
}
}
/**
* 更新前数据收集等
*
* @param $from_version
* @param $to_version
* @return array|array[]
*/
public static function beforeUpdate($from_version, $to_version)
{
// 在更新之前获得老菜单,通过context传递给 update
return ['previous_menus' => static::getMenus()];
}
/**
* 获取菜单
*
* @return array|mixed
*/
public static function getMenus()
{
clearstatcache();
if (is_file($menu_file = __DIR__ . '/../config/menu.php')) {
$menus = include $menu_file;
return $menus ?: [];
}
return [];
}
/**
* 删除不需要的菜单
*
* @param $previous_menus
* @return void
*/
public static function removeUnnecessaryMenus($previous_menus)
{
$menus_to_remove = array_diff(Menu::column($previous_menus, 'name'), Menu::column(static::getMenus(), 'name'));
foreach ($menus_to_remove as $name) {
Menu::delete($name);
}
}
/**
* 安装SQL
*
* @return void
*/
protected static function installSql()
{
static::importSql(__DIR__ . '/../install.sql');
}
/**
* 卸载SQL
*
* @return void
*/
protected static function uninstallSql() {
// 如果卸载数据库文件存在责直接使用
$uninstallSqlFile = __DIR__ . '/../uninstall.sql';
if (is_file($uninstallSqlFile)) {
static::importSql($uninstallSqlFile);
return;
}
// 否则根据install.sql生成卸载数据库文件uninstall.sql
$installSqlFile = __DIR__ . '/../install.sql';
if (!is_file($installSqlFile)) {
return;
}
$installSql = file_get_contents($installSqlFile);
preg_match_all('/CREATE TABLE `(.+?)`/si', $installSql, $matches);
$dropSql = '';
foreach ($matches[1] as $table) {
$dropSql .= "DROP TABLE IF EXISTS `$table`;\n";
}
file_put_contents($uninstallSqlFile, $dropSql);
static::importSql($uninstallSqlFile);
unlink($uninstallSqlFile);
}
/**
* 导入数据库
*
* @return void
*/
public static function importSql($mysqlDumpFile)
{
if (!$mysqlDumpFile || !is_file($mysqlDumpFile)) {
return;
}
foreach (explode(';', file_get_contents($mysqlDumpFile)) as $sql) {
if ($sql = trim($sql)) {
try {
Db::connection(static::$connection)->statement($sql);
} catch (Throwable $e) {}
}
}
}
}

40
plugin/piadmin/app/base/BaseController.php

@ -0,0 +1,40 @@
<?php
namespace plugin\piadmin\app\base;
use support\Request;
use think\exception\ValidateException;
use think\Validate;
/**
* 控制器基础类
*/
abstract class BaseController
{
/**
* Request实例
* @var Request
*/
protected $request;
protected $service;
/**
* 构造方法
* @access public
*/
public function __construct()
{
$this->request = \request();
// 控制器初始化
$this->initialize();
}
// 初始化
protected function initialize()
{}
}

796
plugin/piadmin/app/base/BaseDao.php

@ -0,0 +1,796 @@
<?php
namespace plugin\piadmin\app\base;
use plugin\piadmin\app\utils\IdUtils;
use plugin\piadmin\app\exception\ApiException;
use plugin\piadmin\app\utils\RequestUtils;
use ReflectionException;
use think\Collection;
use think\db\exception\DbException;
use think\facade\Db;
use think\helper\Str;
use think\Model;
/**
* 抽象基类,提供数据访问对象(DAO)的基本结构和通用方法
* 该类不能直接实例化,旨在被其他DAO类继承,以利用其定义的通用功能
* 主要作用包括:
* - 定义了子类必须实现的抽象方法
* - 提供了数据库连接的通用逻辑(假设实现)
* - 其他通用数据操作方法的模板
*/
abstract class BaseDao
{
/**
* 当前表名别名
* @var string
*/
protected string $alias;
/**
* join表别名
* @var string
*/
protected string $joinAlis;
/**
* 获取当前模型
* @return string
*/
abstract protected function setModel(): string;
/**
* 设置join链表模型
* @return string
*/
protected function setJoinModel(): string
{
return '';
}
/**
* 读取数据条数
* @param array $where
* @param bool $search
* @return int
* @throws ReflectionException|DbException
*/
public function count(array $where = [], bool $search = true): int
{
return $this->search($where, $search)->count();
}
/**
* 根据指定条件选择数据列表
*
* 此方法用于根据多个条件查询数据,并支持分页、排序和关联加载等功能
* 它是构建复杂查询的一个灵活入口点,允许开发者指定所需的查询条件和选项,
* 并获取一个数据集合作为结果返回
*
* @param array $where 查询条件数组,用于指定查询的过滤条件
* @param string $field 要查询的字段,默认为 '*',表示查询所有字段
* @param int $page 分页页码,默认为 0,表示不分页
* @param int $limit 每页数据条数,默认为 0,表示不限制条数
* @param string $order 排序条件字符串,默认为空,表示不排序
* @param array $with 关联加载数组,用于指定要加载的关联关系
* @param bool $search 是否使用搜索模式,默认为 false
*
* @return Collection 查询结果集合
* @throws ReflectionException
*/
public function selectList(array $where, string $field = '*', int $page = 0, int $limit = 0,
string $order = '', array $with = [], bool $search = false): Collection
{
return $this->selectModel($where, $field, $page, $limit, $order, $with, $search)->select();
}
/**
* 根据查询条件构造模型
* @param array $where
* @param string $field
* @param int $page
* @param int $limit
* @param string $order
* @param array $with
* @param bool $search
* @return BaseModel
* @throws ReflectionException
*/
private function selectModel(array $where, string $field = '*', int $page = 0, int $limit = 0, string $order = '', array $with = [], bool $search = false): mixed
{
if ($search) {
$model = $this->search($where);
} else {
$model = $this->getModel()->where($where);
}
return $model->field($field)->when($page && $limit, function ($query) use ($page, $limit) {
$query->page($page, $limit);
})->when($order !== '', function ($query) use ($order) {
$query->order($order);
})->when($with, function ($query) use ($with) {
$query->with($with);
});
}
/**
* 获取满足条件的记录数
*
* 该方法用于执行一个查询,以获取数据库中满足给定条件的记录数
* 它通过传递一个数组参数来指定查询条件,然后使用模型的where方法来过滤数据,
* 最后使用count方法来统计满足条件的记录数
*
* @param array $where 查询条件数组,用于指定过滤条件
* @return int 满足条件的记录数
*/
public function getCount(array $where): int
{
return $this->getModel()->where($where)->count();
}
/**
* 获取唯一值的计数
*
* 该方法用于计算给定字段在特定条件下的唯一值数量根据$search参数的值,
* 选择使用search方法还是getModel方法来执行查询
*
* @param array $where 查询条件,用于筛选记录
* @param string $field 需要计算唯一值的字段名称
* @param bool $search 指示是否使用search方法来执行查询,默认为true如果为false,则使用getModel方法
*
* @return int 返回计算得到的唯一值数量如果没有匹配的记录,返回0
*/
public function getDistinctCount(array $where, $field, bool $search = true): int
{
if ($search) {
// 使用search方法执行查询,并计算指定字段的唯一值数量
return $this->search($where)->field('COUNT(distinct(' . $field . ')) as count')->select()->toArray()[0]['count'] ?? 0;
} else {
// 使用getModel方法执行查询,并计算指定字段的唯一值数量
return $this->getModel()->where($where)->field('COUNT(distinct(' . $field . ')) as count')->select()->toArray()[0]['count'] ?? 0;
}
}
/**
* 获取模型
* @return BaseModel
*/
public function getModel(): BaseModel
{
return app()->make($this->setModel());
}
/**
* 获取主键
* @return array|string
*/
protected function getPk(): array|string
{
return $this->getModel()->getPk();
}
/**
* 获取模型对应的表名
* @return string
*/
public function getTableName(): string
{
return $this->getModel()->getName();
}
/**
* 根据主键获取一条或多条数据
* @param $id
* @param array|null $field
* @param array|null $with
* @return array|Model|null
* @throws \think\db\exception\DataNotFoundException
* @throws DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function get($id, ?array $field = [], ?array $with = []): array|Model|null
{
if (is_array($id)) {
$where = $id;
} else {
$where = [$this->getPk() => $id];
}
return $this->getModel()->where($where)->when(count($with), function ($query) use ($with) {
$query->with($with);
})->field($field ?? ['*'])->find();
}
/**
* 查询一条数据是否存在
* @param $map
* @param string $field
* @return bool 是否存在
*/
public function be($map, string $field = '')
{
if (!is_array($map) && empty($field)) $field = $this->getPk();
$map = !is_array($map) ? [$field => $map] : $map;
return 0 < $this->getModel()->where($map)->count();
}
/**
* 根据条件获取一条数据
* @param array $where
* @param string|null $field
* @param array $with
* @return array|Model|null
* @throws \think\db\exception\DataNotFoundException
* @throws DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getOne(array $where, ?string $field = '*', array $with = [])
{
$field = explode(',', $field);
return $this->get($where, $field, $with);
}
/**
* 获取单个字段值
* @param $where
* @param string|null $field
* @param string $order
* @return mixed
*/
public function value($where, ?string $field = '', string $order = ''): mixed
{
$pk = $this->getPk();
return $this->getModel()->where($where)->order($order)->value($field ?: $pk);
}
/**
* 根据条件查询某一列数据
* @param array $where
* @param string $field
* @param string $key
* @return array
*/
public function getColumn(array $where, string $field, string $key = ''): array
{
return $this->getModel()->where($where)->column($field, $key);
}
/**
* 删除记录
*
* 该方法支持根据单个或多个条件删除记录当$id为数组时,表示根据多个条件删除记录;
* $id为单个值时,表示根据主键或指定的$key删除记录
*
* @param array|int|string $id 要删除的记录的ID或条件数组
* @param string|null $key $id为单个值时,指定的字段名,默认为主键
*
* @return bool 成功删除返回true,否则返回false
*/
public function delete(array|int|string $id, ?string $key = null): bool
{
// 判断$id是否为数组,如果是,则直接使用$id作为删除条件
if (is_array($id)) {
$where = $id;
} else {
// 如果$id不是数组,则根据是否设置了$key来决定使用主键还是$key作为删除条件
$where = [is_null($key) ? $this->getPk() : $key => $id];
}
// 使用确定的删除条件执行删除操作,并返回删除结果
return $this->getModel()->where($where)->delete();
}
/**
* 强制删除记录
*
* 该方法用于删除一个指定ID的资源。可以根据$force参数决定是软删除还是硬删除。
*
* @param int $id 要删除的资源的ID
* @param bool $force 是否强制删除,true为硬删除,false为软删除(默认)
* @return mixed 删除操作的结果,具体类型取决于getModel()->destroy方法的返回值
*/
public function destroy(int $id, bool $force = false)
{
return $this->getModel()->destroy($id, $force);
}
/**
* 更新数据
*
* 该方法用于更新数据库中与给定ID或条件相符的记录它接受一个ID(可以是数组、整数或字符串),
* 一个包含要更新的数据的数组,和一个可选的键名称如果ID是一个数组,它将直接用作查询条件
* 否则,它将与主键或提供的键名称一起构建查询条件
*
* @param array|int|string $id 要更新的记录的ID,可以是数组、整数或字符串如果是数组,则视为查询条件
* @param array $data 要更新的数据,以数组形式提供
* @param string|null $key 可选的键名称,用于指定查询时使用的键如果未提供,则使用主键
* @return mixed 更新操作的结果,具体类型取决于底层数据库操作
*/
public function update(array|int|string $id, array $data, ?string $key = null): mixed
{
// 根据$id的类型和$key的值构建查询条件
if (is_array($id)) {
$where = $id;
} else {
$where = [is_null($key) ? $this->getPk() : $key => $id];
}
// 调用模型的update方法执行数据库更新操作
return $this->getModel()::update($data, $where);
}
/**
* @param $where
* @return array|mixed
*/
protected function setWhere($where, ?string $key = null): mixed
{
if (!is_array($where)) {
$where = [is_null($key) ? $this->getPk() : $key => $where];
}
return $where;
}
/**
* 批量更新记录
*
* 该方法用于根据一组ID批量更新数据库中的记录它允许指定更新的数据以及可选的键,
* 如果未提供键,则使用主键进行更新操作
*
* @param array $ids 需要更新的记录的ID数组
* @param array $data 要更新的数据,以键值对形式表示
* @param string|null $key 可选参数,用于指定更新操作使用的键如果未提供,则默认使用主键
* @return BaseModel 更新操作的结果,
*/
public function batchUpdate(array $ids, array $data, ?string $key = null): BaseModel
{
// 获取模型实例,并使用whereIn方法来匹配IDs,如果$key为null,则使用主键
// 这里使用了三元运算符来判断$key是否为null,从而决定使用哪个键进行查询
// 最后调用update方法来执行批量更新操作
return $this->getModel()->whereIn(is_null($key) ? $this->getPk() : $key, $ids)->update($data);
}
/**
* 保存数据到模型
*
* 该方法主要用于将给定的数据数组保存到与模型关联的数据库表中它首先获取模型的字段信息,
* 然后对数据进行预处理,包括设置一些特殊字段的默认值最后,使用处理后的数据创建一个新的模型实例
*
* @param array $data 要保存到模型的数据数组
*/
public function save(array $data): BaseModel
{
// 获取模型的字段信息
$fields = $this->getModelField();
// 对数据进行预处理,设置特殊字段的默认值
$data = $this->setSpecialFieldDefaultValue($data,$fields);
// 使用处理后的数据创建并返回新的模型实例
return $this->getModel()::create($data);
}
/**
* 批量保存数据
*
* @param array $data 要保存的数据数组,每个元素代表一条记录
* @param bool $replace 是否使用替换插入的方式,默认为true
*
* @return mixed 保存结果,具体取决于底层数据库操作的返回值
* @throws \Exception
*/
public function saveAll(array $data, bool $replace = true): mixed
{
// 获取模型字段信息,为后续数据处理做准备
$fields = $this->getModelField();
// 初始化用于保存处理后数据的数组
$saveData = [];
// 遍历传入的数据数组,对每条记录进行处理
foreach ($data as $v) {
// 设置特殊字段的默认值,并将处理后的数据添加到保存数组中
$saveData[] = $this->setSpecialFieldDefaultValue($v,$fields);
}
// 调用模型的saveAll方法,批量保存处理后的数据
return $this->getModel()->saveAll($saveData, $replace);
}
/**
* 批量插入数据
*
* 该方法主要用于批量插入数据到数据库中它首先获取模型的字段信息,然后对输入的数据进行处理,
* 设置一些特殊字段的默认值,最后将处理后的数据批量插入数据库
*
* @param array $data 要插入的数据数组,每个元素代表一行数据
* @return int 插入操作影响的行数
*/
public function insertAll(array $data): int
{
// 获取模型字段信息,用于后续处理数据时参考
$fields = $this->getModelField();
// 初始化一个空数组,用于存储处理后的数据
$saveData = [];
// 遍历输入的数据数组
foreach ($data as $v) {
// 对每行数据,设置特殊字段的默认值,并将处理后的数据添加到保存数组中
$saveData[] = $this->setSpecialFieldDefaultValue($v,$fields);
}
// 调用模型的批量插入方法,插入处理后的数据,并返回插入操作影响的行数
return $this->getModel()->insertAll($saveData);
}
private function setSpecialFieldDefaultValue($data, $fields)
{
// if(in_array('id',$fields)){
// $data['id'] = $data['id'] ?? IdUtils::uuid();
// }
// if(session(('user'))){
// if(in_array('create_by',$fields)){
// $data['create_by'] = $data['create_by'] ?? session('user')['uid'];
// }
// if(in_array('update_by',$fields)){
// $data['update_by'] = $data['update_by'] ?? session('user')['uid'];
// }
// }
$admininfo = RequestUtils::getAdminInfo();
if($admininfo){
if(in_array('create_by',$fields)){
$data['create_by'] = $data['create_by'] ?? $admininfo['id'];
}
if(in_array('update_by',$fields)){
$data['update_by'] = $data['update_by'] ?? $admininfo['id'];
}
}
if(in_array('create_time',$fields)){
$data['create_time'] = $data['create_time'] ?? time();
}
if(in_array('update_time',$fields)){
$data['update_time'] = $data['update_time'] ?? time();
}
return $data;
}
/**
* 获取搜索器和搜索条件key,以及不在搜索器的条件数组
* @param array $where
* @return array[]
* @throws ReflectionException
*/
private function getSearchData(array $where)
{
$with = [];
$otherWhere = [];
$responses = new \ReflectionClass($this->setModel());
foreach ($where as $key => $value) {
if($value === '' || $value === null){
continue;
}
$method = 'search' . Str::studly($key) . 'Attr';
if ($responses->hasMethod($method)) {
$with[] = $key;
} else {
if(!in_array($key,['user'])){
if (!is_array($value)) {
$otherWhere[] = [$key, '=', $value];
} else if (count($value) === 3) {
$otherWhere[] = $value;
}
}
}
}
return [$with, $otherWhere];
}
/**
* 根据搜索器获取搜索内容
* @param $where
* @param $search
* @return BaseModel
* @throws ReflectionException
*/
protected function withSearchSelect($where, $search)
{
[$with, $otherWhere] = $this->getSearchData($where);
return $this->getModel()->withSearch($with, $where)->when($search, function ($query) use ($otherWhere) {
$query->where($this->filterWhere($otherWhere));
});
}
/**
* 过滤数据表中不存在的where条件字段
* @param array $where
* @return array
*/
protected function filterWhere(array $where = []): array
{
$fields = $this->getModel()->getTableFields();
foreach ($where as $key => $item) {
// 带有表别名前缀的去掉前缀
$cols = explode('.', $item[0]);
$col = $cols[count($cols) - 1];
if (!in_array($col, $fields)) {
unset($where[$key]);
}
}
return $where;
}
/**
* 根据给定的条件搜索模型
*
* 当提供了一个非空的条件数组时,该方法会执行一个带有搜索条件的查询;
* 如果条件数组为空,则直接返回模型实例
*
* @param array $where 搜索条件数组,用于指定查询条件,默认为空数组
* @param bool $search 是否执行搜索的标志,用于控制是否应用搜索条件,默认为true
*
* @return mixed 返回一个BaseModel实例,具体类型取决于getModel和withSearchSelect方法的返回值
*/
public function search(array $where = [], bool $search = true): mixed
{
// 判断是否有搜索条件提供
if ($where) {
// 如果有搜索条件,则调用withSearchSelect方法执行带有搜索条件的查询
return $this->withSearchSelect($where, $search);
} else {
// 如果没有搜索条件,则直接返回模型实例
return $this->getModel();
}
}
/**
* 求和
* @param array $where
* @param string $field
* @param bool $search
* @return float
* @throws ReflectionException
*/
public function sum(array $where, string $field, bool $search = false): float
{
if ($search) {
return $this->search($where)->sum($field);
} else {
return $this->getModel()->where($where)->sum($field);
}
}
/**
* 高精度加法
* @param $key
* @param string $incField
* @param string $inc
* @param string|null $keyField
* @param int $acc
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function bcInc($key, string $incField, string $inc, string $keyField = null, int $acc = 2): bool
{
return $this->bc($key, $incField, $inc, $keyField, 1);
}
/**
* 高精度 减法
* @param $key
* @param string $decField
* @param string $dec
* @param string|null $keyField
* @param int $acc
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function bcDec($key, string $decField, string $dec, string $keyField = null, int $acc = 2): bool
{
return $this->bc($key, $decField, $dec, $keyField, 2);
}
/**
* 高精度计算并保存
* @param $key
* @param string $incField
* @param string $inc
* @param string|null $keyField
* @param int $type
* @param int $acc
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function bc($key, string $incField, string $inc, string $keyField = null, int $type = 1, int $acc = 2): bool
{
if ($keyField === null) {
$result = $this->get($key);
} else {
$result = $this->getOne([$keyField => $key]);
}
if (!$result) return false;
$new = 0;
if ($type === 1) {
$new = bcadd($result[$incField], $inc, $acc);
} else if ($type === 2) {
if ($result[$incField] < $inc) return false;
$new = bcsub($result[$incField], $inc, $acc);
}
$result->{$incField} = $new;
return false !== $result->save();
}
/**
* 获取条件数据中的某个值的最大值
* @param array $where
* @param string $field
* @return mixed
*/
public function getMax(array $where = [], string $field = ''): mixed
{
return $this->getModel()->where($where)->max($field);
}
/**
* 获取条件数据中的某个值的最小值
* @param array $where
* @param string $field
* @return mixed
*/
public function getMin(array $where = [], string $field = ''): mixed
{
return $this->getModel()->where($where)->min($field);
}
/**
* 软删除
* @param string $id
* @return mixed
*/
public function softDel(string $id): mixed
{
return $this->getModel()->where('id', $id)->update(['delete_time' => time()]);
}
/**
* 保存唯一验证
* @param array $where
* @param string $msg
*/
public function saveUnique(array $where, string $msg = '', $field = '*'): void
{
$res = $this->getModel()->field($field)->where($where)->findOrEmpty()->toArray();
if (!empty($res)) {
throw new ApiException($msg);
}
}
/**
* 更新唯一验证
* @param array $where
* @param int $id
* @param string $msg
*/
public function updateUnique(array $where, $id, string $msg = '', $field = '*'): void
{
$res = $this->getModel()->field($field)->where($where)->findOrEmpty()->toArray();
if (!empty($res) && $res['id'] != $id) {
throw new ApiException($msg);
}
}
public function getList($where, $field='*', $page=0, $limit=0, $order='id desc',$with=[]): array
{
$query = $this->search($where)->with($with)->field($field)->order($order);
if($page !== 0 && $limit !== 0){
$query = $query->page($page,$limit);
}
return $query->select()->toArray();
}
/**
* 批量插入或更新
* @param array $data
* @param string $duplicateKey
* @return bool
* @throws ApiException
*/
public function batchInsertOrUpdate(array $data,$duplicateKey = 'id'): bool
{
if(empty($data)){
return true;
}
// update on duplicate key
$fields = $this->getModel()->getTableFields();
// 获取所有字段
$userFields = array_keys($data[0]);
// 检查字段是否存在
foreach ($userFields as $field) {
if (!in_array($field, $fields)) {
throw new ApiException($field . '字段不存在',400 );
}
}
$fields = $userFields;
$table = $this->getModel()->getTable();
// 将数据转换为 SQL 语句中的值部分
$values = [];
foreach ($data as $row) {
$valueStr = '(' . implode(',', array_map(function ($value) {
return "'" . addslashes($value) . "'";
}, $row)) . ')';
$values[] = $valueStr;
}
// 构建更新部分的 SQL 语句
$updateStr = '';
foreach ($fields as $field) {
// 移除duplicateKey
if($field == $duplicateKey){
continue;
}
$updateStr .= "`$field` = VALUES(`$field`),";
}
// 构建字段部分的 SQL 语句
$fieldStr = implode(',', array_map(function ($field) {
return "`$field`";
}, $fields));
// 移除最后一个逗号
$updateStr = rtrim($updateStr, ',');
// 构建完整的 SQL 语句
$sql = "INSERT INTO {$table} ({$fieldStr}) VALUES " . implode(',', $values) . " ON DUPLICATE KEY UPDATE {$updateStr}";
// 使用 execute 方法执行 SQL 语句
return Db::execute($sql);
}
/**
* 根据条件更新数据
* @param array $where
* @param array $data
*/
public function updateByWhere(array $where, array $data)
{
return $this->getModel()->where($where)->update($data);
}
/**
* 获取模型字段列表
*
* 该方法用于获取当前模型所在表的所有字段名称
* 它通过查询数据库表的列信息来实现
*
* @return array 返回字段名称数组
*/
public function getModelField(): array
{
// 获取模型对应的表名
$table = $this->getModel()->getTable();
// 执行SQL语句,获取表中所有列的详细信息
$fields = Db::query("SHOW FULL COLUMNS FROM $table");
// 从查询结果中提取所有字段名,并返回
return array_column($fields, 'Field');
}
public function __call(string $name, array $arguments)
{
return call_user_func_array([$this->getModel(), $name], $arguments);
}
}

27
plugin/piadmin/app/base/BaseModel.php

@ -0,0 +1,27 @@
<?php
namespace plugin\piadmin\app\base;
use think\Model;
/**
* 基础模型类,继承自Model,用于定义通用的模型属性和方法
*/
abstract class BaseModel extends Model
{
protected $pk = 'id';
protected $autoWriteTimestamp = 'int'; // 开启自动写入时间戳字段
// public function getCreateTimeAttr($value): string
// {
// return date('Y-m-d H:i:s', $value);
// }
//
// public function getUpdateTimeAttr($value): string
// {
// return date('Y-m-d H:i:s', $value);
// }
}

62
plugin/piadmin/app/base/BaseService.php

@ -0,0 +1,62 @@
<?php
namespace plugin\piadmin\app\base;
use think\Model;
/**
* Class BaseService
* 该类为基本服务类,提供了数据操作的通用方法
* 主要包括数据的增、删、查、改等操作,以及一些特殊处理如软删除和高精度运算
* 通过该类及其子类,可以方便地对数据库进行各种操作,而无需重复编写SQL语句
* 该类使用了魔术方法__call来动态处理方法调用,通过方法名前缀来区分不同的操作类型
*
* 注意:该类的设计基于简化数据操作和提高代码复用性的原则,因此在使用时需要根据具体的业务需求来继承和扩展
*
* @method updateUnique(array $where, int $id, string $msg = '', $field = '*') 修改数据唯一
* @method saveUnique(array $where, string $msg = '', $field = '*') 新增数据唯一
* @method mixed softDel($id) 软删除
* @method array|Model|null get($id, ?array $field = []) 获取一条数据
* @method array|Model|null getOne(array $where, ?string $field = '*') 获取一条数据(不走搜素器)
* @method string|null batchUpdate(array $ids, array $data, ?string $key = null) 批量修改
* @method float sum(array $where, string $field, bool $search = false) 求和
* @method mixed update($id, array $data, ?string $field = '') 修改数据
* @method bool be($map, string $field = '') 查询一条数据是否存在
* @method mixed value(array $where, string $field) 获取指定条件下的数据
* @method int count(array $where = []) 读取数据条数
* @method int getCount(array $where = []) 获取某些条件总数(不走搜素器)
* @method array getColumn(array $where, string $field, string $key = '') 获取某个字段数组(不走搜素器)
* @method mixed delete($id, ?string $key = null) 删除
* @method mixed save(array $data) 保存数据
* @method mixed saveAll(array $data, bool $replace = false) 批量保存数据
* @method mixed insertAll(array $data) 批量保存数据
* @method Model selectList(array $where, string $field = '*', int $page = 0, int $limit = 0, string $order = '', array $with = [], bool $search = false) 获取列表
* @method bool bcInc($key, string $incField, string $inc, string $keyField = null, int $acc = 2) 高精度加法
* @method bool bcDec($key, string $decField, string $dec, string $keyField = null, int $acc = 2) 高精度 减法
*/
abstract class BaseService
{
/**
* 模型注入
* @var object
*/
protected $dao;
/**
* 动态调用Dao层方法
*
* 当尝试调用不存在于当前类的方法时,本函数会被调用此函数旨在转发请求到Dao层,
* 使得当前类可以动态地调用Dao层的方法,而无需在当前类中显式定义每个方法
*
* @param string $name 调用的方法名
* @param array $arguments 方法的参数数组
*
* @return mixed 从Dao层方法调用返回的数据
*/
public function __call(string $name, array $arguments)
{
return call_user_func_array([$this->dao, $name], $arguments);
}
}

16
plugin/piadmin/app/base/BaseValidate.php

@ -0,0 +1,16 @@
<?php
namespace plugin\piadmin\app\base;
/**
* 验证器基类
*
* 继承think\Validate类 其他自定义验证器类必须继承该类
* 用于实现自定义验证方法
*/
abstract class BaseValidate extends \think\Validate
{
protected $regex = [
'domain' => '^(?:(?:https?|ftp):\/\/)?(?:www\.)?[a-zA-Z0-9-]{1,63}(?:\.[a-zA-Z0-9-]{1,63})+(?::\d{1,5})?(?:\/[^\s?#]*)?(?:\?[^#\s]*)?(?:#[^\s]*)?$'
];
}

53
plugin/piadmin/app/common/ContainerProxy.php

@ -0,0 +1,53 @@
<?php
namespace plugin\piadmin\app\common;
use DI\Container;
use support\Container as WebmanContainer;
class ContainerProxy
{
/**
* 获取容器实例
* @return static
*/
public static function instance()
{
return new static();
}
/**
* 创建实例(兼容TP语法)
* @template T
* @param class-string<T> $abstract
* @param array $parameters
* @return T
*/
public function make($abstract, array $parameters = [])
{
return WebmanContainer::get($abstract, $parameters);
}
/**
* 获取实例(兼容TP语法)
* @template T
* @param class-string<T> $abstract
* @return T
*/
public function get($abstract)
{
return WebmanContainer::get($abstract);
}
// 添加其他ThinkPHP兼容方法...
public function bind($abstract, $concrete = [])
{
WebmanContainer::make($abstract, $concrete);
return $this;
}
public function has($abstract)
{
return WebmanContainer::has($abstract);
}
}

51
plugin/piadmin/app/controller/v1/AttachmentController.php

@ -0,0 +1,51 @@
<?php
namespace plugin\piadmin\app\controller\v1;
use plugin\piadmin\app\base\BaseController;
use plugin\piadmin\app\service\AttachmentService;
use Tinywan\Storage\Storage;
class AttachmentController extends BaseController
{
protected $service;
public function __construct(AttachmentService $service)
{
parent::__construct();
$this->service = $service;
}
/**
* @desc 文件上传
* @author zhangf@suq.cn
* @date 2025.9.1
* @return \support\Response
*/
public function upload()
{
$file = request()->file('file');
return success($this->service->upload($file));
}
/**
* @desc 文件列表
* @author zhangf@suq.cn
* @date 2025.9.1
* @return \support\Response
*/
public function index()
{
$params = requestOnly([
'original_name' => ''
]);
return success($this->service->listData($params));
}
public function delete()
{
$id = input('id');
return success($this->service->deleteData($id));
}
}

16
plugin/piadmin/app/controller/v1/DataDictionaryController.php

@ -0,0 +1,16 @@
<?php
namespace plugin\piadmin\app\controller\v1;
use plugin\piadmin\app\base\BaseController;
use plugin\piadmin\app\service\DataDictionaryService;
class DataDictionaryController extends BaseController
{
public function dictionary(DataDictionaryService $service)
{
return success($service->dictionary());
}
}

37
plugin/piadmin/app/controller/v1/IndexController.php

@ -0,0 +1,37 @@
<?php
namespace plugin\piadmin\app\controller\v1;
use plugin\piadmin\app\base\BaseController;
use plugin\piadmin\app\dao\SystemAdminDao;
use plugin\piadmin\app\dao\SystemMenuDao;
use plugin\piadmin\app\dao\SystemRoleDao;
use plugin\piadmin\app\utils\CacheHelper;
use plugin\piadmin\app\validate\LoginValidate;
use support\Cache;
use support\Request;
class IndexController extends BaseController
{
// 无需登录方法
protected $noNeedLogin = ['test'];
public function __construct()
{
parent::__construct();
}
// 测试登录成功
public function index(Request $request)
{
return success(['admin'=>$request->admin]);
}
public function test(Request $request)
{
$c = Cache::store();
var_dump($c);
}
}

50
plugin/piadmin/app/controller/v1/LoginController.php

@ -0,0 +1,50 @@
<?php
namespace plugin\piadmin\app\controller\v1;
use plugin\piadmin\app\base\BaseController;
use plugin\piadmin\app\dao\SystemAdminDao;
use plugin\piadmin\app\dao\SystemMenuDao;
use plugin\piadmin\app\dao\SystemRoleDao;
use plugin\piadmin\app\service\SystemAdminService;
use plugin\piadmin\app\utils\CacheHelper;
use plugin\piadmin\app\utils\Captcha;
use plugin\piadmin\app\validate\LoginValidate;
use support\Container;
use support\Request;
class LoginController extends BaseController
{
// 无需登录方法
protected $noNeedLogin = ['login','captcha'];
public function __construct()
{
parent::__construct();
$this->service = app()->make(SystemAdminService::class);
}
public function login(Request $request){
$params = $request->only([
'username',
'password'
]);
validate(LoginValidate::class)->scene('loginByPassword')->check($params);
// 用户登录
$res = $this->service->login($params);
return success($res);
}
public function captcha(){
$captcha = Captcha::imageCaptcha();
return success($captcha);
}
}

96
plugin/piadmin/app/controller/v1/MenuController.php

@ -0,0 +1,96 @@
<?php
namespace plugin\piadmin\app\controller\v1;
use plugin\piadmin\app\base\BaseController;
use plugin\piadmin\app\service\MenuService;
use plugin\piadmin\app\validate\MenuValidate;
use support\Request;
class MenuController extends BaseController
{
// 无需登录方法
protected $noNeedLogin = [];
public function __construct()
{
parent::__construct();
$this->service = app()->make(MenuService::class);
}
public function index(Request $request)
{
$all = $request->all();
$list = $this->service->getMenuList($all);
return success($list);
}
public function save(Request $request)
{
$params = requestOnly([
'pid' => 0,
'name' => '',
'code' => '',
'icon' => '',
'route' => '',
'component' => '',
'redirect' => '',
'is_hidden' => 2,
'is_layout' => 2,
'type' => '',
'status' => 1,
'sort' => 1000,
'remark' => ''
]);
validate(MenuValidate::class)->scene('save')->check($params);
$res = $this->service->saveData($params);
return success($res);
}
public function update(Request $request)
{
$params = requestOnly([
'id' => '',
'pid' => 0,
'name' => '',
'code' => '',
'icon' => '',
'route' => '',
'component' => '',
'redirect' => '',
'is_hidden' => 2,
'is_layout' => 2,
'type' => '',
'status' => 1,
'sort' => 1000,
'remark' => ''
]);
validate(MenuValidate::class)->scene('update')->check($params);
$res = $this->service->updateData($params);
return success($res);
}
public function delete(Request $request)
{
$ids = $request->input('ids');
$res = $this->service->deleteData($ids);
return success($res);
}
public function read(Request $request)
{
$id = $request->input('id');
$res = $this->service->readData($id);
return success($res);
}
public function getMenuPermissions(MenuService $service)
{
return success($service->getUserMenu());
}
}

83
plugin/piadmin/app/controller/v1/SystemAdminController.php

@ -0,0 +1,83 @@
<?php
namespace plugin\piadmin\app\controller\v1;
use plugin\piadmin\app\base\BaseController;
use plugin\piadmin\app\service\SystemAdminService;
use plugin\piadmin\app\utils\ArrayUtils;
use plugin\piadmin\app\validate\SystemAdminValidate;
use support\Response;
class SystemAdminController extends BaseController
{
public function save(SystemAdminService $service): Response
{
$params = requestOnly([
'name' => '',
'account' => '',
'dept_id' => '',
'password' => '',
'phone' => '',
'email' => '',
'status' => 1,
'gender' => 3,
'role_ids' => []
]);
validate(SystemAdminValidate::class)->check($params, 'save');
return success($service->saveData($params));
}
public function update(SystemAdminService $service): Response
{
$params = requestOnly([
'id' => '',
'name' => '',
'account' => '',
'dept_id' => '',
'password' => '',
'phone' => '',
'email' => '',
'status' => 1,
'gender' => 3,
'role_ids' => []
]);
validate(SystemAdminValidate::class)->check($params, 'update');
return success($service->updateData(ArrayUtils::filterNotEmpty($params)));
}
public function index(SystemAdminService $service): Response
{
$params = requestOnly([
'name' => '',
'email' => '',
'phone' => '',
'status' => '',
'dept_id' => ''
]);
return success($service->listData($params));
}
public function read(SystemAdminService $service): Response
{
$id = input('id');
return success($service->readData($id));
}
public function delete(SystemAdminService $service): Response
{
$id = input('id');
return success($service->deleteData($id));
}
public function ban(SystemAdminService $service)
{
$params = requestOnly([
'id' => '',
]);
return success($service->banAdmin($params));
}
}

76
plugin/piadmin/app/controller/v1/SystemDeptController.php

@ -0,0 +1,76 @@
<?php
namespace plugin\piadmin\app\controller\v1;
use plugin\piadmin\app\base\BaseController;
use plugin\piadmin\app\service\SystemDeptService;
use plugin\piadmin\app\utils\ArrayUtils;
use plugin\piadmin\app\validate\SystemDeptValidate;
use support\Response;
class SystemDeptController extends BaseController
{
public function save(SystemDeptService $service): Response
{
$params = requestOnly([
'pid' => 0,
'name' => '',
'sort' => 1000,
'leader' => '',
'phone' => '',
'email' => '',
'status' => 1
]);
validate(SystemDeptValidate::class)->check($params, 'save');
return success($service->saveData($params));
}
public function update(SystemDeptService $service): Response
{
$params = requestOnly([
'id' => '',
'pid' => 0,
'name' => '',
'sort' => 1000,
'leader' => '',
'phone' => '',
'email' => '',
'status' => 1
]);
validate(SystemDeptValidate::class)->check($params, 'update');
return success($service->updateData(ArrayUtils::filterNotEmpty($params)));
}
public function index(SystemDeptService $service): Response
{
$params = requestOnly([
'name' => '',
'status' => ''
]);
return success($service->listData($params));
}
public function read(SystemDeptService $service): Response
{
$id = input('id');
return success($service->readData($id));
}
public function delete(SystemDeptService $service): Response
{
$ids = input('ids');
return success($service->deleteData($ids));
}
public function pureIndex(SystemDeptService $service): Response
{
$params = requestOnly([
'name' => ''
]);
return success($service->listPureData($params));
}
}

99
plugin/piadmin/app/controller/v1/SystemRoleController.php

@ -0,0 +1,99 @@
<?php
namespace plugin\piadmin\app\controller\v1;
use plugin\piadmin\app\base\BaseController;
use plugin\piadmin\app\service\SystemRoleService;
use plugin\piadmin\app\utils\ArrayUtils;
use plugin\piadmin\app\validate\SystemRoleValidate;
use support\Response;
class SystemRoleController extends BaseController
{
/**
* 新增角色
* @return Response
*/
public function save(SystemRoleService $service): Response
{
$params = requestOnly([
'name' => '',
'code' => '',
'status' => 1,
'sort' => 1000,
'remark' => '',
'menu_ids' => []
]);
validate(SystemRoleValidate::class)->check($params, 'save');
return success($service->saveData($params));
}
/**
* 更新角色
* @return Response
*/
public function update(SystemRoleService $service): Response
{
$params = requestOnly([
'id' => '',
'name' => '',
'code' => '',
'status' => 1,
'sort' => 1000,
'remark' => '',
'menu_ids' => []
]);
validate(SystemRoleValidate::class)->check($params, 'update');
return success($service->updateData(ArrayUtils::filterNotEmpty($params)));
}
/**
* 角色列表
* @param SystemRoleService $service
* @return Response
*/
public function index(SystemRoleService $service): Response
{
$params = requestOnly([
'name' => '',
'code' => '',
'status' => ''
]);
return success($service->listData($params));
}
/**
* 角色详情
* @param SystemRoleService $service
* @return Response
*/
public function read(SystemRoleService $service): Response
{
$id = input('id');
return success($service->readData($id));
}
/**
* 删除角色
* @param SystemRoleService $service
* @return Response
*/
public function delete(SystemRoleService $service): Response
{
$id = input('id');
return success($service->deleteData($id));
}
/**
* 简单角色列表
* @param SystemRoleService $service
* @return Response
*/
public function pureIndex(SystemRoleService $service): Response
{
$params = requestOnly([
'name' => ''
]);
return success($service->listPureData($params));
}
}

16
plugin/piadmin/app/dao/AttachmenatDao.php

@ -0,0 +1,16 @@
<?php
namespace plugin\piadmin\app\dao;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\model\Attachment;
class AttachmenatDao extends BaseDao
{
protected function setModel(): string
{
return Attachment::class;
}
}

46
plugin/piadmin/app/dao/InformationSchemaDao.php

@ -0,0 +1,46 @@
<?php
namespace plugin\piadmin\app\dao;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\model\InformationSchema;
class InformationSchemaDao extends BaseDao
{
protected function setModel(): string
{
return InformationSchema::class;
}
public function getInfomation()
{
$tableModel = new InformationSchema();
$tableModel->setTable('information_schema.tables');
$tables = $tableModel->field('table_name, table_comment')
->where('table_schema', config('think-orm.connections.mysql.database'))
->where('table_type', 'BASE TABLE')
->order('table_name')
->select();
$columnModel = new InformationSchema();
$columnModel->setTable('information_schema.columns');
$tableList = [];
foreach ($tables as $table) {
// 获取表字段信息
$columns = $columnModel
->field('column_name,column_type,is_nullable,column_default,column_comment,column_key')
->where('table_schema', config('think-orm.connections.mysql.database'))
->where('table_name', $table->table_name)
->order('ordinal_position')
->select();
$tableList[] = [
'name' => $table->table_name,
'comment' => $table->table_comment ?: '无注释',
'columns' => $columns
];
}
return $tableList;
}
}

19
plugin/piadmin/app/dao/SystemAdminDao.php

@ -0,0 +1,19 @@
<?php
namespace plugin\piadmin\app\dao;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\model\SystemAdmin;
use plugin\piadmin\app\model\SystemMenu;
use plugin\piadmin\app\model\SystemRole;
class SystemAdminDao extends BaseDao
{
protected function setModel(): string
{
return SystemAdmin::class;
}
}

15
plugin/piadmin/app/dao/SystemAdminDeptDao.php

@ -0,0 +1,15 @@
<?php
namespace plugin\piadmin\app\dao;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\model\SystemAdminDept;
class SystemAdminDeptDao extends BaseDao
{
protected function setModel(): string
{
return SystemAdminDept::class;
}
}

15
plugin/piadmin/app/dao/SystemAdminRoleDao.php

@ -0,0 +1,15 @@
<?php
namespace plugin\piadmin\app\dao;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\model\SystemAdminRole;
class SystemAdminRoleDao extends BaseDao
{
protected function setModel(): string
{
return SystemAdminRole::class;
}
}

15
plugin/piadmin/app/dao/SystemDeptDao.php

@ -0,0 +1,15 @@
<?php
namespace plugin\piadmin\app\dao;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\model\SystemDept;
class SystemDeptDao extends BaseDao
{
protected function setModel(): string
{
return SystemDept::class;
}
}

17
plugin/piadmin/app/dao/SystemMenuDao.php

@ -0,0 +1,17 @@
<?php
namespace plugin\piadmin\app\dao;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\model\SystemMenu;
class SystemMenuDao extends BaseDao
{
protected function setModel(): string
{
return SystemMenu::class;
}
}

18
plugin/piadmin/app/dao/SystemRoleDao.php

@ -0,0 +1,18 @@
<?php
namespace plugin\piadmin\app\dao;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\model\SystemMenu;
use plugin\piadmin\app\model\SystemRole;
class SystemRoleDao extends BaseDao
{
protected function setModel(): string
{
return SystemRole::class;
}
}

19
plugin/piadmin/app/dao/SystemRoleMenuDao.php

@ -0,0 +1,19 @@
<?php
namespace plugin\piadmin\app\dao;
use plugin\piadmin\app\base\BaseDao;
use plugin\piadmin\app\model\SystemMenu;
use plugin\piadmin\app\model\SystemRole;
use plugin\piadmin\app\model\SystemRoleMenu;
class SystemRoleMenuDao extends BaseDao
{
protected function setModel(): string
{
return SystemRoleMenu::class;
}
}

17
plugin/piadmin/app/exception/ApiException.php

@ -0,0 +1,17 @@
<?php
namespace plugin\piadmin\app\exception;
use Webman\Http\Request;
use Webman\Http\Response;
use support\exception\BusinessException;
/**
* 常规异常
*/
class ApiException extends BusinessException
{
public function render(Request $request): ?Response
{
return json(['code' => $this->getCode() ?: 500, 'msg' => $this->getMessage()]);
}
}

17
plugin/piadmin/app/exception/FrameworkException.php

@ -0,0 +1,17 @@
<?php
namespace plugin\piadmin\app\exception;
use Webman\Http\Request;
use Webman\Http\Response;
use support\exception\BusinessException;
/**
* 框架内部异常
*/
class FrameworkException extends BusinessException
{
public function render(Request $request): ?Response
{
return json(['code' => $this->getCode() ?: 500, 'msg' => $this->getMessage()]);
}
}

162
plugin/piadmin/app/functions.php

@ -0,0 +1,162 @@
<?php
use plugin\piadmin\app\common\ContainerProxy;
use plugin\piadmin\app\exception\ApiException;
/**
* Here is your custom functions.
*/
if (!function_exists('app')) {
/**
* 获取对象容器实例
* @return ContainerProxy
*/
function app()
{
return ContainerProxy::instance();
}
}
if (!function_exists('errorException')) {
// 抛出异常
function errorException($msg, $code = 500)
{
throw new ApiException($msg, $code);
}
}
if (!function_exists('getPiAdminVersion')) {
/**
* 获取版本号
* @return mixed|string
*/
function getPiAdminVersion()
{
return config('plugin.piadmin.app.version') ?? '0.0.0';
}
}
if (!function_exists('success')) {
/**
* api返回成功
* @param $data
* @param $msg
* @param $other_result
* @param $code
* @return \support\Response
*/
function success($data = '', $msg = 'ok', $other_result = [], $code = 0): \support\Response
{
if (empty($msg) && is_string($data)) {
$msg = $data;
}
$result = [
'code' => $code,
'msg' => $msg,
'data' => $data,
];
$result = array_merge($result, $other_result);
return json($result);
}
}
if (!function_exists('error')) {
/**
* api返回错误
* @param $data
* @param $msg
* @param $other_result
* @param $code
* @return \support\Response
*/
function error($data = '', $msg = 'fail', $other_result = [], $code = 422): \support\Response
{
if (empty($msg) && is_string($data)) {
$msg = $data;
}
$result = [
'code' => $code,
'msg' => $msg,
'data' => $data,
];
$result = array_merge($result, $other_result);
return json($result);
}
}
if (!function_exists('isNotBlank')) {
/**
* 判非空
* @param $var
* @return bool
*/
function isNotBlank($var): bool
{
return $var !== '' && $var !== null && $var !== false && $var !== [];
}
}
if (!function_exists('isBlank')) {
/**
* 判空
* @param $var
* @return bool
*/
function isBlank($var): bool
{
return !isNotBlank($var);
}
}
if (!function_exists('getColumnForKeyArray')) {
function getColumnForKeyArray(array $data, string $key = 'id'): array
{
$item = [];
foreach ($data as $val) {
$keys = explode(',', $key);
$tmpKey = '';
foreach ($keys as $kv) {
if (!array_key_exists($kv, $val)) {
break;
}
$tmpKey .= $val[$kv] ?? null;
}
$item[$tmpKey] = $val;
}
return $item;
}
}
if (!function_exists('requestOnly')) {
/**
* 获取某些请求参数 仿tp request()->only()方法
* @param array $inputParams
* @return array
*/
function requestOnly(array $inputParams): array
{
$paramsKeys = array_keys($inputParams);
$data = request()->only($paramsKeys);
$result = [];
foreach ($paramsKeys as $key) {
if (!array_key_exists($key, $data) || empty($data[$key])) {
$result[$key] = $inputParams[$key];
} else {
$result[$key] = $data[$key];
}
}
return $result;
}
}

81
plugin/piadmin/app/middleware/AdminAuthorizationMiddleware.php

@ -0,0 +1,81 @@
<?php
namespace plugin\piadmin\app\middleware;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use plugin\piadmin\app\dao\SystemAdminDao;
use plugin\piadmin\app\exception\ApiException;
use plugin\piadmin\app\utils\CacheUtils;
use support\Context;
use Tinywan\Jwt\JwtToken;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
class AdminAuthorizationMiddleware implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
$authHeader = config('plugin.piadmin.piadmin.jwt.admin.auth_header');
$token = $request->header($authHeader, '');
if (empty($token)) {
throw new ApiException(403);
}
$token = trim(str_replace('Bearer ', '', $token));
// 解token payload
$key = config("plugin.piadmin.piadmin.jwt.admin.key");
$keyId = config("plugin.piadmin.piadmin.jwt.admin.key_id");
$payload = JWT::decode($token, new Key($key, 'HS256'));
if (empty($token)) {
throw new ApiException(403);
}
// 缓存
$tokenCacheData = CacheUtils::get('admin.token.'. md5($token));
if (empty($tokenCacheData)) {
throw new ApiException(403);
}
$tokenCacheData = json_decode($tokenCacheData, true);
if (empty($tokenCacheData) || !isset($tokenCacheData['uid'])) {
throw new ApiException(403);
}
// 验证账号状态
$uid = $tokenCacheData['uid'];
$adminDao = app()->make(SystemAdminDao::class);
$admin = $adminDao->getOne(['id' => $uid]);
if (empty($admin)) {
throw new ApiException(403);
}
if ($admin['status'] != 1 ) {
throw new ApiException(400006);
}
$adminInfo = $admin->toArray();
$adminInfo['is_login'] = true;
$adminInfo['uid'] = $adminInfo['id'];
$adminInfo['token'] = $token;
// 检查版本
$this->checkVersionKey($uid, $token);
$request->admin = $adminInfo;
// todo 补全session
Context::set('admin', $adminInfo);
// session('user', $userInfo);
return $handler($request);
}
/**
* 版本检查
* @param string|int $uid
* @param string $token
* @return void
* @throws ApiException
*/
private function checkVersionKey(string|int $uid, string $token): void
{
$userVersionKey = "admin.version.{$uid}";
$version = CacheUtils::get($userVersionKey);
if ($version != getPiAdminVersion()) {
CacheUtils::delete(md5($token));
throw new ApiException(403);
}
}
}

28
plugin/piadmin/app/middleware/CrossDomainMiddleware.php

@ -0,0 +1,28 @@
<?php
namespace plugin\piadmin\app\middleware;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
/**
* 跨域中间件
*/
class CrossDomainMiddleware implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
// 如果是options请求则返回一个空响应,否则继续向洋葱芯穿越,并得到一个响应
$response = $request->method() == 'OPTIONS' ? response('') : $handler($request);
// 给响应添加跨域相关的http头
$response->withHeaders([
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Allow-Origin' => $request->header('origin', '*'),
'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'),
'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'),
]);
return $response;
}
}

26
plugin/piadmin/app/middleware/LanguageMiddleware.php

@ -0,0 +1,26 @@
<?php
namespace plugin\piadmin\app\middleware;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
/**
* 多语言处理中间件
*/
class LanguageMiddleware implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
$lang = $request->header('PIADMIN_LANG','zh_CN');
$fallback_locale = config('plugin.piadmin.translation.fallback_locale');
if(in_array($lang,$fallback_locale)){
locale($lang);
}
return $handler($request);
}
}

145
plugin/piadmin/app/middleware/ParameterConvertMiddleware.php

@ -0,0 +1,145 @@
<?php
namespace plugin\piadmin\app\middleware;
use Webman\Http\Response;
use Webman\Http\Request;
/**
* 参数格式处理中间件
*/
class ParameterConvertMiddleware
{
public function process(Request $request, callable $handler): Response
{
// 1. 请求处理前:转换传入参数为小写下划线格式
$this->convertRequestKeysToSnakeCase($request);
// 继续传递请求,获取响应
$response = $handler($request);
// 2. 响应返回前:转换返回数据为小驼峰格式
$this->convertResponseKeysToCamelCase($response);
return $response;
}
/**
* 将请求中的所有参数键名转换为小写下划线格式
* 支持 GET、POST JSON 请求
*/
protected function convertRequestKeysToSnakeCase(Request $request): void
{
// 转换 GET 参数
$getParams = $request->get();
if (!empty($getParams)) {
$convertedGet = $this->convertArrayKeysToSnakeCase($getParams);
$request->setGet($convertedGet);
}
// 转换 POST 参数 (application/x-www-form-urlencoded 或 multipart/form-data)
$postParams = $request->post();
if (!empty($postParams)) {
$convertedPost = $this->convertArrayKeysToSnakeCase($postParams);
$request->setGet($convertedPost);
}
// 处理 JSON 请求体 (application/json)
$contentType = $request->header('content-type', '');
if (strpos($contentType, 'application/json') !== false) {
$rawBody = $request->rawBody();
if (!empty($rawBody)) {
$jsonData = json_decode($rawBody, true);
if (is_array($jsonData) && !empty($jsonData)) {
$convertedJson = $this->convertArrayKeysToSnakeCase($jsonData);
// 替换请求的原始body和post数据
$request->setPost(json_encode($convertedJson));
// 同时更新 post 数据,因为后续可能通过 post() 方法获取
// $request->request->replace($convertedJson);
}
}
}
}
/**
* 将响应内容中的键名转换为小驼峰格式
* 支持 JSON 响应
*/
protected function convertResponseKeysToCamelCase(Response $response): void
{
$contentType = $response->getHeader('Content-Type');
if (is_array($contentType)) {
$contentType = implode('; ', $contentType);
}
// 只处理 JSON 响应
if (strpos($contentType, 'application/json') !== false) {
$originalContent = $response->rawBody();
if (!empty($originalContent)) {
$data = json_decode($originalContent, true);
if (is_array($data) && !empty($data)) {
$convertedData = $this->convertArrayKeysToCamelCase($data);
$response->withBody(json_encode($convertedData));
}
}
}
}
/**
* 递归地将数组的键转换为小写下划线格式 (snake_case)
*/
protected function convertArrayKeysToSnakeCase(array $data): array
{
$result = [];
foreach ($data as $key => $value) {
// 转换键名
$newKey = $this->camelCaseToSnakeCase($key);
// 递归处理数组值
if (is_array($value)) {
$value = $this->convertArrayKeysToSnakeCase($value);
}
$result[$newKey] = $value;
}
return $result;
}
/**
* 递归地将数组的键转换为小驼峰格式 (camelCase)
*/
protected function convertArrayKeysToCamelCase(array $data): array
{
$result = [];
foreach ($data as $key => $value) {
// 转换键名
$newKey = $this->snakeCaseToCamelCase($key);
// 递归处理数组值
if (is_array($value)) {
$value = $this->convertArrayKeysToCamelCase($value);
}
$result[$newKey] = $value;
}
return $result;
}
/**
* 将驼峰命名字符串转换为下划线命名
* 例如: helloWorld -> hello_world, UserID -> user_id
*/
protected function camelCaseToSnakeCase(string $input): string
{
return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $input));
}
/**
* 将下划线命名字符串转换为小驼峰命名
* 例如: hello_world -> helloWorld, user_id -> userId
*/
protected function snakeCaseToCamelCase(string $input): string
{
return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input))));
}
}

40
plugin/piadmin/app/middleware/PermissionsMiddleware.php

@ -0,0 +1,40 @@
<?php
namespace plugin\piadmin\app\middleware;
use plugin\piadmin\app\exception\ApiException;
use plugin\piadmin\app\model\SystemMenu;
use plugin\piadmin\app\utils\AdminTokenUtils;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
class PermissionsMiddleware implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
//调试模式,不校验权限
if (env('APP_DEBUG')) {
return $handler($request);
}
$requestRoute = $request->route->param('perm')[0];
//接口白名单
$white_list = [
'login',
'captcha',
'getMenuPermissions',
];
if (in_array($requestRoute, $white_list)) {
return $handler($request);
}
$menu_ids = AdminTokenUtils::getMenus();
$menuModel = new SystemMenu();
$routes = $menuModel->whereIn('id', $menu_ids)->field('route')->select()->toArray();
//权限路由
$routes = array_column($routes, 'route');
if (!in_array($requestRoute, $routes)) {
throw new ApiException('没有权限', 401);
}
return $handler($request);
}
}

44
plugin/piadmin/app/model/Attachment.php

@ -0,0 +1,44 @@
<?php
namespace plugin\piadmin\app\model;
use plugin\piadmin\app\base\BaseModel;
use think\model\concern\SoftDelete;
use Tinywan\Storage\Storage;
class Attachment extends BaseModel
{
protected $table = 'pi_attachment';
protected $autoWriteTimestamp = true;
/**
* @desc 关联创建者ID
* @author zhangf@suq.cn
* @date 2025.9.1
* @return \think\model\relation\BelongsTo
*/
public function creator()
{
return $this->belongsTo(SystemAdmin::class, 'create_by', 'id');
}
/**
* @desc 上传方式格式化
* @author zhangf@suq.cn
* @date 2025.9.1
*/
public static function getStorage()
{
// local:本地 oss:阿里云 cos:腾讯云 qos:七牛云
$storage = Storage::getConfig('default');
$storages = [
'local' => 1,
'qos' => 2,
'cos' => 3,
'oss' => 4,
];
return $storages[$storage];
}
}

20
plugin/piadmin/app/model/InformationSchema.php

@ -0,0 +1,20 @@
<?php
namespace plugin\piadmin\app\model;
use plugin\piadmin\app\base\BaseModel;
class InformationSchema extends BaseModel
{
protected $table = 'information_schema';
protected $autoWriteTimestamp = true;
/**
* @param string $table
*/
public function setTable(string $table): void
{
$this->table = $table;
}
}

45
plugin/piadmin/app/model/SystemAdmin.php

@ -0,0 +1,45 @@
<?php
namespace plugin\piadmin\app\model;
use plugin\piadmin\app\base\BaseModel;
use think\model\concern\SoftDelete;
class SystemAdmin extends BaseModel
{
protected $table = 'pi_system_admin';
protected $hidden = ['password'];
use SoftDelete;
protected string $deleteTime = 'delete_time';
protected $defaultSoftDelete = 0;
protected $append = [
'role_name'
];
public function getRoleNameAttr($value, $data)
{
$id = $data['id'];
$systemAdminRole = new SystemAdminRole();
$role_ids = $systemAdminRole->where('admin_id', $id)->column('role_id');
$systemRole = new SystemRole();
$data = $systemRole->whereIn('id', $role_ids)->column('name');
return implode(',', $data);
}
// 定义管理员与角色的多对多关联
public function roles()
{
return $this->belongsToMany(SystemRole::class, SystemAdminRole::class,'role_guid','admin_guid');
}
// 部门信息
public function dept()
{
return $this->hasOne(SystemDept::class,'id','dept_id')->bind(['dept_name' => 'name']);
}
}

11
plugin/piadmin/app/model/SystemAdminDept.php

@ -0,0 +1,11 @@
<?php
namespace plugin\piadmin\app\model;
use plugin\piadmin\app\base\BaseModel;
class SystemAdminDept extends BaseModel
{
protected $table = 'pi_system_admin_dept';
}

27
plugin/piadmin/app/model/SystemAdminRole.php

@ -0,0 +1,27 @@
<?php
namespace plugin\piadmin\app\model;
use plugin\piadmin\app\base\BaseModel;
class SystemAdminRole extends BaseModel
{
protected $table = 'pi_system_admin_role';
protected $autoWriteTimestamp = 'int';
const SUPER_ADMIN_ID = 1;
// 定义中间表与管理员的一对一关联
public function admin()
{
return $this->belongsTo(SystemAdmin::class);
}
// 定义中间表与角色的一对一关联
public function role()
{
return $this->belongsTo(SystemRole::class);
}
}

26
plugin/piadmin/app/model/SystemDept.php

@ -0,0 +1,26 @@
<?php
namespace plugin\piadmin\app\model;
use app\model\DistillationWord;
use plugin\piadmin\app\base\BaseModel;
use think\model\concern\SoftDelete;
class SystemDept extends BaseModel
{
protected $table = 'pi_system_dept';
use SoftDelete;
protected string $deleteTime = 'delete_time';
protected $defaultSoftDelete = 0;
protected $append = [
'leader_name'
];
public function getLeaderNameAttr($value, $data)
{
return SystemAdmin::where('id', $data['leader'])->value('name') ?? '';
}
}

16
plugin/piadmin/app/model/SystemMenu.php

@ -0,0 +1,16 @@
<?php
namespace plugin\piadmin\app\model;
use plugin\piadmin\app\base\BaseModel;
use think\model\concern\SoftDelete;
class SystemMenu extends BaseModel
{
protected $table = 'pi_system_menu';
use SoftDelete;
protected string $deleteTime = 'delete_time';
protected $defaultSoftDelete = 0;
}

30
plugin/piadmin/app/model/SystemRole.php

@ -0,0 +1,30 @@
<?php
namespace plugin\piadmin\app\model;
use plugin\piadmin\app\base\BaseModel;
use think\model\concern\SoftDelete;
class SystemRole extends BaseModel
{
protected $table = 'pi_system_role';
use SoftDelete;
protected string $deleteTime = 'delete_time';
protected $defaultSoftDelete = 0;
// 定义角色与管理员的多对多关联
public function admins()
{
return $this->belongsToMany(SystemAdmin::class, SystemAdminRole::class);
}
// 角色与菜单的多对多关联
public function menus()
{
return $this->belongsToMany(SystemMenu::class, SystemRoleMenu::class,'menu_guid','role_guid');
}
}

25
plugin/piadmin/app/model/SystemRoleMenu.php

@ -0,0 +1,25 @@
<?php
namespace plugin\piadmin\app\model;
use plugin\piadmin\app\base\BaseModel;
use think\model\concern\SoftDelete;
use think\model\Pivot;
class SystemRoleMenu extends BaseModel
{
protected $table = 'pi_system_role_menu';
protected $autoWriteTimestamp = 'int';
public function menu()
{
return $this->belongsTo(SystemMenu::class);
}
public function role()
{
return $this->belongsTo(SystemRole::class);
}
}

16
plugin/piadmin/app/route/v1/attachment.php

@ -0,0 +1,16 @@
<?php
use plugin\piadmin\app\controller\v1\AttachmentController;
use plugin\piadmin\app\middleware\AdminAuthorizationMiddleware;
use Webman\Route;
Route::group('/piadmin/v1', function () {
//文件上传
Route::post('/upload', [AttachmentController::class, 'upload']);
//文件列表
Route::get('/attachment/index', [AttachmentController::class, 'index']);
//文件删除
Route::post('/attachment/delete', [AttachmentController::class, 'delete']);
})->middleware([AdminAuthorizationMiddleware::class]);

71
plugin/piadmin/app/route/v1/route.php

@ -0,0 +1,71 @@
<?php
use plugin\piadmin\app\controller\v1\DataDictionaryController;
use plugin\piadmin\app\controller\v1\IndexController;
use plugin\piadmin\app\controller\v1\LoginController;
use plugin\piadmin\app\controller\v1\MenuController;
use plugin\piadmin\app\controller\v1\SystemAdminController;
use plugin\piadmin\app\controller\v1\SystemDeptController;
use plugin\piadmin\app\controller\v1\SystemRoleController;
use plugin\piadmin\app\middleware\AdminAuthorizationMiddleware;
use plugin\piadmin\app\middleware\PermissionsMiddleware;
use Webman\Route;
// 不需要登录的接口
Route::group('/piadmin/v1', function () {
Route::any('/test', [IndexController::class, 'test']);
Route::any('/testlogin', [IndexController::class, 'index']);
Route::post('/login', [LoginController::class, 'login'])->setParams(['perm' => ['login']]);
Route::get('/captcha', [LoginController::class, 'captcha'])->setParams(['perm' => ['captcha']]);
});
// 需要登录的接口
Route::group('/piadmin/v1', function () {
// 获取当前用户菜单权限
Route::get('/getMenuPermissions', [MenuController::class, 'getMenuPermissions'])->setParams(['perm' => ['getMenuPermissions']]);
// 菜单
Route::group('/menu', function () {
Route::get('/index', [MenuController::class, 'index'])->setParams(['perm' => ['menuIndex']]);
Route::post('/save', [MenuController::class, 'save'])->setParams(['perm' => ['menuSave']]);
Route::post('/update', [MenuController::class, 'update'])->setParams(['perm' => ['menuUpdate']]);
Route::get('/read', [MenuController::class, 'read'])->setParams(['perm' => ['menuRead']]);
Route::post('/delete', [MenuController::class, 'delete'])->setParams(['perm' => ['menuDelete']]);
});
// 角色
Route::group('/role', function () {
Route::post('/save', [SystemRoleController::class, 'save'])->setParams(['perm' => ['roleSave']]);
Route::post('/update', [SystemRoleController::class, 'update'])->setParams(['perm' => ['roleUpdate']]);
Route::get('/index', [SystemRoleController::class, 'index'])->setParams(['perm' => ['roleIndex']]);
Route::get('/read', [SystemRoleController::class, 'read'])->setParams(['perm' => ['roleRead']]);
Route::post('/delete', [SystemRoleController::class, 'delete'])->setParams(['perm' => ['roleDelete']]);
Route::get('/pure/index', [SystemRoleController::class, 'pureIndex'])->setParams(['perm' => ['rolePureIndex']]);
});
// 部门
Route::group('/dept', function () {
Route::post('/save', [SystemDeptController::class, 'save'])->setParams(['perm' => ['deptSave']]);
Route::post('/update', [SystemDeptController::class, 'update'])->setParams(['perm' => ['deptUpdate']]);
Route::get('/index', [SystemDeptController::class, 'index'])->setParams(['perm' => ['deptIndex']]);
Route::get('/read', [SystemDeptController::class, 'read'])->setParams(['perm' => ['deptRead']]);
Route::post('/delete', [SystemDeptController::class, 'delete'])->setParams(['perm' => ['deptDelete']]);
Route::get('/pure/index', [SystemDeptController::class, 'pureIndex'])->setParams(['perm' => ['deptPureIndex']]);
});
// 用户
Route::group('/admin', function () {
Route::post('/save', [SystemAdminController::class, 'save'])->setParams(['perm' => ['adminSave']]);
Route::post('/update', [SystemAdminController::class, 'update'])->setParams(['perm' => ['adminUpdate']]);
Route::get('/index', [SystemAdminController::class, 'index'])->setParams(['perm' => ['adminIndex']]);
Route::get('/read', [SystemAdminController::class, 'read'])->setParams(['perm' => ['adminRead']]);
Route::post('/delete', [SystemAdminController::class, 'delete'])->setParams(['perm' => ['adminDelete']]);
//封禁/解封
Route::post('/ban', [SystemAdminController::class, 'ban'])->setParams(['perm' => ['adminBan']]);
});
//数据字典
Route::get('/dataDictionary', [DataDictionaryController::class, 'dictionary'])->setParams(['perm' => ['dataDictionary']]);
})->middleware([
AdminAuthorizationMiddleware::class,
PermissionsMiddleware::class
]);

98
plugin/piadmin/app/service/AttachmentService.php

@ -0,0 +1,98 @@
<?php
namespace plugin\piadmin\app\service;
use plugin\piadmin\app\base\BaseService;
use plugin\piadmin\app\dao\AttachmenatDao;
use plugin\piadmin\app\exception\ApiException;
use plugin\piadmin\app\model\Attachment;
use plugin\piadmin\app\utils\IdUtils;
use plugin\piadmin\app\utils\RequestUtils;
use plugin\piadmin\app\utils\storage\AliOssStorage;
use think\facade\Db;
use Webman\Http\UploadFile;
class AttachmentService extends BaseService
{
private AliOssStorage $ossStorage;
public function __construct()
{
$this->ossStorage = new AliOssStorage();
$this->dao = app()->make(AttachmenatDao::class);
}
/**
* @desc 文件上传
* @author zhangf@suq.cn
* @date 2025.9.1
* @param $res
* @return array|\support\Response
*/
public function upload(array|UploadFile|null $file)
{
$fileExt = pathinfo($file->getUploadName(), PATHINFO_EXTENSION);
$uniqueName = uniqid();
$result = $this->ossStorage->uploadObject($file->getPathname(), "$uniqueName.$fileExt", 'geo/images/user_avatar');
$aliyunOssUrl = $result['info']['url'] ?? '';
$path = parse_url($aliyunOssUrl, PHP_URL_PATH);
//封装数据
$data = [
'id' => IdUtils::uuid(),
'storage' => Attachment::getStorage(),
'type' => $file->getUploadMimeType(),
'name' => "$uniqueName.$fileExt",
'path' => config('plugin.piadmin.oss.ali.domain') . '/' . $path,
'suffix' => $fileExt,
'original_name' => $file->getUploadName(),
'size' => $file->getSize(),
];
$res = $this->dao->save($data);
return [
'id' => $res['id'],
'url' => config('plugin.piadmin.oss.ali.domain') . '/' . $path
];
}
/**
* @desc 获取文件列表
* @author zhangf@suq.cn
* @date 2025.9.1
* @param array $params
* @return array
*/
public function listData(array $params): array
{
[$page, $limit] = RequestUtils::getPageParameter();
[$sortRule, $sortField] = RequestUtils::getSortParameter();
$query = [
'delete_time' => 0
];
// 文件原名筛选
if (isNotBlank($params['original_name'])) {
$query[] = ['original_name', 'like', '%' . $params['original_name'] . '%'];
}
$list = $this->dao->getList($query, '*', $page, $limit, "$sortField $sortRule", ['creator']);
$count = $this->dao->getCount($query);
return compact('list', 'count');
}
/**
* @desc 删除文件
* @author zhangf@suq.com
* @date 2025.9.1
* @param $id
* @return void
*/
public function deleteData($id)
{
Db::startTrans();
try {
$this->dao->softDel($id);
Db::commit();
} catch (\Exception $exception) {
Db::rollback();
throw new ApiException($exception->getMessage());
}
}
}

24
plugin/piadmin/app/service/DataDictionaryService.php

@ -0,0 +1,24 @@
<?php
namespace plugin\piadmin\app\service;
use plugin\piadmin\app\base\BaseService;
use plugin\piadmin\app\dao\InformationSchemaDao;
use support\Db;
class DataDictionaryService extends BaseService
{
protected $dao;
public function __construct()
{
$this->dao = app()->make(InformationSchemaDao::class);
}
public function dictionary()
{
return $this->dao->getInfomation();
}
}

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

Loading…
Cancel
Save