first commit
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute;
|
||||
|
||||
interface AnnoRoute
|
||||
{
|
||||
|
||||
/**
|
||||
* register route
|
||||
*
|
||||
* @param string $path
|
||||
* @return void
|
||||
*/
|
||||
public function register(string $path): void;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute;
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class AnnoRouteService implements AnnoRoute
|
||||
{
|
||||
|
||||
/**
|
||||
* register route
|
||||
* @param string|array $path
|
||||
* @return void
|
||||
*/
|
||||
public function register($path): void
|
||||
{
|
||||
$paths = is_array($path) ? $path : [$path];
|
||||
|
||||
foreach ($paths as $p) {
|
||||
$this->registerFromPath($p);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定路径注册路由
|
||||
*/
|
||||
private function registerFromPath(string $path): void
|
||||
{
|
||||
if (!is_dir($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$finder = new Finder();
|
||||
$finder->files()
|
||||
->in($path)
|
||||
->name('*Controller.php');
|
||||
|
||||
foreach ($finder as $controller) {
|
||||
$className = $this->getClassNameFromFile(
|
||||
$controller->getRealPath(),
|
||||
$controller->getPath()
|
||||
);
|
||||
if ($className && class_exists($className)) {
|
||||
RouteRegisterService::register($className);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件路径和所在目录解析类名
|
||||
*/
|
||||
private function getClassNameFromFile(string $filePath, string $fileDir): ?string
|
||||
{
|
||||
// 读取文件内容获取命名空间
|
||||
$content = file_get_contents($filePath);
|
||||
|
||||
if (!preg_match('/namespace\s+([^;]+);/', $content, $namespaceMatch)) {
|
||||
// 没有命名空间,尝试使用 PSR-0 规则
|
||||
return $this->guessClassNameFromPath($filePath, $fileDir);
|
||||
}
|
||||
|
||||
$namespace = trim($namespaceMatch[1]);
|
||||
$className = basename($filePath, '.php');
|
||||
|
||||
return $namespace . '\\' . $className;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当文件没有命名空间时,通过路径猜测类名
|
||||
*/
|
||||
private function guessClassNameFromPath(string $filePath, string $fileDir): ?string
|
||||
{
|
||||
// 获取相对于项目根目录的路径
|
||||
$basePath = base_path();
|
||||
$relativePath = str_replace($basePath, '', $fileDir);
|
||||
|
||||
// 将路径转换为命名空间
|
||||
$namespace = str_replace('/', '\\', ltrim($relativePath, '/'));
|
||||
|
||||
// 移除开头的反斜杠并转换路径分隔符
|
||||
$namespace = trim($namespace, '\\');
|
||||
|
||||
$className = basename($filePath, '.php');
|
||||
|
||||
// 如果命名空间为空,直接返回类名
|
||||
if (empty($namespace)) {
|
||||
return $className;
|
||||
}
|
||||
|
||||
return $namespace . '\\' . $className;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute\Attribute;
|
||||
|
||||
use Attribute;
|
||||
use Modules\AnnoRoute\BaseAttribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
class DeleteRoute extends BaseAttribute
|
||||
{
|
||||
/** @var string 请求方法 */
|
||||
public string $httpMethod = 'DELETE';
|
||||
|
||||
/**
|
||||
* @param string $route 路由地址
|
||||
* @param string|bool $authorize 权限字段
|
||||
* @param string|array $middleware 中间件
|
||||
* @param array $where 路由参数约束
|
||||
*/
|
||||
public function __construct(
|
||||
public string $route = '',
|
||||
public string|bool $authorize = true,
|
||||
public string | array $middleware = '',
|
||||
public array $where = [],
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute\Attribute;
|
||||
|
||||
use Attribute;
|
||||
use Modules\AnnoRoute\BaseAttribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
class GetRoute extends BaseAttribute
|
||||
{
|
||||
/** @var string 请求方法 */
|
||||
public string $httpMethod = 'GET';
|
||||
|
||||
/**
|
||||
* @param string $route 路由地址
|
||||
* @param string|bool $authorize 权限字段
|
||||
* @param string|array $middleware 中间件
|
||||
* @param array $where 路由参数约束
|
||||
*/
|
||||
public function __construct(
|
||||
public string $route = '',
|
||||
public string|bool $authorize = true,
|
||||
public string | array $middleware = '',
|
||||
public array $where = [],
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute\Attribute;
|
||||
|
||||
use Attribute;
|
||||
use Modules\AnnoRoute\BaseAttribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
class PostRoute extends BaseAttribute
|
||||
{
|
||||
/** @var string 请求方法 */
|
||||
public string $httpMethod = 'POST';
|
||||
|
||||
/**
|
||||
* @param string $route 路由地址
|
||||
* @param string|bool $authorize 权限字段
|
||||
* @param string|array $middleware 中间件
|
||||
* @param array $where 路由参数约束
|
||||
*/
|
||||
public function __construct(
|
||||
public string $route = '',
|
||||
public string|bool $authorize = true,
|
||||
public string | array $middleware = '',
|
||||
public array $where = [],
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute\Attribute;
|
||||
|
||||
use Attribute;
|
||||
use Modules\AnnoRoute\BaseAttribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
class PutRoute extends BaseAttribute
|
||||
{
|
||||
/** @var string 请求方法 */
|
||||
public string $httpMethod = 'PUT';
|
||||
|
||||
/**
|
||||
* @param string $route 路由地址
|
||||
* @param string|bool $authorize 权限字段
|
||||
* @param string|array $middleware 中间件
|
||||
* @param array $where 路由参数约束
|
||||
*/
|
||||
public function __construct(
|
||||
public string $route = '',
|
||||
public string|bool $authorize = true,
|
||||
public string | array $middleware = '',
|
||||
public array $where = [],
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute\Attribute;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class RequestAttribute
|
||||
{
|
||||
/**
|
||||
* @param string $routePrefix 路由前缀
|
||||
* @param string $abilitiesPrefix 权限前缀
|
||||
* @param string | array $middleware 控制器中间件
|
||||
* @param string | null $authGuard 用户提供程序
|
||||
*/
|
||||
public function __construct(
|
||||
public string $routePrefix = '',
|
||||
public string $abilitiesPrefix = '',
|
||||
public string | array $middleware = '',
|
||||
public ?string $authGuard = null
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Modules\Common\Enum\ShowType as ShopTypeEnum;
|
||||
|
||||
abstract class BaseAttribute
|
||||
{
|
||||
/**
|
||||
* 路由地址,使用该属性来指定路由地址,完整的路由地址由 RequestAttribute 的 routePrefix 和 route 拼接而成,例如:
|
||||
* RequestAttribute 的 routePrefix 为 /admin/user,Mapping 的 route 为 /create,那么完整的路由地址为 /admin/user/create
|
||||
*
|
||||
* Routing address, use this attribute to specify the routing address. The complete routing address is composed
|
||||
* of the routePrefix of RequestAttribute and the route concatenated, for example: If the routePrefix of RequestAttribute
|
||||
* is /admin/user and the route of Mapping is /create, then the complete routing address is /admin/user/create
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $route = '';
|
||||
|
||||
/**
|
||||
* 路由中间件,该属性用于指定路由中间件,多个中间件以数组形式传递,例如:['auth', 'admin']
|
||||
*
|
||||
* Route middleware, this attribute is used to specify the route middleware, multiple middleware are passed in
|
||||
* as an array, for example: ['auth', 'admin']
|
||||
*
|
||||
* @var string|array
|
||||
*/
|
||||
public string | array $middleware = '';
|
||||
|
||||
/**
|
||||
* HTTP 的请求方法
|
||||
*
|
||||
* HTTP request method
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $httpMethod = 'POST';
|
||||
|
||||
/**
|
||||
* 权限字符串,该属性用于指定权限【能力】字符串,用于权限【能力】认证,完整的能力字符串由 RequestAttribute 的 abilitiesPrefix 和 authorize
|
||||
* 拼接而成,例如:RequestAttribute 的 abilitiesPrefix 为 admin,Mapping 的 authorize 为 user,那么完整的权限【能力】字符串为 admin.user
|
||||
*
|
||||
* Permission string, this attribute is used to specify the permission string, used for permission authentication,
|
||||
* the complete permission string is composed of the abilitiesPrefix of RequestAttribute and the authorize concatenated,
|
||||
* for example: If the abilitiesPrefix of RequestAttribute is admin and the authorize of Mapping is user, then the complete
|
||||
* permission string is admin.user
|
||||
*
|
||||
* @var string | bool
|
||||
*/
|
||||
public string | bool $authorize = true;
|
||||
|
||||
/**
|
||||
* 路由参数约束,该属性用于指定路由参数的正则约束,例如:['id' => '[0-9]+']
|
||||
*
|
||||
* Route parameter constraints, this attribute is used to specify regex constraints for route parameters,
|
||||
* for example: ['id' => '[0-9]+']
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public array $where = [];
|
||||
|
||||
/**
|
||||
* 成功响应
|
||||
* @param $msg array|string
|
||||
* @return JsonResponse
|
||||
*/
|
||||
protected static function success(array|string $msg = ''): JsonResponse
|
||||
{
|
||||
if (is_array($msg)) {
|
||||
$data = $msg;
|
||||
$msg = '';
|
||||
} else {
|
||||
$data = [];
|
||||
}
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $data,
|
||||
'showType' => ShopTypeEnum::SUCCESS_MESSAGE->value,
|
||||
'msg' => $msg,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
|
||||
class RouteRegisterService
|
||||
{
|
||||
/**
|
||||
* @var array|string[]
|
||||
*/
|
||||
private static array $mapping = [
|
||||
GetRoute::class,
|
||||
PostRoute::class,
|
||||
PutRoute::class,
|
||||
DeleteRoute::class
|
||||
];
|
||||
|
||||
/**
|
||||
* register 注册路由
|
||||
*/
|
||||
public static function register(string $className): void
|
||||
{
|
||||
try {
|
||||
$classRef = new ReflectionClass($className);
|
||||
|
||||
$classAttr = collect($classRef->getAttributes());
|
||||
if($classAttr->isEmpty()) return;
|
||||
|
||||
$classAttrName = $classAttr->map->getName();
|
||||
if(! $classAttrName->contains(RequestAttribute::class)) {
|
||||
return;
|
||||
}
|
||||
$requestMapping = $classAttr->first(fn ($item) => $item->getName() == RequestAttribute::class);
|
||||
$routeInstance = $requestMapping->newInstance();
|
||||
// 默认参数
|
||||
$routePrefix = $routeInstance->routePrefix ?? '';
|
||||
$authGuard = $routeInstance->authGuard ?? null;
|
||||
$abilitiesPrefix = $routeInstance->abilitiesPrefix ?? '';
|
||||
$middleware = self::registerMiddleware($routeInstance->middleware);
|
||||
|
||||
$methods = $classRef->getMethods();
|
||||
|
||||
foreach ($methods as $method) {
|
||||
// 方法注解
|
||||
$attributes = $method->getAttributes();
|
||||
if(empty($attributes)) {
|
||||
continue;
|
||||
}
|
||||
$methodName = $method->getName();
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
if (in_array($attribute->getName(), self::$mapping)) {
|
||||
$instance = $attribute->newInstance();
|
||||
self::registerRoute(
|
||||
$instance,
|
||||
$methodName,
|
||||
$className,
|
||||
$authGuard,
|
||||
$middleware,
|
||||
$routePrefix,
|
||||
$abilitiesPrefix,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ReflectionException $e) {
|
||||
echo $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册路由
|
||||
* @param BaseAttribute $instance
|
||||
* @param string $method
|
||||
* @param string $className
|
||||
* @param string|null $authGuard
|
||||
* @param array $middleware
|
||||
* @param string $routePrefix
|
||||
* @param string $abilitiesPrefix
|
||||
* @return void
|
||||
*/
|
||||
private static function registerRoute(
|
||||
BaseAttribute $instance,
|
||||
string $method,
|
||||
string $className,
|
||||
string $authGuard = null,
|
||||
array $middleware = [],
|
||||
string $routePrefix = '',
|
||||
string $abilitiesPrefix = ''
|
||||
): void
|
||||
{
|
||||
$authorize = $instance->authorize;
|
||||
|
||||
$authMiddleware = [];
|
||||
if (!empty($authorize)) {
|
||||
$authMiddleware[] = 'auth:sanctum';
|
||||
if(! empty($authGuard) ) {
|
||||
$authMiddleware[] = 'authGuard:' . $authGuard;
|
||||
} else {
|
||||
$authMiddleware[] = 'authGuard';
|
||||
}
|
||||
if (is_string($authorize) && !empty($abilitiesPrefix)) {
|
||||
$authMiddleware[] = 'abilities:' . $abilitiesPrefix . '.' . $authorize;
|
||||
} else if ($authorize !== true) {
|
||||
$authMiddleware[] = 'abilities:' . $authorize;
|
||||
}
|
||||
}
|
||||
|
||||
$middleware = array_merge($authMiddleware, self::registerMiddleware($instance->middleware), $middleware);
|
||||
$route = Route::{Str::lower($instance->httpMethod)}(
|
||||
$routePrefix . $instance->route,
|
||||
[$className, $method]
|
||||
)->middleware(array_unique($middleware));
|
||||
|
||||
if (!empty($instance->where)) {
|
||||
$route->where($instance->where);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取中间件
|
||||
* @param $middleware
|
||||
* @return string[]
|
||||
*/
|
||||
private static function registerMiddleware($middleware): array
|
||||
{
|
||||
if(empty($middleware)) {
|
||||
return [];
|
||||
}
|
||||
if(is_array($middleware)) {
|
||||
return $middleware;
|
||||
}
|
||||
if (is_string($middleware)) {
|
||||
return [$middleware];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace Modules\AnnoRoute;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->bind(AnnoRoute::class, AnnoRouteService::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Common\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class GenerateRouteHelperCommand extends Command
|
||||
{
|
||||
protected $signature = 'route:helper';
|
||||
|
||||
protected $description = 'Scan annotation routes and generate IDE route helper files for Laravel Idea';
|
||||
|
||||
private static array $mapping = [
|
||||
GetRoute::class,
|
||||
PostRoute::class,
|
||||
PutRoute::class,
|
||||
DeleteRoute::class,
|
||||
];
|
||||
|
||||
private static array $httpMethodMap = [
|
||||
GetRoute::class => 'get',
|
||||
PostRoute::class => 'post',
|
||||
PutRoute::class => 'put',
|
||||
DeleteRoute::class => 'delete',
|
||||
];
|
||||
|
||||
/** @var array<string, list<array{method: string, uri: string, controller: string, action: string, middleware: array}>> */
|
||||
private array $controllerRoutes = [];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$paths = [
|
||||
app_path('Http/Controllers'),
|
||||
base_path('modules'),
|
||||
];
|
||||
|
||||
$totalRoutes = 0;
|
||||
|
||||
foreach ($paths as $path) {
|
||||
if (!is_dir($path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$finder = new Finder();
|
||||
$finder->files()->in($path)->name('*Controller.php');
|
||||
|
||||
foreach ($finder as $file) {
|
||||
$className = $this->getClassNameFromFile(
|
||||
$file->getRealPath(),
|
||||
$file->getPath()
|
||||
);
|
||||
|
||||
if ($className && class_exists($className)) {
|
||||
$routes = $this->scanController($className);
|
||||
if (!empty($routes)) {
|
||||
$this->controllerRoutes[$className] = $routes;
|
||||
$totalRoutes += count($routes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->controllerRoutes)) {
|
||||
$this->warn('No annotation routes found.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->writeHelperFile();
|
||||
$this->info("Routes generated to: " . base_path('routes/api.php'));
|
||||
$this->info("Found {$totalRoutes} routes.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function scanController(string $className): array
|
||||
{
|
||||
$routes = [];
|
||||
|
||||
try {
|
||||
$classRef = new ReflectionClass($className);
|
||||
} catch (ReflectionException) {
|
||||
return $routes;
|
||||
}
|
||||
|
||||
$classAttrs = collect($classRef->getAttributes());
|
||||
if ($classAttrs->isEmpty()) {
|
||||
return $routes;
|
||||
}
|
||||
|
||||
$hasRequestAttr = $classAttrs->some(
|
||||
fn($attr) => $attr->getName() === RequestAttribute::class
|
||||
);
|
||||
if (!$hasRequestAttr) {
|
||||
return $routes;
|
||||
}
|
||||
|
||||
$requestAttr = $classAttrs->first(
|
||||
fn($attr) => $attr->getName() === RequestAttribute::class
|
||||
);
|
||||
$requestInstance = $requestAttr->newInstance();
|
||||
$routePrefix = $requestInstance->routePrefix ?? '';
|
||||
$authGuard = $requestInstance->authGuard ?? null;
|
||||
$abilitiesPrefix = $requestInstance->abilitiesPrefix ?? '';
|
||||
$classMiddleware = $this->normalizeMiddleware($requestInstance->middleware);
|
||||
|
||||
foreach ($classRef->getMethods() as $method) {
|
||||
$attrs = $method->getAttributes();
|
||||
if (empty($attrs)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$methodName = $method->getName();
|
||||
|
||||
foreach ($attrs as $attr) {
|
||||
if (in_array($attr->getName(), self::$mapping)) {
|
||||
$instance = $attr->newInstance();
|
||||
|
||||
$authMiddleware = $this->buildAuthMiddleware(
|
||||
$instance->authorize,
|
||||
$authGuard,
|
||||
$abilitiesPrefix,
|
||||
);
|
||||
|
||||
$allMiddleware = array_values(array_unique(array_merge(
|
||||
$authMiddleware,
|
||||
$this->normalizeMiddleware($instance->middleware),
|
||||
$classMiddleware,
|
||||
)));
|
||||
|
||||
$routes[] = [
|
||||
'method' => self::$httpMethodMap[$attr->getName()],
|
||||
'uri' => $instance->route,
|
||||
'prefix' => trim($routePrefix, '/'),
|
||||
'controller' => $className,
|
||||
'action' => $methodName,
|
||||
'middleware' => $allMiddleware,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
private function buildAuthMiddleware(string|bool $authorize, ?string $authGuard, string $abilitiesPrefix): array
|
||||
{
|
||||
if (empty($authorize) || $authorize === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$authMiddleware = ['auth:sanctum'];
|
||||
|
||||
if (!empty($authGuard)) {
|
||||
$authMiddleware[] = 'authGuard:' . $authGuard;
|
||||
} else {
|
||||
$authMiddleware[] = 'authGuard';
|
||||
}
|
||||
|
||||
if (is_string($authorize) && !empty($abilitiesPrefix)) {
|
||||
$authMiddleware[] = 'abilities:' . $abilitiesPrefix . '.' . $authorize;
|
||||
} else if ($authorize !== true) {
|
||||
$authMiddleware[] = 'abilities:' . $authorize ;
|
||||
}
|
||||
|
||||
return $authMiddleware;
|
||||
}
|
||||
|
||||
private function normalizeMiddleware(string|array $middleware): array
|
||||
{
|
||||
if (empty($middleware)) {
|
||||
return [];
|
||||
}
|
||||
if (is_array($middleware)) {
|
||||
return $middleware;
|
||||
}
|
||||
return [$middleware];
|
||||
}
|
||||
|
||||
private function getClassNameFromFile(string $filePath, string $fileDir): ?string
|
||||
{
|
||||
$content = file_get_contents($filePath);
|
||||
|
||||
if (!preg_match('/namespace\s+([^;]+);/', $content, $namespaceMatch)) {
|
||||
return $this->guessClassNameFromPath($filePath, $fileDir);
|
||||
}
|
||||
|
||||
$namespace = trim($namespaceMatch[1]);
|
||||
$className = basename($filePath, '.php');
|
||||
|
||||
return $namespace . '\\' . $className;
|
||||
}
|
||||
|
||||
private function guessClassNameFromPath(string $filePath, string $fileDir): ?string
|
||||
{
|
||||
$basePath = base_path();
|
||||
$relativePath = str_replace($basePath, '', $fileDir);
|
||||
$namespace = str_replace('/', '\\', ltrim($relativePath, '/'));
|
||||
$namespace = trim($namespace, '\\');
|
||||
$className = basename($filePath, '.php');
|
||||
|
||||
return empty($namespace) ? $className : $namespace . '\\' . $className;
|
||||
}
|
||||
|
||||
private function writeHelperFile(): void
|
||||
{
|
||||
$outputPath = base_path('routes/api.php');
|
||||
|
||||
$lines = [
|
||||
'<?php',
|
||||
'',
|
||||
'use Illuminate\Support\Facades\Route;',
|
||||
'',
|
||||
'Route::get(\'/\', function () {',
|
||||
' return "Hello, thank you for using XinAdmin. ";',
|
||||
'});',
|
||||
'',
|
||||
];
|
||||
|
||||
// Each controller gets its own Route::controller group
|
||||
foreach ($this->controllerRoutes as $className => $routes) {
|
||||
$prefix = $routes[0]['prefix'];
|
||||
|
||||
$shortName = substr(strrchr($className, '\\'), 1);
|
||||
$lines[] = "// {$shortName}";
|
||||
|
||||
$chain = "Route::controller({$className}::class)";
|
||||
if ($prefix !== '') {
|
||||
$chain .= "->prefix('{$prefix}')";
|
||||
}
|
||||
$chain .= '->group(function () {';
|
||||
$lines[] = $chain;
|
||||
|
||||
foreach ($routes as $route) {
|
||||
$uri = $route['uri'];
|
||||
if ($uri === '') {
|
||||
$uri = '/';
|
||||
}
|
||||
|
||||
$middlewareStr = '';
|
||||
if (!empty($route['middleware'])) {
|
||||
$middlewareStr = "->middleware(['" . implode("', '", $route['middleware']) . "'])";
|
||||
}
|
||||
|
||||
$lines[] = " Route::{$route['method']}('{$uri}', '{$route['action']}'){$middlewareStr};";
|
||||
}
|
||||
|
||||
$lines[] = '});';
|
||||
$lines[] = '';
|
||||
}
|
||||
|
||||
File::put($outputPath, implode("\n", $lines));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
namespace Modules\Common\Enum;
|
||||
|
||||
/**
|
||||
* 响应状态枚举类
|
||||
*/
|
||||
enum ShowType: int
|
||||
{
|
||||
// 成功响应
|
||||
case SUCCESS_MESSAGE = 0;
|
||||
|
||||
// 警告响应
|
||||
case WARN_MESSAGE = 1;
|
||||
|
||||
// 错误响应
|
||||
case ERROR_MESSAGE = 2;
|
||||
|
||||
// 成功通知
|
||||
case SUCCESS_NOTIFICATION = 3;
|
||||
|
||||
// 警告通知
|
||||
case WARN_NOTIFICATION = 4;
|
||||
|
||||
// 错误通知
|
||||
case ERROR_NOTIFICATION = 5;
|
||||
|
||||
// 静默响应
|
||||
case SILENT = 99;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Common\Http\Controllers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Modules\Common\Trait\RequestJson;
|
||||
|
||||
abstract class BaseController extends Controller
|
||||
{
|
||||
use RequestJson;
|
||||
|
||||
/**
|
||||
* 查询字符串
|
||||
* The fields queried by the current model
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $searchField = [];
|
||||
|
||||
/**
|
||||
* 快速搜索字段
|
||||
* Quick search field
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $quickSearchField = [];
|
||||
|
||||
|
||||
/** 构建查询方法 */
|
||||
protected function buildSearch(array $params, Builder $model): Builder
|
||||
{
|
||||
// 构建筛选
|
||||
if (isset($params['filter']) && $params['filter'] != '') {
|
||||
if(is_array($params['filter'])) {
|
||||
$filter = $params['filter'];
|
||||
} else {
|
||||
$filter = json_decode($params['filter'], true);
|
||||
}
|
||||
foreach ($filter as $k => $v) {
|
||||
if (! $v) {
|
||||
continue;
|
||||
}
|
||||
$model->whereIn($k, $v);
|
||||
}
|
||||
unset($params['filter']);
|
||||
}
|
||||
|
||||
// 构建查询
|
||||
foreach ($this->searchField ?? [] as $key => $op) {
|
||||
if (isset($params[$key]) && $params[$key] != '') {
|
||||
if (in_array($op, ['=', '>', '<>', '<', '>=', '<='])) {
|
||||
$model->where($key, $op, $params[$key]);
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($op == 'like') {
|
||||
$model->where($key, $op, '%'.$params[$key].'%');
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($op == 'afterLike') {
|
||||
$model->where($key, $op, $params[$key].'%');
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($op == 'beforeLike') {
|
||||
$model->where($key, $op, '%'.$params[$key]);
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($op == 'date') {
|
||||
$date = date('Y-m-d', strtotime($params[$key]));
|
||||
$model->whereDate($key, $date);
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($op == 'betweenDate') {
|
||||
if (is_array($params[$key])) {
|
||||
$start = $params[$key][0];
|
||||
$end = $params[$key][1];
|
||||
$model->whereDate($key, '>=', $start);
|
||||
$model->whereDate($key, '<=', $end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 快速搜索
|
||||
if (isset($params['keywordSearch']) && $params['keywordSearch'] != '') {
|
||||
$quickSearchArr = $this->quickSearchField ?? [];
|
||||
if (count($quickSearchArr) > 0) {
|
||||
$model->whereAny(
|
||||
$quickSearchArr,
|
||||
'like',
|
||||
'%'.str_replace('%', '\%', $params['keywordSearch']).'%'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建排序
|
||||
if (isset($params['sorter']) && $params['sorter']) {
|
||||
if(is_array($params['sorter'])) {
|
||||
$sorter = $params['sorter'];
|
||||
} else {
|
||||
$sorter = json_decode($params['sorter'], true);
|
||||
}
|
||||
if (count($sorter) > 0) {
|
||||
$column = array_keys($sorter)[0];
|
||||
$direction = $sorter[$column] == 'ascend' ? 'asc' : 'desc';
|
||||
$model->orderBy($column, $direction);
|
||||
}
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Common\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class BaseFormRequest extends FormRequest
|
||||
{
|
||||
|
||||
/**
|
||||
* 是否为更新请求
|
||||
* @return bool
|
||||
*/
|
||||
protected function isUpdate(): bool
|
||||
{
|
||||
if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Common\Middlewares;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AllowCrossDomainMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
// 添加跨域头
|
||||
$response->headers->set('Access-Control-Allow-Origin', '*');
|
||||
$response->headers->set('Access-Control-Allow-Credentials', 'true');
|
||||
$response->headers->set('Access-Control-Max-Age', 1800);
|
||||
$response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
$response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, User-Language');
|
||||
|
||||
// 如果是预检请求, 返回 204
|
||||
if ($request->isMethod('OPTIONS')) {
|
||||
return response()->json([], 204, $response->headers->all());
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Common\Middlewares;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
class LanguageMiddleware
|
||||
{
|
||||
/**
|
||||
* 支持的语言列表
|
||||
*/
|
||||
protected array $supportedLanguages = [
|
||||
'en' => 'en', // 英语
|
||||
'zh' => 'zh', // 简体中文
|
||||
'jp' => 'ja', // 日语
|
||||
];
|
||||
|
||||
/**
|
||||
* 默认语言
|
||||
*/
|
||||
protected string $defaultLanguage = 'zh';
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
// 获取当前语言
|
||||
$locale = $this->getLocale($request);
|
||||
// 设置应用语言
|
||||
App::setLocale($locale);
|
||||
// 让请求继续处理
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前语言设置
|
||||
*/
|
||||
protected function getLocale(Request $request): string
|
||||
{
|
||||
// 优先级 1: URL 参数 (例如 ?lang=en)
|
||||
if ($request->has('lang')) {
|
||||
$lang = $request->get('lang');
|
||||
if ($this->isSupported($lang)) {
|
||||
return $lang;
|
||||
}
|
||||
}
|
||||
|
||||
// 优先级 2: User-Language 头
|
||||
$browserLocale = $this->getBrowserLocale($request);
|
||||
if ($browserLocale && $this->isSupported($browserLocale)) {
|
||||
return $browserLocale;
|
||||
}
|
||||
|
||||
// 优先级 3: Session 中存储的语言
|
||||
if (Session::has('locale')) {
|
||||
$lang = Session::get('locale');
|
||||
if ($this->isSupported($lang)) {
|
||||
return $lang;
|
||||
}
|
||||
}
|
||||
|
||||
// 优先级 4: 配置文件中的默认语言
|
||||
return config('app.locale', $this->defaultLanguage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 User-Language 头中获取浏览器偏好语言
|
||||
*/
|
||||
protected function getBrowserLocale(Request $request): ?string
|
||||
{
|
||||
$acceptLanguage = $request->header('User-Language');
|
||||
|
||||
if (!$acceptLanguage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $acceptLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查语言是否被支持
|
||||
*/
|
||||
protected function isSupported(string $locale): bool
|
||||
{
|
||||
return array_key_exists($locale, $this->supportedLanguages);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Common\Providers;
|
||||
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class PaginationProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->bind('Illuminate\Pagination\LengthAwarePaginator', function ($app, $options) {
|
||||
return new class(
|
||||
$options['items'],
|
||||
$options['total'],
|
||||
$options['perPage'],
|
||||
$options['currentPage'],
|
||||
$options['options']
|
||||
) extends LengthAwarePaginator {
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'data' => $this->items(),
|
||||
'total' => $this->total(),
|
||||
'pageSize' => $this->perPage(),
|
||||
'current' => $this->currentPage(),
|
||||
];
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
namespace Modules\Common\Trait;
|
||||
|
||||
use App\Exceptions\HttpResponseException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Modules\Common\Enum\ShowType as ShopTypeEnum;
|
||||
|
||||
/**
|
||||
* 响应 trait
|
||||
* 支持 throw 响应
|
||||
*/
|
||||
trait RequestJson
|
||||
{
|
||||
/**
|
||||
* 成功响应
|
||||
*
|
||||
* @param string|array $data 响应数据
|
||||
* @param string $message 响应内容
|
||||
*/
|
||||
protected function success(string|array $data = [], string $message = 'ok'): JsonResponse
|
||||
{
|
||||
if (is_array($data)) {
|
||||
return self::renderJson(true, $data, $message);
|
||||
}
|
||||
|
||||
return self::renderJson(true, [], $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 抛出成功响应,中断程序运行
|
||||
*
|
||||
* @param string|array $data 响应数据
|
||||
* @param string $message 响应内容
|
||||
*/
|
||||
protected function throwSuccess(string|array $data = [], string $message = 'ok'): void
|
||||
{
|
||||
if (is_array($data)) {
|
||||
self::renderThrow(true, $data, $message);
|
||||
}
|
||||
self::renderThrow(true, [], $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回失败响应
|
||||
*
|
||||
* @param string|array $data 响应数据
|
||||
* @param string $message 响应内容
|
||||
*/
|
||||
protected function error(string|array $data = [], string $message = ''): JsonResponse
|
||||
{
|
||||
if (is_array($data)) {
|
||||
return self::renderJson(false, $data, $message, ShopTypeEnum::ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
return self::renderJson(false, [], $data, ShopTypeEnum::ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 抛出失败响应,中断程序运行
|
||||
*
|
||||
* @param string|array $data 响应数据
|
||||
* @param string $message 响应内容
|
||||
*/
|
||||
protected function throwError(string|array $data = [], string $message = ''): void
|
||||
{
|
||||
if (is_array($data)) {
|
||||
self::renderThrow(false, $data, $message, ShopTypeEnum::ERROR_MESSAGE);
|
||||
}
|
||||
self::renderThrow(false, [], $data, ShopTypeEnum::ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回警告响应
|
||||
*
|
||||
* @param string|array $data 响应数据
|
||||
* @param string $message 响应内容
|
||||
*/
|
||||
protected function warn(string|array $data = [], string $message = ''): JsonResponse
|
||||
{
|
||||
if (is_array($data)) {
|
||||
return self::renderJson(false, $data, $message, ShopTypeEnum::WARN_MESSAGE);
|
||||
}
|
||||
|
||||
return self::renderJson(false, [], $data, ShopTypeEnum::WARN_MESSAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 抛出失败警告,中断程序运行
|
||||
*
|
||||
* @param string|array $data 响应数据
|
||||
* @param string $message 响应内容
|
||||
*/
|
||||
protected function throwWarn(string|array $data = [], string $message = ''): void
|
||||
{
|
||||
if (is_array($data)) {
|
||||
self::renderThrow(false, $data, $message, ShopTypeEnum::WARN_MESSAGE);
|
||||
}
|
||||
self::renderThrow(false, [], $data, ShopTypeEnum::WARN_MESSAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知响应
|
||||
*
|
||||
* @param string $msg 通知标题
|
||||
* @param string $description 通知描述
|
||||
* @param string $placement 通知位置 top topLeft topRight bottom bottomLeft bottomRight
|
||||
* @param ShopTypeEnum $showTypeEnum 通知类型
|
||||
*/
|
||||
protected function notification(
|
||||
string $msg,
|
||||
string $description,
|
||||
ShopTypeEnum $showTypeEnum = ShopTypeEnum::SUCCESS_NOTIFICATION,
|
||||
string $placement = 'topRight'
|
||||
): JsonResponse {
|
||||
$showType = $showTypeEnum->value;
|
||||
$success = false;
|
||||
return response()->json(compact('description', 'success', 'msg', 'showType', 'placement'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 Json 响应
|
||||
*
|
||||
* @param bool $success 响应状态
|
||||
* @param array $data 响应数据
|
||||
* @param string $msg 响应内容
|
||||
*/
|
||||
protected static function renderJson(
|
||||
bool $success = true,
|
||||
array $data = [],
|
||||
string $msg = '',
|
||||
ShopTypeEnum $showTypeEnum = ShopTypeEnum::SUCCESS_MESSAGE
|
||||
): JsonResponse {
|
||||
$showType = $showTypeEnum->value;
|
||||
|
||||
return response()->json(compact('data', 'success', 'msg', 'showType'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 抛出 API 数据
|
||||
*
|
||||
* @param bool $success 响应状态
|
||||
* @param mixed $data 返回数据
|
||||
* @param string $msg 响应内容
|
||||
* @param ShopTypeEnum $showTypeEnum
|
||||
*/
|
||||
public static function renderThrow(
|
||||
bool $success = true,
|
||||
array $data = [],
|
||||
string $msg = '',
|
||||
ShopTypeEnum $showTypeEnum = ShopTypeEnum::SUCCESS_MESSAGE
|
||||
) {
|
||||
$showType = $showTypeEnum->value;
|
||||
throw new HttpResponseException(compact('data', 'success', 'msg', 'showType'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
use Modules\SystemTool\Services\SysSiteConfigService;
|
||||
|
||||
if (! function_exists('site_config')) {
|
||||
/**
|
||||
* 获取或设置系统配置
|
||||
*
|
||||
* @param string|null $name 配置名称,格式:'group.key' 或 'group',为null时返回所有配置
|
||||
* @param mixed|null $default 默认值(仅在获取时使用)
|
||||
* @return mixed
|
||||
*
|
||||
* @example
|
||||
* site_config('site.name') // 获取配置
|
||||
* site_config('site.name', 'Default') // 带默认值获取
|
||||
* site_config('site') // 获取整个组
|
||||
* site_config() // 获取所有配置
|
||||
*/
|
||||
function site_config(?string $name = null, mixed $default = null): mixed
|
||||
{
|
||||
return SysSiteConfigService::getSiteConfig($name, $default);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('web_path')) {
|
||||
/**
|
||||
* Get the path to the web of the install.
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
function web_path(string $path = ''): string
|
||||
{
|
||||
return base_path('web'. DIRECTORY_SEPARATOR . $path);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('getTreeData')) {
|
||||
/**
|
||||
* 获取树形数据
|
||||
*
|
||||
* @param array $list
|
||||
* @param int $parentId
|
||||
* @param string[] $params
|
||||
* @return array
|
||||
*/
|
||||
function getTreeData(
|
||||
array &$list,
|
||||
int $parentId = 0,
|
||||
array $params = []
|
||||
): array
|
||||
{
|
||||
$params = array_merge($params, [
|
||||
'id' => 'id',
|
||||
'parent_id' => 'parent_id',
|
||||
'children' => 'children'
|
||||
]);
|
||||
$data = [];
|
||||
foreach ($list as $k => $item) {
|
||||
if ($item[$params['parent_id']] == $parentId) {
|
||||
$children = getTreeData($list, $item[$params['id']]);
|
||||
!empty($children) && $item[$params['children']] = $children;
|
||||
$data[] = $item;
|
||||
unset($list[$k]);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemAgent\Ai\Agents;
|
||||
|
||||
use Laravel\Ai\Concerns\RemembersConversations;
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\Conversational;
|
||||
use Laravel\Ai\Promptable;
|
||||
use Stringable;
|
||||
|
||||
class XinChatAgent implements Agent, Conversational
|
||||
{
|
||||
use Promptable, RemembersConversations;
|
||||
|
||||
/**
|
||||
* Get the instructions that the agent should follow.
|
||||
*/
|
||||
public function instructions(): Stringable|string
|
||||
{
|
||||
return 'You are a helpful, friendly AI assistant. Answer questions clearly and concisely. '
|
||||
. 'If you don\'t know something, be honest about it. '
|
||||
. 'Use markdown formatting when it helps readability.';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemAgent\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemAgent\Models\AgentModel;
|
||||
|
||||
#[RequestAttribute('/ai/agent', 'ai.agent')]
|
||||
class AgentController extends BaseController
|
||||
{
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(): JsonResponse
|
||||
{
|
||||
$agents = AgentModel::orderBy('id')->get();
|
||||
return $this->success($agents->toArray());
|
||||
}
|
||||
|
||||
#[GetRoute('/{id}', authorize: 'query', where: ['id' => '[0-9]+'])]
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$agent = AgentModel::find($id);
|
||||
if (! $agent) {
|
||||
return $this->error('Agent not found');
|
||||
}
|
||||
return $this->success($agent->toArray());
|
||||
}
|
||||
|
||||
#[PutRoute('/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
|
||||
public function update(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$enabled = $request->boolean('enabled', true);
|
||||
$model = AgentModel::find($id);
|
||||
if (! $model) {
|
||||
return $this->error('Agent not found');
|
||||
}
|
||||
$model->enabled = $enabled;
|
||||
$model->save();
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemAgent\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Ai\Responses\StreamableAgentResponse;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemAgent\Ai\Agents\XinChatAgent;
|
||||
use Modules\SystemAgent\Models\AgentModel;
|
||||
|
||||
#[RequestAttribute('/ai/chat', 'ai.chat')]
|
||||
class ChatController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 发送消息并返回 SSE 流式响应
|
||||
*/
|
||||
#[PostRoute('/send', 'send')]
|
||||
public function send(Request $request): StreamableAgentResponse|JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'message' => 'required|string|max:10000',
|
||||
'conversation_id' => 'nullable|string|max:36',
|
||||
'agent_id' => 'nullable|integer|exists:agents,id',
|
||||
]);
|
||||
|
||||
$message = $request->input('message');
|
||||
$conversationId = $request->input('conversation_id');
|
||||
$agentId = $request->input('agent_id');
|
||||
$user = $request->user();
|
||||
|
||||
try {
|
||||
if ($agentId) {
|
||||
$agentModel = AgentModel::findOrFail($agentId);
|
||||
$agentClass = $agentModel->namespace;
|
||||
$agent = $agentClass::make();
|
||||
} else {
|
||||
$agent = XinChatAgent::make();
|
||||
}
|
||||
|
||||
if ($conversationId) {
|
||||
// 继续已有会话
|
||||
$response = $agent
|
||||
->continue($conversationId, as: $user)
|
||||
->stream($message);
|
||||
} else {
|
||||
// 新建会话
|
||||
$response = $agent
|
||||
->forUser($user)
|
||||
->stream($message);
|
||||
}
|
||||
|
||||
return $response;
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error('AI 响应失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的会话列表
|
||||
*/
|
||||
#[GetRoute('/conversations', 'conversations')]
|
||||
public function conversations(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$conversations = $user->conversations()
|
||||
->latest('updated_at')
|
||||
->get()
|
||||
->map(fn ($conversation) => [
|
||||
'key' => $conversation->id,
|
||||
'label' => $conversation->title,
|
||||
'updated_at' => $conversation->updated_at->toISOString(),
|
||||
]);
|
||||
|
||||
return $this->success($conversations->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定会话的消息列表
|
||||
*/
|
||||
#[GetRoute('/messages/{conversationId}', 'messages')]
|
||||
public function messages(Request $request, string $conversationId): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$conversation = $user->conversations()->find($conversationId);
|
||||
|
||||
if (! $conversation) {
|
||||
return $this->error('会话不存在');
|
||||
}
|
||||
|
||||
$messages = $conversation->messages()
|
||||
->oldest()
|
||||
->get()
|
||||
->map(fn ($msg) => [
|
||||
'key' => $msg->id,
|
||||
'role' => $msg->role,
|
||||
'content' => $msg->content,
|
||||
'created_at' => $msg->created_at->toISOString(),
|
||||
]);
|
||||
|
||||
return $this->success($messages->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定会话
|
||||
*/
|
||||
#[DeleteRoute('/messages/{conversationId}', 'delete')]
|
||||
public function deleteConversation(Request $request, string $conversationId): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$conversation = $user->conversations()->find($conversationId);
|
||||
|
||||
if (! $conversation) {
|
||||
return $this->error('会话不存在');
|
||||
}
|
||||
|
||||
$conversation->delete();
|
||||
|
||||
return $this->success('会话已删除');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemAgent\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Ai\Models\Conversation;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemUser\Models\SysUserModel;
|
||||
|
||||
#[RequestAttribute('/ai/conversation', 'ai.conversation')]
|
||||
class ConversationController extends BaseController
|
||||
{
|
||||
protected array $searchField = [
|
||||
'title' => 'like',
|
||||
];
|
||||
|
||||
protected array $quickSearchField = ['title'];
|
||||
|
||||
/**
|
||||
* 会话列表
|
||||
*/
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(Request $request): JsonResponse
|
||||
{
|
||||
$params = $request->all();
|
||||
$perPage = (int) ($params['pageSize'] ?? 10);
|
||||
$query = Conversation::query()->withCount('messages');
|
||||
|
||||
$data = $this->buildSearch($params, $query)
|
||||
->orderBy('updated_at', 'desc')
|
||||
->paginate($perPage);
|
||||
|
||||
$userIds = $data->pluck('user_id')->filter()->unique();
|
||||
$users = SysUserModel::whereIn('id', $userIds)->pluck('username', 'id');
|
||||
|
||||
$data = $data->through(function ($conversation) use ($users) {
|
||||
return [
|
||||
'id' => $conversation->id,
|
||||
'user_id' => $conversation->user_id,
|
||||
'username' => $users[$conversation->user_id] ?? '',
|
||||
'title' => $conversation->title,
|
||||
'message_count' => $conversation->messages_count,
|
||||
'created_at' => $conversation->created_at?->toISOString(),
|
||||
'updated_at' => $conversation->updated_at?->toISOString(),
|
||||
];
|
||||
});
|
||||
|
||||
return $this->success($data->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*/
|
||||
#[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[a-zA-Z0-9\-]+'])]
|
||||
public function delete(string $id): JsonResponse
|
||||
{
|
||||
$conversation = Conversation::find($id);
|
||||
|
||||
if (! $conversation) {
|
||||
return $this->error('会话不存在');
|
||||
}
|
||||
|
||||
$conversation->delete();
|
||||
|
||||
return $this->success('会话已删除');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话消息列表
|
||||
*/
|
||||
#[GetRoute('/{id}/messages', authorize: 'query', where: ['id' => '[a-zA-Z0-9\-]+'])]
|
||||
public function messages(string $id, Request $request): JsonResponse
|
||||
{
|
||||
$conversation = Conversation::find($id);
|
||||
|
||||
if (! $conversation) {
|
||||
return $this->error('会话不存在');
|
||||
}
|
||||
|
||||
$perPage = (int) $request->input('pageSize', 20);
|
||||
$data = $conversation->messages()
|
||||
->orderBy('created_at')
|
||||
->paginate($perPage)
|
||||
->toArray();
|
||||
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话详情
|
||||
*/
|
||||
#[GetRoute('/{id}', authorize: 'query', where: ['id' => '[a-zA-Z0-9\-]+'])]
|
||||
public function show(string $id): JsonResponse
|
||||
{
|
||||
$conversation = Conversation::withCount('messages')->find($id);
|
||||
|
||||
if (! $conversation) {
|
||||
return $this->error('会话不存在');
|
||||
}
|
||||
|
||||
$user = $conversation->user_id
|
||||
? SysUserModel::find($conversation->user_id)
|
||||
: null;
|
||||
|
||||
return $this->success([
|
||||
'id' => $conversation->id,
|
||||
'user_id' => $conversation->user_id,
|
||||
'username' => $user?->username ?? '',
|
||||
'title' => $conversation->title,
|
||||
'message_count' => $conversation->messages_count,
|
||||
'created_at' => $conversation->created_at?->toISOString(),
|
||||
'updated_at' => $conversation->updated_at?->toISOString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemAgent\Http\Requests;
|
||||
|
||||
use Modules\Common\Http\Requests\BaseFormRequest;
|
||||
|
||||
class AgentFormRequest extends BaseFormRequest
|
||||
{
|
||||
protected $stopOnFirstFailure = true;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'enabled' => 'required|boolean',
|
||||
'name' => 'nullable|max:100',
|
||||
'description' => 'nullable|max:1000',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'enabled.required' => '启用状态不能为空',
|
||||
'enabled.boolean' => '启用状态格式错误',
|
||||
'name.max' => '名称不能超过100个字符',
|
||||
'description.max' => '描述不能超过1000个字符',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemAgent\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AgentModel extends Model
|
||||
{
|
||||
protected $table = 'agents';
|
||||
|
||||
protected $fillable = [
|
||||
'namespace',
|
||||
'name',
|
||||
'icon',
|
||||
'description',
|
||||
'tags',
|
||||
'enabled',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'tags' => 'array',
|
||||
'enabled' => 'boolean',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemAgent\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\AnnoRoute\AnnoRoute;
|
||||
|
||||
class SystemAgentServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(AnnoRoute $annoRoute): void
|
||||
{
|
||||
$annoRoute->register(base_path('modules/SystemAgent/Http/Controllers'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Ai\Agents;
|
||||
|
||||
use Laravel\Ai\Concerns\RemembersConversations;
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\Conversational;
|
||||
use Laravel\Ai\Contracts\HasTools;
|
||||
use Laravel\Ai\Contracts\Tool;
|
||||
use Laravel\Ai\Messages\Message;
|
||||
use Laravel\Ai\Promptable;
|
||||
use Stringable;
|
||||
|
||||
class TestAgent implements Agent, Conversational, HasTools
|
||||
{
|
||||
use Promptable, RemembersConversations;
|
||||
|
||||
/**
|
||||
* Get the instructions that the agent should follow.
|
||||
*/
|
||||
public function instructions(): Stringable|string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of messages comprising the conversation so far.
|
||||
*
|
||||
* @return Message[]
|
||||
*/
|
||||
public function messages(): iterable
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tools available to the agent.
|
||||
*
|
||||
* @return Tool[]
|
||||
*/
|
||||
public function tools(): iterable
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\SystemTool\Ai\Boots;
|
||||
|
||||
use Laravel\Boost\Contracts\SupportsGuidelines;
|
||||
use Laravel\Boost\Contracts\SupportsMcp;
|
||||
use Laravel\Boost\Contracts\SupportsSkills;
|
||||
use Laravel\Boost\Install\Agents\Agent;
|
||||
use Laravel\Boost\Install\Enums\McpInstallationStrategy;
|
||||
use Laravel\Boost\Install\Enums\Platform;
|
||||
|
||||
class Reasonix extends Agent implements SupportsGuidelines, SupportsMcp, SupportsSkills
|
||||
{
|
||||
public function name(): string
|
||||
{
|
||||
return 'reasonix';
|
||||
}
|
||||
|
||||
public function displayName(): string
|
||||
{
|
||||
return 'Reasonix';
|
||||
}
|
||||
|
||||
public function systemDetectionConfig(Platform $platform): array
|
||||
{
|
||||
return match ($platform) {
|
||||
Platform::Darwin, Platform::Linux => [
|
||||
'command' => 'command -v reasonix',
|
||||
],
|
||||
Platform::Windows => [
|
||||
'command' => 'cmd /c where reasonix 2>nul',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
public function projectDetectionConfig(): array
|
||||
{
|
||||
return [
|
||||
'paths' => ['.reasonix'],
|
||||
'files' => ['REASONIX.md'],
|
||||
];
|
||||
}
|
||||
|
||||
public function mcpInstallationStrategy(): McpInstallationStrategy
|
||||
{
|
||||
return McpInstallationStrategy::FILE;
|
||||
}
|
||||
|
||||
public function mcpConfigPath(): string
|
||||
{
|
||||
return config('boost.agents.reasonix.mcp_config_path', '.mcp.json');
|
||||
}
|
||||
|
||||
public function guidelinesPath(): string
|
||||
{
|
||||
return config('boost.agents.reasonix.guidelines_path', 'REASONIX.md');
|
||||
}
|
||||
|
||||
public function skillsPath(): string
|
||||
{
|
||||
return config('boost.agents.reasonix.skills_path', '.reasonix/skills');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Attributes;
|
||||
|
||||
use Attribute;
|
||||
use Modules\SystemTool\Enum\ESettingType;
|
||||
|
||||
/**
|
||||
* 在 SettingsDefinition 子类上声明配置项
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
|
||||
class Setting
|
||||
{
|
||||
public function __construct(
|
||||
public string $config,
|
||||
public ESettingType $type = ESettingType::String,
|
||||
public ?string $description = null,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Base;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use InvalidArgumentException;
|
||||
use Modules\SystemTool\Attributes\Setting;
|
||||
use Modules\SystemTool\Enum\ESettingType;
|
||||
use ReflectionClass;
|
||||
|
||||
/**
|
||||
* 设置定义基类
|
||||
*/
|
||||
abstract class SettingsDefinition
|
||||
{
|
||||
/**
|
||||
* 聚合缓存键名 — 全局中间件 LoadAppSettingsMiddleware 使用
|
||||
* 所有设置一次性缓存在此键中,set() 写入时自动清除。
|
||||
*/
|
||||
const AGGREGATE_CACHE_KEY = 'app-settings-all';
|
||||
|
||||
/**
|
||||
* 解析后的定义缓存(类名 → 定义数组)
|
||||
*
|
||||
* @var array<string, array<string, array{config: string, type: string, description: string|null}>>
|
||||
*/
|
||||
protected static array $definitions = [];
|
||||
|
||||
/**
|
||||
* 从 #[Setting] Attribute 解析设置定义
|
||||
*
|
||||
* @return array<string, array{config: string, type: string, description: string|null}>
|
||||
*/
|
||||
public static function getDefinition(): array
|
||||
{
|
||||
$class = static::class;
|
||||
|
||||
if (! isset(self::$definitions[$class])) {
|
||||
$reflection = new ReflectionClass($class);
|
||||
$attributes = $reflection->getAttributes(Setting::class);
|
||||
$definition = [];
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
/** @var Setting $instance */
|
||||
$instance = $attribute->newInstance();
|
||||
$definition[$instance->config] = [
|
||||
'config' => $instance->config,
|
||||
'type' => $instance->type->value,
|
||||
'description' => $instance->description,
|
||||
];
|
||||
}
|
||||
|
||||
self::$definitions[$class] = $definition;
|
||||
}
|
||||
|
||||
return self::$definitions[$class];
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断某个 key 是否在本类的定义中
|
||||
*/
|
||||
public static function hasDefinitionKey(string $key): bool
|
||||
{
|
||||
return array_key_exists($key, static::getDefinition());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据库表名
|
||||
*/
|
||||
public static function getTableName(): string
|
||||
{
|
||||
return config('app_settings.table', 'sys_app_settings');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将本类的所有配置项定义同步到 应用配置 表
|
||||
*
|
||||
* - 新 key 插入,值来自当前 config() 的默认值
|
||||
* - 已有 key 只更新 description,不覆盖值
|
||||
* - 部署时在 migration 中调用
|
||||
*/
|
||||
public static function init(): void
|
||||
{
|
||||
$definition = static::getDefinition();
|
||||
$table = static::getTableName();
|
||||
|
||||
foreach ($definition as $key => $setting) {
|
||||
$exists = DB::table($table)->where('key', $key)->exists();
|
||||
|
||||
if (! $exists) {
|
||||
$value = static::getConfigValue($key);
|
||||
static::set($key, $value);
|
||||
} else {
|
||||
DB::table($table)
|
||||
->where('key', $key)
|
||||
->update([
|
||||
'description' => $setting['description'] ?? null,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取配置值
|
||||
*
|
||||
* @throws InvalidArgumentException 当 key 未定义时
|
||||
*/
|
||||
public static function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
if (! static::hasDefinitionKey($key)) {
|
||||
throw new InvalidArgumentException("Setting key '{$key}' is not defined in " . static::class);
|
||||
}
|
||||
|
||||
return static::getCacheValue($key, function () use ($key, $default) {
|
||||
return static::getDBValue($key, function () use ($key, $default) {
|
||||
return static::setCacheValue(
|
||||
key: $key,
|
||||
value: static::getConfigValue($key, $default),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入配置值 → DB + Cache
|
||||
*
|
||||
* @throws InvalidArgumentException 当 key 未定义或类型不匹配时
|
||||
*/
|
||||
public static function set(string $key, mixed $value): void
|
||||
{
|
||||
if (! static::hasDefinitionKey($key)) {
|
||||
throw new InvalidArgumentException("Setting key '{$key}' is not defined in " . static::class);
|
||||
}
|
||||
|
||||
static::setDBValue($key, $value);
|
||||
static::setCacheValue($key, $value);
|
||||
|
||||
// 当前请求立即生效(中间件在下一个请求才会重新加载)
|
||||
config([$key => $value]);
|
||||
|
||||
// 清除聚合缓存,使下次请求通过中间件重新从 DB 加载
|
||||
Cache::forget(self::AGGREGATE_CACHE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Laravel config 读取(最终 fallback)
|
||||
*/
|
||||
public static function getConfigValue(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return config($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存读取
|
||||
*
|
||||
* @param mixed $default 默认值或闭包 (fn() => mixed)
|
||||
*/
|
||||
public static function getCacheValue(string $key, mixed $default = null): mixed
|
||||
{
|
||||
if ($default instanceof \Closure) {
|
||||
return Cache::rememberForever('app-setting-' . $key, $default);
|
||||
}
|
||||
|
||||
return Cache::get('app-setting-' . $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入缓存
|
||||
*/
|
||||
public static function setCacheValue(string $key, mixed $value, ?int $ttl = null): mixed
|
||||
{
|
||||
if ($ttl !== null) {
|
||||
Cache::put('app-setting-' . $key, $value, $ttl);
|
||||
} else {
|
||||
Cache::forever('app-setting-' . $key, $value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将本类所有配置项从 DB 加载到 Laravel config() 运行时
|
||||
*
|
||||
* 调用后 config('filesystems.default') 等可直接返回 DB 值。
|
||||
* 在 getConfig() 批量读取场景下避免逐个 get() 的开销。
|
||||
*/
|
||||
public static function reloadIntoConfig(): void
|
||||
{
|
||||
$table = static::getTableName();
|
||||
$rows = DB::table($table)->whereIn('key', array_keys(static::getDefinition()))->get();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$value = match ((int) $row->type) {
|
||||
ESettingType::String->value => $row->s,
|
||||
ESettingType::Bool->value => is_null($row->n) ? null : (bool) $row->n,
|
||||
ESettingType::Number->value => is_null($row->n) ? null : (int) $row->n,
|
||||
ESettingType::Array->value => is_null($row->e) ? null : json_decode($row->e, true),
|
||||
ESettingType::Object->value => is_null($row->e) ? null : unserialize(base64_decode($row->e)),
|
||||
ESettingType::EncryptedString->value => is_null($row->e) ? null : \Illuminate\Support\Facades\Crypt::decrypt(base64_decode($row->e)),
|
||||
default => $row->s ?? null,
|
||||
};
|
||||
|
||||
config([$row->key => $value]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除某个 key 的缓存
|
||||
*/
|
||||
public static function forgetCache(string $key): void
|
||||
{
|
||||
Cache::forget('app-setting-' . $key);
|
||||
|
||||
// 同时清除聚合缓存,保持一致性
|
||||
Cache::forget(self::AGGREGATE_CACHE_KEY);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从数据库读取(按类型自动转换)
|
||||
*
|
||||
* @param mixed $default 默认值或闭包
|
||||
*/
|
||||
public static function getDBValue(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$rec = DB::table(static::getTableName())->where('key', $key)->first();
|
||||
|
||||
if (! $rec) {
|
||||
return $default instanceof \Closure ? $default() : value($default);
|
||||
}
|
||||
|
||||
return match ((int) $rec->type) {
|
||||
ESettingType::String->value => $rec->s,
|
||||
ESettingType::Bool->value => is_null($rec->n) ? null : (bool) $rec->n,
|
||||
ESettingType::Number->value => is_null($rec->n) ? null : (int) $rec->n,
|
||||
ESettingType::Array->value => is_null($rec->e) ? null : json_decode($rec->e, true),
|
||||
ESettingType::Object->value => is_null($rec->e) ? null : unserialize(base64_decode($rec->e)),
|
||||
ESettingType::EncryptedString->value => is_null($rec->e) ? null : Crypt::decrypt(base64_decode($rec->e)),
|
||||
default => $rec->s ?? $rec->value ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入数据库(按类型选择列)
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public static function setDBValue(string $key, mixed $value): void
|
||||
{
|
||||
$definition = static::getDefinition();
|
||||
|
||||
if (! isset($definition[$key])) {
|
||||
throw new InvalidArgumentException("Setting key '{$key}' is not defined in " . static::class);
|
||||
}
|
||||
|
||||
$type = $definition[$key]['type'];
|
||||
$description = $definition[$key]['description'] ?? null;
|
||||
$table = static::getTableName();
|
||||
|
||||
$data = [
|
||||
'key' => $key,
|
||||
'type' => $type,
|
||||
'description' => $description,
|
||||
'updated_at' => now(),
|
||||
];
|
||||
|
||||
switch ((int) $type) {
|
||||
case ESettingType::String->value:
|
||||
if (! is_string($value) && ! is_null($value)) {
|
||||
throw new InvalidArgumentException("Value for '{$key}' must be a string.");
|
||||
}
|
||||
$data['s'] = $value;
|
||||
$data['n'] = null;
|
||||
$data['e'] = null;
|
||||
break;
|
||||
|
||||
case ESettingType::Bool->value:
|
||||
if (! is_bool($value) && ! is_null($value)) {
|
||||
throw new InvalidArgumentException("Value for '{$key}' must be a boolean.");
|
||||
}
|
||||
$data['n'] = is_null($value) ? null : (int) $value;
|
||||
$data['s'] = null;
|
||||
$data['e'] = null;
|
||||
break;
|
||||
|
||||
case ESettingType::Number->value:
|
||||
if (! is_numeric($value) && ! is_null($value)) {
|
||||
throw new InvalidArgumentException("Value for '{$key}' must be a number.");
|
||||
}
|
||||
$data['n'] = $value;
|
||||
$data['s'] = null;
|
||||
$data['e'] = null;
|
||||
break;
|
||||
|
||||
case ESettingType::Array->value:
|
||||
if (! is_array($value) && ! is_null($value)) {
|
||||
throw new InvalidArgumentException("Value for '{$key}' must be an array.");
|
||||
}
|
||||
$data['e'] = is_null($value) ? null : json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
$data['s'] = null;
|
||||
$data['n'] = null;
|
||||
break;
|
||||
|
||||
case ESettingType::Object->value:
|
||||
if (! is_object($value) && ! is_null($value)) {
|
||||
throw new InvalidArgumentException("Value for '{$key}' must be an object.");
|
||||
}
|
||||
$data['e'] = is_null($value) ? null : base64_encode(serialize($value));
|
||||
$data['s'] = null;
|
||||
$data['n'] = null;
|
||||
break;
|
||||
|
||||
case ESettingType::EncryptedString->value:
|
||||
if (! is_string($value) && ! is_null($value)) {
|
||||
throw new InvalidArgumentException("Value for '{$key}' must be a string.");
|
||||
}
|
||||
$data['e'] = is_null($value) ? null : base64_encode(Crypt::encrypt($value));
|
||||
$data['s'] = null;
|
||||
$data['n'] = null;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidArgumentException("Unknown setting type '{$type}' for key '{$key}'.");
|
||||
}
|
||||
|
||||
$exists = DB::table($table)->where('key', $key)->exists();
|
||||
|
||||
if ($exists) {
|
||||
DB::table($table)->where('key', $key)->update($data);
|
||||
} else {
|
||||
$data['created_at'] = now();
|
||||
DB::table($table)->insert($data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库删除某个配置项
|
||||
*/
|
||||
public static function deleteDBValue(string $key): void
|
||||
{
|
||||
DB::table(static::getTableName())->where('key', $key)->delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Enum;
|
||||
|
||||
/**
|
||||
* 设置类型枚举
|
||||
*
|
||||
* 对应 应用设置 表的 type 列,决定值存储在 s / n / e 哪个字段。
|
||||
*/
|
||||
enum ESettingType: int
|
||||
{
|
||||
case String = 10;
|
||||
case Bool = 15;
|
||||
case Number = 20;
|
||||
case Array = 30;
|
||||
case Object = 40;
|
||||
case EncryptedString = 50;
|
||||
|
||||
/**
|
||||
* 是否为字符串类型
|
||||
*/
|
||||
public function isString(): bool
|
||||
{
|
||||
return $this === self::String;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为数字/布尔类型(存储在 n 列)
|
||||
*/
|
||||
public function isNumeric(): bool
|
||||
{
|
||||
return in_array($this, [self::Bool, self::Number], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为扩展类型(存储在 e 列)
|
||||
*/
|
||||
public function isExtended(): bool
|
||||
{
|
||||
return in_array($this, [self::Array, self::Object, self::EncryptedString], true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Enum;
|
||||
|
||||
/**
|
||||
* 枚举类:文件类型
|
||||
* Class FileType
|
||||
*/
|
||||
enum FileType: int
|
||||
{
|
||||
// 图片
|
||||
case IMAGE = 10;
|
||||
|
||||
// 音频
|
||||
case AUDIO = 20;
|
||||
|
||||
// 视频
|
||||
case VIDEO = 30;
|
||||
|
||||
// 压缩包
|
||||
case ZIP = 40;
|
||||
|
||||
// 文档
|
||||
case DOCUMENT = 50;
|
||||
|
||||
// 未知文件
|
||||
case ANNEX = 99;
|
||||
|
||||
/**
|
||||
* 获取类型名称
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::IMAGE => __('system.file.image'),
|
||||
self::AUDIO => __('system.file.audio'),
|
||||
self::VIDEO => __('system.file.video'),
|
||||
self::ZIP => __('system.file.zip'),
|
||||
self::DOCUMENT => __('system.file.document'),
|
||||
self::ANNEX => __('system.file.annex'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览地址
|
||||
*/
|
||||
public function previewPath(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::IMAGE => 'static/image.svg',
|
||||
self::AUDIO => 'static/audio.svg',
|
||||
self::VIDEO => 'static/video.svg',
|
||||
self::ZIP => 'static/zip.svg',
|
||||
self::DOCUMENT => 'static/document.svg',
|
||||
self::ANNEX => 'static/annex.svg',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据扩展名推断文件类型
|
||||
*/
|
||||
public static function guessFromExtension(string $extension): self
|
||||
{
|
||||
$extension = strtolower($extension);
|
||||
|
||||
foreach (self::cases() as $case) {
|
||||
if ($case === self::ANNEX) continue; // 跳过 OTHER
|
||||
|
||||
if (in_array($extension, $case->fileExt())) {
|
||||
return $case;
|
||||
}
|
||||
}
|
||||
|
||||
return self::ANNEX;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件扩展名
|
||||
*/
|
||||
public function fileExt(): array|string
|
||||
{
|
||||
return match ($this) {
|
||||
self::IMAGE => ['jpg', 'jpeg', 'png', 'bmp', 'gif', 'avif', 'webp', 'svg', 'ico'],
|
||||
self::AUDIO => ['mp3', 'wma', 'wav', 'ape', 'flac', 'ogg', 'aac'],
|
||||
self::VIDEO => ['mp4', 'mov', 'wmv', 'flv', 'avl', 'webm', 'mkv'],
|
||||
self::DOCUMENT => ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'csv'],
|
||||
self::ZIP => ['zip', 'rar', '7z', 'tar', 'gz'],
|
||||
self::ANNEX => '*',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Enum;
|
||||
|
||||
/**
|
||||
* 系统设置类型枚举
|
||||
*/
|
||||
enum SiteConfigType: string
|
||||
{
|
||||
// 前端表单组件类型(新版,与前端保持一致)
|
||||
case INPUT = 'Input'; // 输入框
|
||||
case TEXTAREA = 'TextArea'; // 文本域
|
||||
case INPUT_NUMBER = 'InputNumber'; // 数字输入框
|
||||
case SWITCH = 'Switch'; // 开关
|
||||
case RADIO = 'Radio'; // 单选框
|
||||
case CHECKBOX = 'Checkbox'; // 复选框
|
||||
|
||||
/**
|
||||
* 从字符串创建枚举实例(宽松匹配)
|
||||
*/
|
||||
public static function fromString(string $type): ?self
|
||||
{
|
||||
return self::tryFrom($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类型自动转换值
|
||||
*
|
||||
* @param mixed $value 原始值
|
||||
* @return mixed 转换后的值
|
||||
*/
|
||||
public function castValue(mixed $value): mixed
|
||||
{
|
||||
// 如果值为 null,直接返回 null
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match($this) {
|
||||
// 数字类型:转换为整数或浮点数
|
||||
self::INPUT_NUMBER => $this->castToNumber($value),
|
||||
|
||||
// 布尔类型:转换为布尔值
|
||||
self::SWITCH => $this->castToBoolean($value),
|
||||
|
||||
// 数组类型:转换为数组
|
||||
self::CHECKBOX => $this->castToArray($value),
|
||||
|
||||
// 默认:保持字符串(去除首尾空白)
|
||||
self::INPUT, self::TEXTAREA, self::RADIO => $value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为数字(整数或浮点数)
|
||||
*/
|
||||
private function castToNumber(mixed $value): int|float|null
|
||||
{
|
||||
// 如果是数字字符串或数字
|
||||
if (is_numeric($value)) {
|
||||
// 如果包含小数点,转换为浮点数,否则转换为整数
|
||||
return str_contains((string)$value, '.')
|
||||
? (float)$value
|
||||
: (int)$value;
|
||||
}
|
||||
|
||||
// 如果是布尔值
|
||||
if (is_bool($value)) {
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
// 其他情况返回 null 或 0?这里返回 null 表示无效转换
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为布尔值
|
||||
*/
|
||||
private function castToBoolean(mixed $value): bool
|
||||
{
|
||||
// 如果是字符串
|
||||
if (is_string($value)) {
|
||||
$lowerValue = strtolower(trim($value));
|
||||
// 检查是否为 true 相关的字符串
|
||||
if (in_array($lowerValue, ['true', '1', 'on', 'yes', 'y'], true)) {
|
||||
return true;
|
||||
}
|
||||
// 检查是否为 false 相关的字符串
|
||||
if (in_array($lowerValue, ['false', '0', 'off', 'no', 'n', ''], true)) {
|
||||
return false;
|
||||
}
|
||||
// 默认转换为布尔值
|
||||
return (bool)$value;
|
||||
}
|
||||
|
||||
// 如果是数字
|
||||
if (is_numeric($value)) {
|
||||
return (bool)$value;
|
||||
}
|
||||
|
||||
// 其他类型直接转换为布尔值
|
||||
return (bool)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为数组
|
||||
*/
|
||||
private function castToArray(mixed $value): array
|
||||
{
|
||||
// 如果已经是数组,直接返回
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// 如果是 JSON 字符串
|
||||
if (is_string($value)) {
|
||||
$trimmed = trim($value);
|
||||
// 检查是否为 JSON 数组格式
|
||||
if ((str_starts_with($trimmed, '[') && str_ends_with($trimmed, ']')) ||
|
||||
(str_starts_with($trimmed, '{') && str_ends_with($trimmed, '}'))) {
|
||||
$decoded = json_decode($trimmed, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试按逗号分割(支持中文逗号和英文逗号)
|
||||
if (str_contains($trimmed, ',') || str_contains($trimmed, ',')) {
|
||||
$separator = str_contains($trimmed, ',') ? ',' : ',';
|
||||
return array_map('trim', explode($separator, $trimmed));
|
||||
}
|
||||
|
||||
// 单个值转为数组
|
||||
return [$trimmed];
|
||||
}
|
||||
|
||||
// 其他类型转为数组
|
||||
return (array)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为字符串
|
||||
*/
|
||||
private function castToString(mixed $value): string
|
||||
{
|
||||
if (is_array($value)) {
|
||||
// 数组转为 JSON 字符串
|
||||
return json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
if (is_bool($value)) {
|
||||
return $value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return trim((string)$value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Ai\Enums\Lab;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Laravel\Ai\Responses\AgentResponse;
|
||||
use Laravel\Ai\Responses\StreamableAgentResponse;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemTool\Ai\Agents\TestAgent;
|
||||
use Modules\SystemTool\Settings\AiSettings;
|
||||
|
||||
/**
|
||||
* AI 服务配置管理
|
||||
*/
|
||||
#[RequestAttribute('/system/ai', 'system.ai')]
|
||||
class SysAiController extends BaseController
|
||||
{
|
||||
/** 获取支持的AI */
|
||||
#[GetRoute('/list', 'list')]
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$default = [Lab::Anthropic, Lab::OpenAI, Lab::Gemini, Lab::Azure, Lab::Bedrock, Lab::Groq, Lab::xAI, Lab::DeepSeek, Lab::Mistral, Lab::Ollama, Lab::OpenRouter ];
|
||||
return $this->success(compact('default'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 AI 配置(从 DB 加载已保存的值,fallback 到 config 文件)
|
||||
*/
|
||||
#[GetRoute('/config', 'config')]
|
||||
public function getConfig(): JsonResponse
|
||||
{
|
||||
$ai = config('ai');
|
||||
|
||||
return $this->success([
|
||||
'default' => $ai['default'] ?? 'openai',
|
||||
'providers' => $ai['providers'] ?? [],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 AI 配置到数据库
|
||||
*/
|
||||
#[PostRoute('/save', 'save')]
|
||||
public function saveConfig(): JsonResponse
|
||||
{
|
||||
$data = request()->all();
|
||||
|
||||
try {
|
||||
// 默认驱动
|
||||
AiSettings::set('ai.default', $data['default'] ?? 'openai');
|
||||
|
||||
// 各供应商配置
|
||||
$providers = $data['providers'] ?? [];
|
||||
|
||||
// Anthropic
|
||||
if (isset($providers['anthropic'])) {
|
||||
$p = $providers['anthropic'];
|
||||
AiSettings::set('ai.providers.anthropic.key', $p['key'] ?? '');
|
||||
if (isset($p['url'])) {
|
||||
AiSettings::set('ai.providers.anthropic.url', $p['url']);
|
||||
}
|
||||
}
|
||||
|
||||
// Azure
|
||||
if (isset($providers['azure'])) {
|
||||
$p = $providers['azure'];
|
||||
AiSettings::set('ai.providers.azure.key', $p['key'] ?? '');
|
||||
AiSettings::set('ai.providers.azure.url', $p['url'] ?? '');
|
||||
AiSettings::set('ai.providers.azure.api_version', $p['api_version'] ?? '2025-04-01-preview');
|
||||
AiSettings::set('ai.providers.azure.deployment', $p['deployment'] ?? 'gpt-4o');
|
||||
AiSettings::set('ai.providers.azure.embedding_deployment', $p['embedding_deployment'] ?? 'text-embedding-3-small');
|
||||
AiSettings::set('ai.providers.azure.image_deployment', $p['image_deployment'] ?? 'gpt-image-1');
|
||||
}
|
||||
|
||||
// Bedrock
|
||||
if (isset($providers['bedrock'])) {
|
||||
$p = $providers['bedrock'];
|
||||
AiSettings::set('ai.providers.bedrock.region', $p['region'] ?? 'us-east-1');
|
||||
AiSettings::set('ai.providers.bedrock.key', $p['key'] ?? '');
|
||||
AiSettings::set('ai.providers.bedrock.access_key_id', $p['access_key_id'] ?? '');
|
||||
AiSettings::set('ai.providers.bedrock.secret_access_key', $p['secret_access_key'] ?? '');
|
||||
AiSettings::set('ai.providers.bedrock.session_token', $p['session_token'] ?? '');
|
||||
}
|
||||
|
||||
// Cohere
|
||||
if (isset($providers['cohere'])) {
|
||||
AiSettings::set('ai.providers.cohere.key', $providers['cohere']['key'] ?? '');
|
||||
}
|
||||
|
||||
// DeepSeek
|
||||
if (isset($providers['deepseek'])) {
|
||||
AiSettings::set('ai.providers.deepseek.key', $providers['deepseek']['key'] ?? '');
|
||||
}
|
||||
|
||||
// ElevenLabs
|
||||
if (isset($providers['eleven'])) {
|
||||
AiSettings::set('ai.providers.eleven.key', $providers['eleven']['key'] ?? '');
|
||||
}
|
||||
|
||||
// Gemini
|
||||
if (isset($providers['gemini'])) {
|
||||
$p = $providers['gemini'];
|
||||
AiSettings::set('ai.providers.gemini.key', $p['key'] ?? '');
|
||||
if (isset($p['url'])) {
|
||||
AiSettings::set('ai.providers.gemini.url', $p['url']);
|
||||
}
|
||||
}
|
||||
|
||||
// Groq
|
||||
if (isset($providers['groq'])) {
|
||||
AiSettings::set('ai.providers.groq.key', $providers['groq']['key'] ?? '');
|
||||
}
|
||||
|
||||
// Jina
|
||||
if (isset($providers['jina'])) {
|
||||
AiSettings::set('ai.providers.jina.key', $providers['jina']['key'] ?? '');
|
||||
}
|
||||
|
||||
// Mistral
|
||||
if (isset($providers['mistral'])) {
|
||||
AiSettings::set('ai.providers.mistral.key', $providers['mistral']['key'] ?? '');
|
||||
}
|
||||
|
||||
// Ollama
|
||||
if (isset($providers['ollama'])) {
|
||||
$p = $providers['ollama'];
|
||||
AiSettings::set('ai.providers.ollama.key', $p['key'] ?? '');
|
||||
AiSettings::set('ai.providers.ollama.url', $p['url'] ?? 'http://localhost:11434');
|
||||
}
|
||||
|
||||
// OpenAI
|
||||
if (isset($providers['openai'])) {
|
||||
$p = $providers['openai'];
|
||||
AiSettings::set('ai.providers.openai.key', $p['key'] ?? '');
|
||||
if (isset($p['url'])) {
|
||||
AiSettings::set('ai.providers.openai.url', $p['url']);
|
||||
}
|
||||
}
|
||||
|
||||
// OpenRouter
|
||||
if (isset($providers['openrouter'])) {
|
||||
AiSettings::set('ai.providers.openrouter.key', $providers['openrouter']['key'] ?? '');
|
||||
}
|
||||
|
||||
// VoyageAI
|
||||
if (isset($providers['voyageai'])) {
|
||||
AiSettings::set('ai.providers.voyageai.key', $providers['voyageai']['key'] ?? '');
|
||||
}
|
||||
|
||||
// xAI
|
||||
if (isset($providers['xai'])) {
|
||||
AiSettings::set('ai.providers.xai.key', $providers['xai']['key'] ?? '');
|
||||
}
|
||||
|
||||
// 清除 Laravel 配置缓存
|
||||
Artisan::call('config:clear');
|
||||
|
||||
return $this->success('保存成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error('保存失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 AI 供应商连接
|
||||
*/
|
||||
#[PostRoute('/test', 'test')]
|
||||
public function testConnection(Request $request): StreamableAgentResponse | JsonResponse
|
||||
{
|
||||
try {
|
||||
$agent = TestAgent::make()->forUser($request->user());
|
||||
return $agent->stream('Hello, Who are you?');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error('连接测试失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemTool\Http\Requests\SysCarouselFormRequest;
|
||||
use Modules\SystemTool\Models\SysCarouselModel;
|
||||
|
||||
/**
|
||||
* 首页轮播图管理
|
||||
*/
|
||||
#[RequestAttribute('/system/carousel', 'system.carousel')]
|
||||
class SysCarouselController extends BaseController
|
||||
{
|
||||
protected array $searchField = [
|
||||
'status' => '=',
|
||||
];
|
||||
|
||||
protected array $quickSearchField = ['title'];
|
||||
|
||||
/** 查询轮播图列表 */
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(Request $request): JsonResponse
|
||||
{
|
||||
$query = SysCarouselModel::query();
|
||||
$data = $this->buildSearch($request->all(), $query)
|
||||
->orderBy('sort', 'desc')
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($request->input('pageSize', 10))
|
||||
->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/** 创建轮播图 */
|
||||
#[PostRoute(authorize: 'create')]
|
||||
public function create(SysCarouselFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysCarouselModel::create($validated);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 编辑轮播图 */
|
||||
#[PutRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'update',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function update(int $id, SysCarouselFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysCarouselModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error('轮播图不存在');
|
||||
}
|
||||
$model->update($validated);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 删除轮播图 */
|
||||
#[DeleteRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'delete',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function delete(int $id): JsonResponse
|
||||
{
|
||||
$model = SysCarouselModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error('轮播图不存在');
|
||||
}
|
||||
$model->delete();
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Controllers;
|
||||
|
||||
use App\Exceptions\RepositoryException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemTool\Http\Requests\SysDictFormRequest;
|
||||
use Modules\SystemTool\Models\SysDictModel;
|
||||
|
||||
/**
|
||||
* 字典管理
|
||||
*/
|
||||
#[RequestAttribute('/system/dict/list', 'system.dict.list')]
|
||||
class SysDictController extends BaseController
|
||||
{
|
||||
protected array $quickSearchField = ['name', 'code', 'describe'];
|
||||
protected array $searchField = [
|
||||
'status' => '='
|
||||
];
|
||||
|
||||
/** 查询字典列表 */
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(Request $request): JsonResponse
|
||||
{
|
||||
$params = $request->all();
|
||||
$pageSize = $params['pageSize'] ?? 10;
|
||||
$query = SysDictModel::query();
|
||||
$data = $this->buildSearch($params, $query)
|
||||
->paginate($pageSize)
|
||||
->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/** 创建字典 */
|
||||
#[PostRoute(authorize: 'create')]
|
||||
public function create(SysDictFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysDictModel::create($validated);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 编辑字典 */
|
||||
#[PutRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'update',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function update(int $id, SysDictFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysDictModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
$model->update($validated);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 删除字典 */
|
||||
#[DeleteRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'delete',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function delete(int $id): JsonResponse
|
||||
{
|
||||
$model = SysDictModel::find($id);
|
||||
if (empty($model)) {
|
||||
throw new RepositoryException('字典不存在');
|
||||
}
|
||||
$count = $model->dictItems()->count();
|
||||
if ($count > 0) {
|
||||
throw new RepositoryException('字典包含子项,请先删除子项!');
|
||||
}
|
||||
$model->delete();
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 获取所有字典数据 */
|
||||
#[GetRoute('/all', false)]
|
||||
public function all(): JsonResponse
|
||||
{
|
||||
$data = SysDictModel::getAllDictWithItems();
|
||||
return $this->success($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemTool\Http\Requests\SysDictItemFormRequest;
|
||||
use Modules\SystemTool\Models\SysDictItemModel;
|
||||
|
||||
/**
|
||||
* 字典项控制器
|
||||
*/
|
||||
#[RequestAttribute('/system/dict/item', 'system.dict.item')]
|
||||
class SysDictItemController extends BaseController
|
||||
{
|
||||
protected array $quickSearchField = ['label', 'value'];
|
||||
protected array $searchField = [
|
||||
'dict_id' => '=',
|
||||
'status' => '='
|
||||
];
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
/** 查询字典项列表 */
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(Request $request): JsonResponse
|
||||
{
|
||||
$params = $request->all();
|
||||
$pageSize = $params['pageSize'] ?? 10;
|
||||
$query = SysDictItemModel::query();
|
||||
$data = $this->buildSearch($params, $query)
|
||||
->paginate($pageSize)
|
||||
->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/** 创建字典项 */
|
||||
#[PostRoute(authorize: 'create')]
|
||||
public function create(SysDictItemFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysDictItemModel::create($validated);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 编辑字典项 */
|
||||
#[PutRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'update',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function update(int $id, SysDictItemFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysDictItemModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
$model->update($validated);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 删除字典项 */
|
||||
#[DeleteRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'delete',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function delete(int $id): JsonResponse
|
||||
{
|
||||
$model = SysDictItemModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
$model->delete();
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemTool\Http\Requests\SysFileMoveOrCopyRequest;
|
||||
use Modules\SystemTool\Models\SysFileModel;
|
||||
use Modules\SystemTool\Services\SysFileService;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
* 文件列表
|
||||
*/
|
||||
#[RequestAttribute('/system/file/list', 'system.file.list')]
|
||||
class SysFileController extends BaseController
|
||||
{
|
||||
protected array $searchField = [
|
||||
'group_id' => '=',
|
||||
'name' => 'like',
|
||||
'file_type' => '=',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected SysFileService $service
|
||||
) {}
|
||||
|
||||
/** 查询文件列表 */
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(Request $request): JsonResponse
|
||||
{
|
||||
$params = $request->all();
|
||||
$pageSize = $params['pageSize'] ?? 10;
|
||||
$query = SysFileModel::query();
|
||||
$data = $this->buildSearch($params, $query)
|
||||
->paginate($pageSize)
|
||||
->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/** 上传文件 */
|
||||
#[PostRoute('/upload', 'upload')]
|
||||
public function uploadImage(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'file' => 'required|file',
|
||||
'group_id' => [
|
||||
'nullable', 'integer',
|
||||
function ($attribute, $value, $fail) {
|
||||
if ($value == 0) {
|
||||
return;
|
||||
}
|
||||
if (!DB::table('sys_file_group')->where('id', $value)->exists()) {
|
||||
$fail('所选的分组 ID 不存在。');
|
||||
}
|
||||
},
|
||||
],
|
||||
]);
|
||||
$result = $this->service->upload(
|
||||
$data['file'],
|
||||
$data['group_id'] ?? 0,
|
||||
10,
|
||||
Auth::id()
|
||||
);
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
/** 获取回收站文件列表 */
|
||||
#[GetRoute('/trashed', 'trashed')]
|
||||
public function trashed(): JsonResponse
|
||||
{
|
||||
$list = $this->service->getTrashedList(request()->all());
|
||||
return $this->success($list);
|
||||
}
|
||||
|
||||
/** 删除文件(软删除) */
|
||||
#[DeleteRoute('/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])]
|
||||
public function delete(int $id): JsonResponse
|
||||
{
|
||||
$this->service->delete($id);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 批量删除文件 */
|
||||
#[DeleteRoute('/batch/delete', 'delete')]
|
||||
public function batchDelete(Request $request): JsonResponse
|
||||
{
|
||||
$ids = $request->input('ids', []);
|
||||
$count = $this->service->batchDelete($ids);
|
||||
return $this->success(['count' => $count]);
|
||||
}
|
||||
|
||||
/** 彻底删除文件 */
|
||||
#[DeleteRoute('/force-delete/{id}', 'force-delete', where: ['id' => '[0-9]+'])]
|
||||
public function forceDelete(int $id): JsonResponse
|
||||
{
|
||||
$this->service->forceDelete($id);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 批量彻底删除文件 */
|
||||
#[DeleteRoute('/batch/force-delete', 'force-delete')]
|
||||
public function batchForceDelete(Request $request): JsonResponse
|
||||
{
|
||||
$ids = $request->input('ids', []);
|
||||
$count = $this->service->batchForceDelete($ids);
|
||||
return $this->success(['count' => $count]);
|
||||
}
|
||||
|
||||
/** 恢复文件 */
|
||||
#[PostRoute('/restore/{id}', 'restore', where: ['id' => '[0-9]+'])]
|
||||
public function restore(int $id): JsonResponse
|
||||
{
|
||||
$this->service->restore($id);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 批量恢复文件 */
|
||||
#[PostRoute('/batch/restore', 'restore')]
|
||||
public function batchRestore(Request $request): JsonResponse
|
||||
{
|
||||
$ids = $request->input('ids', []);
|
||||
$count = $this->service->batchRestore($ids);
|
||||
return $this->success(['count' => $count]);
|
||||
}
|
||||
|
||||
/** 复制文件 */
|
||||
#[PostRoute('/copy', 'copy')]
|
||||
public function copy(SysFileMoveOrCopyRequest $request): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
if(! is_array($data['ids'])) {
|
||||
$result = $this->service->copy($data['ids'], $data['group_id']);
|
||||
} else {
|
||||
$result = $this->service->batchCopy($data['ids'], $data['group_id']);
|
||||
}
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
/** 移动文件 */
|
||||
#[PostRoute('/move', 'move')]
|
||||
public function move(SysFileMoveOrCopyRequest $request): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
if(! is_array($data['ids'])) {
|
||||
$result = $this->service->move($data['ids'], $data['group_id']);
|
||||
} else {
|
||||
$result = $this->service->batchMove($data['ids'], $data['group_id']);
|
||||
}
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
/** 重命名文件 */
|
||||
#[PutRoute('/rename/{id}', 'rename', where: ['id' => '[0-9]+'])]
|
||||
public function rename(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$newName = $request->input('name');
|
||||
$this->service->rename($id, $newName);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 下载文件 */
|
||||
#[GetRoute('/download/{id}', false, where: ['id' => '[0-9]+'])]
|
||||
public function download(int $id): StreamedResponse
|
||||
{
|
||||
return $this->service->download($id);
|
||||
}
|
||||
|
||||
/** 清空回收站文件 */
|
||||
#[DeleteRoute('/clean/trashed', 'clean-trashed')]
|
||||
public function cleanTrashed(Request $request): JsonResponse
|
||||
{
|
||||
$count = $this->service->cleanTrashed();
|
||||
return $this->success(['count' => $count]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Controllers;
|
||||
|
||||
use App\Exceptions\RepositoryException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemTool\Http\Requests\SysFileGroupFormRequest;
|
||||
use Modules\SystemTool\Models\SysFileGroupModel;
|
||||
|
||||
/**
|
||||
* 文件分组控制器
|
||||
*/
|
||||
#[RequestAttribute('/system/file/group', 'system.file.group')]
|
||||
class SysFileGroupController extends BaseController
|
||||
{
|
||||
public function __construct() {}
|
||||
|
||||
/** 获取文件分组列表 */
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function list(): JsonResponse
|
||||
{
|
||||
$query = SysFileGroupModel::query()->orderBy('sort', 'asc');
|
||||
$keywordSearch = request()->input('keywordSearch', '');
|
||||
if (isset($keywordSearch) && $keywordSearch != '') {
|
||||
$query->whereAny(
|
||||
['name'],
|
||||
'like',
|
||||
'%' . str_replace('%', '\%', $keywordSearch) . '%'
|
||||
);
|
||||
return $this->success($query->get()->toArray());
|
||||
}
|
||||
$group = $query->get()->toArray();
|
||||
return $this->success(getTreeData($group));
|
||||
}
|
||||
|
||||
/** 创建文件分组 */
|
||||
#[PostRoute(authorize: 'create')]
|
||||
public function create(SysFileGroupFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysFileGroupModel::create($validated);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 编辑文件分组 */
|
||||
#[PutRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'update',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function update(int $id, SysFileGroupFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysFileGroupModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
$model->update($validated);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 删除文件分组 */
|
||||
#[DeleteRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'delete',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function delete(int $id): JsonResponse
|
||||
{
|
||||
$model = SysFileGroupModel::find($id);
|
||||
if (empty($model)) {
|
||||
throw new RepositoryException('Model not found');
|
||||
}
|
||||
if ($model->countFiles > 0) {
|
||||
throw new RepositoryException('该文件夹下存在文件,无法删除');
|
||||
}
|
||||
$model->delete();
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemTool\Http\Requests\SysGridNavFormRequest;
|
||||
use Modules\SystemTool\Models\SysGridNavModel;
|
||||
|
||||
/**
|
||||
* 首页宫格导航管理
|
||||
*/
|
||||
#[RequestAttribute('/system/grid-nav', 'system.grid-nav')]
|
||||
class SysGridNavController extends BaseController
|
||||
{
|
||||
protected array $searchField = [
|
||||
'status' => '=',
|
||||
];
|
||||
|
||||
protected array $quickSearchField = ['title'];
|
||||
|
||||
/** 查询宫格导航列表 */
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(Request $request): JsonResponse
|
||||
{
|
||||
$query = SysGridNavModel::query();
|
||||
$data = $this->buildSearch($request->all(), $query)
|
||||
->orderBy('sort', 'desc')
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($request->input('pageSize', 10))
|
||||
->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/** 创建宫格导航 */
|
||||
#[PostRoute(authorize: 'create')]
|
||||
public function create(SysGridNavFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysGridNavModel::create($validated);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 编辑宫格导航 */
|
||||
#[PutRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'update',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function update(int $id, SysGridNavFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysGridNavModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error('宫格导航不存在');
|
||||
}
|
||||
$model->update($validated);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 删除宫格导航 */
|
||||
#[DeleteRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'delete',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function delete(int $id): JsonResponse
|
||||
{
|
||||
$model = SysGridNavModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error('宫格导航不存在');
|
||||
}
|
||||
$model->delete();
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
|
||||
#[RequestAttribute]
|
||||
class SysIndexController extends BaseController
|
||||
{
|
||||
|
||||
/** 获取首页信息 */
|
||||
#[GetRoute('/index', false)]
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$web_setting = site_config('web');
|
||||
return $this->success($web_setting);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemTool\Settings\MailSettings;
|
||||
|
||||
/**
|
||||
* 邮件配置管理
|
||||
*/
|
||||
#[RequestAttribute('/system/mail', 'system.mail')]
|
||||
class SysMailController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取邮件配置(从 DB 加载已保存的值,fallback 到 config 文件)
|
||||
*/
|
||||
#[GetRoute('/config', 'config')]
|
||||
public function getConfig(): JsonResponse
|
||||
{
|
||||
$mail = config('mail');
|
||||
$mode = $mail['default'] ?? 'single';
|
||||
$mailers = [];
|
||||
if($mode === 'failover') {
|
||||
$mailers = $mail['mailers']['failover']['mailers'] ?? [];
|
||||
} elseif ($mode === 'roundrobin') {
|
||||
$mailers = $mail['mailers']['roundrobin']['mailers'] ?? [];
|
||||
}
|
||||
$other = [
|
||||
'mode' => 'single',
|
||||
'mailers' => $mailers
|
||||
];
|
||||
if($mode === 'failover' || $mode == 'roundrobin') {
|
||||
$other['mode'] = $mode;
|
||||
$mail['default'] = $mailers[0] ?? 'smtp';
|
||||
}
|
||||
return $this->success([
|
||||
'other' => $other,
|
||||
'mail' => $mail,
|
||||
'services' => config('services')
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存邮件配置到数据库
|
||||
*
|
||||
* 使用 MailSettings::set() 将每个配置项写入 应用配置 表,
|
||||
* 写入后自动更新缓存,并通过全局中间件同步到 config() 运行时。
|
||||
*/
|
||||
#[PostRoute('/save', 'save')]
|
||||
public function saveConfig(): JsonResponse
|
||||
{
|
||||
$data = request()->all();
|
||||
|
||||
// 解析前端提交的数据结构
|
||||
$other = $data['other'] ?? [];
|
||||
$mail = $data['mail'] ?? [];
|
||||
$services = $data['services'] ?? [];
|
||||
|
||||
// 确定模式并构建 mailers 配置
|
||||
$mode = $other['mode'] ?? 'single';
|
||||
$selectedMailers = $other['mailers'] ?? [];
|
||||
|
||||
try {
|
||||
// 邮件默认驱动
|
||||
if ($mode === 'single') {
|
||||
MailSettings::set('mail.default', $mail['default'] ?? 'smtp');
|
||||
} elseif ($mode === 'failover') {
|
||||
MailSettings::set('mail.default', 'failover');
|
||||
MailSettings::set('mail.mailers.failover.mailers', $selectedMailers);
|
||||
} elseif ($mode === 'roundrobin') {
|
||||
MailSettings::set('mail.default', 'roundrobin');
|
||||
MailSettings::set('mail.mailers.roundrobin.mailers', $selectedMailers);
|
||||
}
|
||||
|
||||
// SMTP 配置
|
||||
if (isset($mail['mailers']['smtp'])) {
|
||||
$smtp = $mail['mailers']['smtp'];
|
||||
MailSettings::set('mail.mailers.smtp.host', $smtp['host'] ?? '127.0.0.1');
|
||||
MailSettings::set('mail.mailers.smtp.port', (int) ($smtp['port'] ?? 587));
|
||||
MailSettings::set('mail.mailers.smtp.username', $smtp['username'] ?? '');
|
||||
MailSettings::set('mail.mailers.smtp.password', $smtp['password'] ?? '');
|
||||
}
|
||||
|
||||
// 发件人配置
|
||||
if (isset($mail['from'])) {
|
||||
MailSettings::set('mail.from.address', $mail['from']['address'] ?? '');
|
||||
MailSettings::set('mail.from.name', $mail['from']['name'] ?? '');
|
||||
}
|
||||
|
||||
// 日志驱动配置
|
||||
if (isset($mail['mailers']['log'])) {
|
||||
MailSettings::set('mail.mailers.log.channel', $mail['mailers']['log']['channel'] ?? 'stack');
|
||||
}
|
||||
|
||||
// 第三方服务配置
|
||||
if (isset($services['postmark'])) {
|
||||
MailSettings::set('services.postmark.token', $services['postmark']['token'] ?? '');
|
||||
}
|
||||
if (isset($services['ses'])) {
|
||||
MailSettings::set('services.ses.key', $services['ses']['key'] ?? '');
|
||||
MailSettings::set('services.ses.secret', $services['ses']['secret'] ?? '');
|
||||
MailSettings::set('services.ses.region', $services['ses']['region'] ?? 'us-east-1');
|
||||
MailSettings::set('services.ses.token', $services['ses']['token'] ?? '');
|
||||
}
|
||||
if (isset($services['resend'])) {
|
||||
MailSettings::set('services.resend.key', $services['resend']['key'] ?? '');
|
||||
}
|
||||
if (isset($services['mailgun'])) {
|
||||
MailSettings::set('services.mailgun.domain', $services['mailgun']['domain'] ?? '');
|
||||
MailSettings::set('services.mailgun.secret', $services['mailgun']['secret'] ?? '');
|
||||
MailSettings::set('services.mailgun.endpoint', $services['mailgun']['endpoint'] ?? 'api.mailgun.net');
|
||||
}
|
||||
|
||||
// 清除 Laravel 配置缓存
|
||||
Artisan::call('config:clear');
|
||||
|
||||
return $this->success('保存成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error('保存失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送测试邮件
|
||||
*/
|
||||
#[PostRoute('/test', 'test')]
|
||||
public function sendTest(): JsonResponse
|
||||
{
|
||||
$to = request()->input('to');
|
||||
if (empty($to)) {
|
||||
return $this->error('请输入收件人邮箱');
|
||||
}
|
||||
try {
|
||||
Mail::raw('这是一封来自 Xin Admin 的测试邮件,用于验证邮件服务配置是否正确。', function ($message) use ($to) {
|
||||
$message->to($to)
|
||||
->subject('Xin Admin 邮件配置测试');
|
||||
});
|
||||
return $this->success('测试邮件发送成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error('发送失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Controllers;
|
||||
|
||||
use App\Exceptions\RepositoryException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemTool\Http\Requests\SysSiteConfigGroupFormRequest;
|
||||
use Modules\SystemTool\Models\SysSiteConfigGroupModel;
|
||||
|
||||
/**
|
||||
* 设置分组控制器
|
||||
*/
|
||||
#[RequestAttribute('/system/config/group', 'system.config.group')]
|
||||
class SysSiteConfigGroupController extends BaseController
|
||||
{
|
||||
/** 查询设置分组列表 */
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(Request $request): JsonResponse
|
||||
{
|
||||
$params = $request->all();
|
||||
$query = SysSiteConfigGroupModel::query();
|
||||
|
||||
if (!empty($params['keywordSearch'])) {
|
||||
$query->whereAny(
|
||||
['title', 'remark', 'key'],
|
||||
'like',
|
||||
'%' . str_replace('%', '\%', $params['keywordSearch']) . '%'
|
||||
);
|
||||
}
|
||||
|
||||
$data = $query->get()->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/** 创建设置分组 */
|
||||
#[PostRoute(authorize: 'create')]
|
||||
public function create(SysSiteConfigGroupFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysSiteConfigGroupModel::create($validated);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 编辑设置分组 */
|
||||
#[PutRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'update',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function update(int $id, SysSiteConfigGroupFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysSiteConfigGroupModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
$model->update($validated);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 删除设置分组 */
|
||||
#[DeleteRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'delete',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function delete(int $id): JsonResponse
|
||||
{
|
||||
$model = SysSiteConfigGroupModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
$count = $model->configs()->count();
|
||||
if ($count > 0) {
|
||||
throw new RepositoryException('当前分组有未删除的设置项!');
|
||||
}
|
||||
$model->delete();
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Controllers;
|
||||
|
||||
use App\Exceptions\RepositoryException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemTool\Http\Requests\SysSiteConfigItemsFormRequest;
|
||||
use Modules\SystemTool\Models\SysSiteConfigItemsModel;
|
||||
use Modules\SystemTool\Services\SysSiteConfigService;
|
||||
|
||||
/**
|
||||
* 系统设置
|
||||
*/
|
||||
#[RequestAttribute('/system/config/items', 'system.config.items')]
|
||||
class SysSiteConfigItemsController extends BaseController
|
||||
{
|
||||
protected array $searchField = [
|
||||
'group_id' => '=',
|
||||
];
|
||||
|
||||
/** 查询设置项列表 */
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(Request $request): JsonResponse
|
||||
{
|
||||
$params = $request->all();
|
||||
if (empty($params['group_id'])) {
|
||||
throw new RepositoryException('请选择设置分组');
|
||||
}
|
||||
$query = SysSiteConfigItemsModel::query();
|
||||
$data = $this->buildSearch($params, $query)
|
||||
->orderBy('sort', 'desc')
|
||||
->get()
|
||||
->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/** 创建设置项 */
|
||||
#[PostRoute(authorize: 'create')]
|
||||
public function create(SysSiteConfigItemsFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysSiteConfigItemsModel::create($validated);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 编辑设置项 */
|
||||
#[PutRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'update',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function update(int $id, SysSiteConfigItemsFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysSiteConfigItemsModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
$model->update($validated);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 删除设置项 */
|
||||
#[DeleteRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'delete',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function delete(int $id): JsonResponse
|
||||
{
|
||||
$model = SysSiteConfigItemsModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
$model->delete();
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 批量保存设置 */
|
||||
#[PutRoute('/save', 'save')]
|
||||
public function save(): JsonResponse
|
||||
{
|
||||
$configs = request()->input('configs');
|
||||
if (empty($configs) || !is_array($configs)) {
|
||||
return $this->error('请提供设置数据');
|
||||
}
|
||||
|
||||
$result = SysSiteConfigService::batchSaveSiteConfig($configs);
|
||||
|
||||
if ($result['success']) {
|
||||
SysSiteConfigService::refreshSiteConfig();
|
||||
return $this->success();
|
||||
}
|
||||
$message = '设置保存失败:' . $result['message'];
|
||||
return $this->error($message);
|
||||
}
|
||||
|
||||
/** 刷新设置 */
|
||||
#[PostRoute('/refreshCache', 'refresh')]
|
||||
public function refreshCache(): JsonResponse
|
||||
{
|
||||
SysSiteConfigService::refreshSiteConfig();
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemTool\Settings\StorageSettings;
|
||||
|
||||
/**
|
||||
* 文件存储配置管理
|
||||
*/
|
||||
#[RequestAttribute('/system/storage', 'system.storage')]
|
||||
class SysStorageController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取存储配置(从 DB 加载已保存的值,fallback 到 config 文件)
|
||||
*/
|
||||
#[GetRoute('/config', 'config')]
|
||||
public function getConfig(): JsonResponse
|
||||
{
|
||||
$filesystems = config('filesystems');
|
||||
$default = $filesystems['default'] ?? 'local';
|
||||
|
||||
// 获取各驱动配置
|
||||
$disks = $filesystems['disks'] ?? [];
|
||||
|
||||
// 本地存储配置
|
||||
$local = [
|
||||
'root' => $disks['local']['root'] ?? storage_path('app/public'),
|
||||
'url' => $disks['local']['url'] ?? config('app.url') . '/storage',
|
||||
'visibility' => $disks['local']['visibility'] ?? 'public',
|
||||
];
|
||||
|
||||
// S3 / OSS 配置
|
||||
$s3 = [
|
||||
'key' => $disks['s3']['key'] ?? '',
|
||||
'secret' => $disks['s3']['secret'] ?? '',
|
||||
'region' => $disks['s3']['region'] ?? '',
|
||||
'bucket' => $disks['s3']['bucket'] ?? '',
|
||||
'url' => $disks['s3']['url'] ?? '',
|
||||
'endpoint' => $disks['s3']['endpoint'] ?? '',
|
||||
'use_path_style_endpoint' => $disks['s3']['use_path_style_endpoint'] ?? false,
|
||||
];
|
||||
|
||||
// FTP 配置
|
||||
$ftp = [
|
||||
'host' => $disks['ftp']['host'] ?? '',
|
||||
'username' => $disks['ftp']['username'] ?? '',
|
||||
'password' => $disks['ftp']['password'] ?? '',
|
||||
'port' => $disks['ftp']['port'] ?? 21,
|
||||
'root' => $disks['ftp']['root'] ?? '',
|
||||
'passive' => $disks['ftp']['passive'] ?? true,
|
||||
'ssl' => $disks['ftp']['ssl'] ?? false,
|
||||
'timeout' => $disks['ftp']['timeout'] ?? 30,
|
||||
];
|
||||
|
||||
// SFTP 配置
|
||||
$sftp = [
|
||||
'host' => $disks['sftp']['host'] ?? '',
|
||||
'username' => $disks['sftp']['username'] ?? '',
|
||||
'password' => $disks['sftp']['password'] ?? '',
|
||||
'port' => $disks['sftp']['port'] ?? 22,
|
||||
'root' => $disks['sftp']['root'] ?? '',
|
||||
'timeout' => $disks['sftp']['timeout'] ?? 30,
|
||||
'private_key' => $disks['sftp']['privateKey'] ?? '',
|
||||
'passphrase' => $disks['sftp']['passphrase'] ?? '',
|
||||
];
|
||||
|
||||
return $this->success([
|
||||
'default' => $default,
|
||||
'local' => $local,
|
||||
's3' => $s3,
|
||||
'ftp' => $ftp,
|
||||
'sftp' => $sftp,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存存储配置到数据库
|
||||
*
|
||||
* 使用 StorageSettings::set() 将每个配置项写入 应用配置 表,
|
||||
* 写入后自动更新缓存,并通过全局中间件同步到 config() 运行时。
|
||||
*/
|
||||
#[PostRoute('/save', 'save')]
|
||||
public function saveConfig(): JsonResponse
|
||||
{
|
||||
$data = request()->all();
|
||||
|
||||
$default = $data['default'] ?? 'local';
|
||||
$local = $data['local'] ?? [];
|
||||
$s3 = $data['s3'] ?? [];
|
||||
$ftp = $data['ftp'] ?? [];
|
||||
$sftp = $data['sftp'] ?? [];
|
||||
|
||||
try {
|
||||
// 默认存储驱动
|
||||
StorageSettings::set('filesystems.default', $default);
|
||||
|
||||
// 本地存储
|
||||
if (!empty($local['url'])) {
|
||||
StorageSettings::set('filesystems.disks.local.url', $local['url']);
|
||||
}
|
||||
|
||||
// S3
|
||||
if ($default === 's3' || !empty($s3['key'])) {
|
||||
StorageSettings::set('filesystems.disks.s3.key', $s3['key'] ?? '');
|
||||
StorageSettings::set('filesystems.disks.s3.secret', $s3['secret'] ?? '');
|
||||
StorageSettings::set('filesystems.disks.s3.region', $s3['region'] ?? '');
|
||||
StorageSettings::set('filesystems.disks.s3.bucket', $s3['bucket'] ?? '');
|
||||
StorageSettings::set('filesystems.disks.s3.url', $s3['url'] ?? '');
|
||||
StorageSettings::set('filesystems.disks.s3.endpoint', $s3['endpoint'] ?? '');
|
||||
StorageSettings::set('filesystems.disks.s3.use_path_style_endpoint', (bool) ($s3['use_path_style_endpoint'] ?? false));
|
||||
}
|
||||
|
||||
// FTP
|
||||
if ($default === 'ftp' || !empty($ftp['host'])) {
|
||||
StorageSettings::set('filesystems.disks.ftp.host', $ftp['host'] ?? '');
|
||||
StorageSettings::set('filesystems.disks.ftp.username', $ftp['username'] ?? '');
|
||||
StorageSettings::set('filesystems.disks.ftp.password', $ftp['password'] ?? '');
|
||||
StorageSettings::set('filesystems.disks.ftp.port', (int) ($ftp['port'] ?? 21));
|
||||
StorageSettings::set('filesystems.disks.ftp.root', $ftp['root'] ?? '');
|
||||
StorageSettings::set('filesystems.disks.ftp.passive', (bool) ($ftp['passive'] ?? true));
|
||||
StorageSettings::set('filesystems.disks.ftp.ssl', (bool) ($ftp['ssl'] ?? false));
|
||||
StorageSettings::set('filesystems.disks.ftp.timeout', (int) ($ftp['timeout'] ?? 30));
|
||||
}
|
||||
|
||||
// SFTP
|
||||
if ($default === 'sftp' || !empty($sftp['host'])) {
|
||||
StorageSettings::set('filesystems.disks.sftp.host', $sftp['host'] ?? '');
|
||||
StorageSettings::set('filesystems.disks.sftp.username', $sftp['username'] ?? '');
|
||||
StorageSettings::set('filesystems.disks.sftp.password', $sftp['password'] ?? '');
|
||||
StorageSettings::set('filesystems.disks.sftp.port', (int) ($sftp['port'] ?? 22));
|
||||
StorageSettings::set('filesystems.disks.sftp.root', $sftp['root'] ?? '');
|
||||
StorageSettings::set('filesystems.disks.sftp.timeout', (int) ($sftp['timeout'] ?? 30));
|
||||
StorageSettings::set('filesystems.disks.sftp.privateKey', $sftp['private_key'] ?? '');
|
||||
StorageSettings::set('filesystems.disks.sftp.passphrase', $sftp['passphrase'] ?? '');
|
||||
}
|
||||
|
||||
// 清除 Laravel 配置缓存
|
||||
Artisan::call('config:clear');
|
||||
|
||||
return $this->success('保存成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error('保存失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试存储连接
|
||||
*/
|
||||
#[PostRoute('/test', 'test')]
|
||||
public function testConnection(): JsonResponse
|
||||
{
|
||||
$disk = request()->input('disk', 'local');
|
||||
|
||||
try {
|
||||
$storage = Storage::disk($disk);
|
||||
$testFile = 'storage_test_' . time() . '.txt';
|
||||
$testContent = 'XinAdmin 存储测试文件 - ' . date('Y-m-d H:i:s');
|
||||
|
||||
// 测试写入
|
||||
$storage->put($testFile, $testContent);
|
||||
|
||||
// 测试读取
|
||||
$readContent = $storage->get($testFile);
|
||||
if ($readContent !== $testContent) {
|
||||
return $this->error('读取测试失败:内容不匹配');
|
||||
}
|
||||
|
||||
// 测试删除
|
||||
$storage->delete($testFile);
|
||||
|
||||
return $this->success('存储连接测试成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error('连接测试失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\SystemTool\Base\SettingsDefinition;
|
||||
use Modules\SystemTool\Enum\ESettingType;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* 全局中间件 — 从缓存加载 DB 应用设置到 config() 运行时
|
||||
*
|
||||
* 每次请求自动执行,确保 config('mail.*') / config('filesystems.*') 等
|
||||
* 返回的是数据库中的值而非配置文件的默认值。
|
||||
*
|
||||
* 聚合缓存键名: SettingsDefinition::AGGREGATE_CACHE_KEY
|
||||
* 在 SettingsDefinition::set() 写入时会自动清除该缓存,
|
||||
* 使下一次请求重新从 DB 加载最新值。
|
||||
*/
|
||||
class LoadAppSettingsMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// 从聚合缓存加载(rememberForever = 永不过期,仅 set() 时清除)
|
||||
$settings = Cache::rememberForever(SettingsDefinition::AGGREGATE_CACHE_KEY, function () {
|
||||
return $this->loadAllFromDB();
|
||||
});
|
||||
|
||||
// 写入 Laravel config() 运行时
|
||||
foreach ($settings as $key => $value) {
|
||||
config([$key => $value]);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库加载所有应用设置,按类型转换后返回 key → value 数组
|
||||
*/
|
||||
protected function loadAllFromDB(): array
|
||||
{
|
||||
$rows = DB::table(SettingsDefinition::getTableName())->get();
|
||||
$result = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$result[$row->key] = match ((int) $row->type) {
|
||||
ESettingType::String->value => $row->s,
|
||||
ESettingType::Bool->value => is_null($row->n) ? null : (bool) $row->n,
|
||||
ESettingType::Number->value => is_null($row->n) ? null : (int) $row->n,
|
||||
ESettingType::Array->value => is_null($row->e) ? null : json_decode($row->e, true),
|
||||
ESettingType::Object->value => is_null($row->e) ? null : unserialize(base64_decode($row->e)),
|
||||
ESettingType::EncryptedString->value => is_null($row->e) ? null : Crypt::decrypt(base64_decode($row->e)),
|
||||
default => $row->s ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Requests;
|
||||
|
||||
use Illuminate\Validation\Rules\Exists;
|
||||
use Modules\Common\Http\Requests\BaseFormRequest;
|
||||
use Modules\SystemTool\Models\SysFileModel;
|
||||
|
||||
class SysCarouselFormRequest extends BaseFormRequest
|
||||
{
|
||||
protected $stopOnFirstFailure = true;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'required|string|max:100',
|
||||
'image_id' => ['required', 'integer', new Exists(SysFileModel::class, 'id')],
|
||||
'link' => 'nullable|string|max:500',
|
||||
'status' => 'nullable|integer|in:0,1',
|
||||
'sort' => 'nullable|integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'title.required' => '轮播图标题是必填的',
|
||||
'title.max' => '轮播图标题不能超过 :max 个字符',
|
||||
'image_id.required' => '轮播图图片是必填的',
|
||||
'link.max' => '跳转链接不能超过 :max 个字符',
|
||||
'status.in' => '状态值只能是 0(启用)或 1(禁用)',
|
||||
'sort.integer' => '排序必须是整数',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Requests;
|
||||
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Common\Http\Requests\BaseFormRequest;
|
||||
|
||||
class SysDictFormRequest extends BaseFormRequest
|
||||
{
|
||||
protected $stopOnFirstFailure = true;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
if (!$this->isUpdate()) {
|
||||
return [
|
||||
'name' => 'required|max:100',
|
||||
'code' => 'required|max:100|unique:sys_dict,code',
|
||||
'describe' => 'nullable|max:500',
|
||||
'status' => 'required|in:0,1',
|
||||
'sort' => 'nullable|integer|min:0',
|
||||
];
|
||||
} else {
|
||||
$id = $this->route('id');
|
||||
return [
|
||||
'name' => 'required|max:100',
|
||||
'code' => [
|
||||
'required',
|
||||
'max:100',
|
||||
Rule::unique('sys_dict', 'code')->ignore($id)
|
||||
],
|
||||
'describe' => 'nullable|max:500',
|
||||
'status' => 'required|in:0,1',
|
||||
'sort' => 'nullable|integer|min:0',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => '字典名称不能为空',
|
||||
'name.max' => '字典名称不能超过100个字符',
|
||||
'code.required' => '字典编码不能为空',
|
||||
'code.max' => '字典编码不能超过100个字符',
|
||||
'code.unique' => '字典编码已存在',
|
||||
'status.required' => '状态不能为空',
|
||||
'status.in' => '状态格式错误',
|
||||
'sort.integer' => '排序必须为整数',
|
||||
'sort.min' => '排序不能小于0',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Requests;
|
||||
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Common\Http\Requests\BaseFormRequest;
|
||||
|
||||
class SysDictItemFormRequest extends BaseFormRequest
|
||||
{
|
||||
protected $stopOnFirstFailure = true;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
if (!$this->isUpdate()) {
|
||||
$dict_id = $this->input('dict_id');
|
||||
return [
|
||||
'dict_id' => 'required|exists:sys_dict,id',
|
||||
'label' => 'required|max:100',
|
||||
'value' => [
|
||||
'required',
|
||||
'max:100',
|
||||
Rule::unique('sys_dict_item')->where(function ($query) use ($dict_id) {
|
||||
return $query->where('dict_id', $dict_id);
|
||||
})
|
||||
],
|
||||
'color' => 'nullable|string',
|
||||
'status' => 'required|in:0,1',
|
||||
'sort' => 'nullable|integer|min:0',
|
||||
];
|
||||
} else {
|
||||
$id = $this->route('id');
|
||||
$dict_id = $this->input('dict_id');
|
||||
return [
|
||||
'dict_id' => 'required|exists:sys_dict,id',
|
||||
'label' => 'required|max:100',
|
||||
'value' => [
|
||||
'required',
|
||||
'max:100',
|
||||
Rule::unique('sys_dict_item')->where(function ($query) use ($dict_id) {
|
||||
return $query->where('dict_id', $dict_id);
|
||||
})->ignore($id)
|
||||
],
|
||||
'color' => 'nullable|string',
|
||||
'status' => 'required|in:0,1',
|
||||
'sort' => 'nullable|integer|min:0',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'dict_id.required' => '字典ID不能为空',
|
||||
'dict_id.exists' => '字典不存在',
|
||||
'label.required' => '字典标签不能为空',
|
||||
'label.max' => '字典标签不能超过100个字符',
|
||||
'value.required' => '字典键值不能为空',
|
||||
'value.max' => '字典键值不能超过100个字符',
|
||||
'value.unique' => '该字典下已存在相同的键值',
|
||||
'color.in' => '颜色格式错误',
|
||||
'status.required' => '状态不能为空',
|
||||
'status.in' => '状态格式错误',
|
||||
'sort.integer' => '排序必须为整数',
|
||||
'sort.min' => '排序不能小于0',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Requests;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\Common\Http\Requests\BaseFormRequest;
|
||||
|
||||
class SysFileGroupFormRequest extends BaseFormRequest
|
||||
{
|
||||
protected $stopOnFirstFailure = true;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
if (!$this->isUpdate()) {
|
||||
return [
|
||||
'parent_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
function ($attribute, $value, $fail) {
|
||||
if ($value != 0 && !DB::table('sys_file_group')->where('id', $value)->exists()) {
|
||||
$fail('选择的上级分组不存在。');
|
||||
}
|
||||
},
|
||||
],
|
||||
'name' => 'required|string|max:255',
|
||||
'describe' => 'sometimes|string|max:500',
|
||||
'sort' => 'sometimes|integer|min:0',
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'describe' => 'sometimes|string|max:500',
|
||||
'sort' => 'sometimes|integer|min:0',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => '分组名称不能为空',
|
||||
'name.string' => '分组名称必须是字符串',
|
||||
'name.max' => '分组名称不能超过50个字符',
|
||||
'sort.integer' => '分组排序必须是整数',
|
||||
'sort.min' => '分组排序不能为负数',
|
||||
'describe.string' => '分组描述必须是字符串',
|
||||
'describe.max' => '分组描述不能超过500个字符',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SysFileMoveOrCopyRequest extends FormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'group_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
function ($attribute, $value, $fail) {
|
||||
if ($value != 0 && !DB::table('sys_file_group')->where('id', $value)->exists()) {
|
||||
$fail('选择的上级部门不存在。');
|
||||
}
|
||||
},
|
||||
],
|
||||
'ids' => [
|
||||
'required',
|
||||
function ($attribute, $value, $fail) {
|
||||
// 检查是否为数字
|
||||
if (is_numeric($value)) {
|
||||
return;
|
||||
}
|
||||
// 检查是否为数字数组
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $item) {
|
||||
if (!is_numeric($item)) {
|
||||
$fail("$attribute 中的元素必须全部是数字");
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
$fail("$attribute 必须是数字或数字数组");
|
||||
},
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Requests;
|
||||
|
||||
use Illuminate\Validation\Rules\Exists;
|
||||
use Modules\Common\Http\Requests\BaseFormRequest;
|
||||
use Modules\SystemTool\Models\SysFileModel;
|
||||
|
||||
class SysGridNavFormRequest extends BaseFormRequest
|
||||
{
|
||||
protected $stopOnFirstFailure = true;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'required|string|max:100',
|
||||
'image_id' => ['required', 'integer', new Exists(SysFileModel::class, 'id')],
|
||||
'link' => 'nullable|string|max:500',
|
||||
'status' => 'nullable|integer|in:0,1',
|
||||
'sort' => 'nullable|integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'title.required' => '导航标题是必填的',
|
||||
'title.max' => '导航标题不能超过 :max 个字符',
|
||||
'image_id.required' => '导航图标是必填的',
|
||||
'link.max' => '跳转链接不能超过 :max 个字符',
|
||||
'status.in' => '状态值只能是 0(启用)或 1(禁用)',
|
||||
'sort.integer' => '排序必须是整数',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Requests;
|
||||
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Common\Http\Requests\BaseFormRequest;
|
||||
use Modules\SystemTool\Models\SysSiteConfigGroupModel;
|
||||
|
||||
class SysSiteConfigGroupFormRequest extends BaseFormRequest
|
||||
{
|
||||
protected $stopOnFirstFailure = true;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
if (!$this->isUpdate()) {
|
||||
return [
|
||||
'key' => ['required', Rule::unique(SysSiteConfigGroupModel::class, 'key')],
|
||||
'title' => 'required',
|
||||
'remark' => 'sometimes|required',
|
||||
];
|
||||
} else {
|
||||
$id = $this->route('id');
|
||||
return [
|
||||
'key' => ['required', Rule::unique(SysSiteConfigGroupModel::class, 'key')->ignore($id)],
|
||||
'title' => 'required',
|
||||
'remark' => 'sometimes|required',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'key.required' => '键名字段是必填的',
|
||||
'key.unique' => '键名已存在',
|
||||
'title.required' => '标题字段是必填的',
|
||||
'remark.required' => '备注字段是必填的',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Http\Requests;
|
||||
|
||||
use Illuminate\Validation\Rules\Exists;
|
||||
use Modules\Common\Http\Requests\BaseFormRequest;
|
||||
use Modules\SystemTool\Models\SysSiteConfigGroupModel;
|
||||
use Modules\SystemTool\Models\SysSiteConfigItemsModel;
|
||||
use Modules\SystemTool\Rules\SysSiteConfigTypeRule;
|
||||
|
||||
class SysSiteConfigItemsFormRequest extends BaseFormRequest
|
||||
{
|
||||
protected $stopOnFirstFailure = true;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'title' => 'required|string',
|
||||
'key' => ['required', 'string', 'min:2', 'max:255'],
|
||||
'group_id' => ['required', 'integer', new Exists(SysSiteConfigGroupModel::class, 'id')],
|
||||
'type' => ['required', 'string', new SysSiteConfigTypeRule],
|
||||
'describe' => 'nullable|string',
|
||||
'options' => [
|
||||
'sometimes',
|
||||
'nullable',
|
||||
'string',
|
||||
'regex:/^(?:[^=\n]+=[^=\n]+)(?:\n[^=\n]+=[^=\n]+)*$/',
|
||||
],
|
||||
'props' => [
|
||||
'sometimes',
|
||||
'nullable',
|
||||
'string',
|
||||
'regex:/^(?:[^=\n]+=[^=\n]+)(?:\n[^=\n]+=[^=\n]+)*$/',
|
||||
],
|
||||
'sort' => 'nullable|integer',
|
||||
'values' => 'nullable|string',
|
||||
];
|
||||
|
||||
if (!$this->isUpdate()) {
|
||||
$rules['key'][] = function ($attribute, $value, $fail) {
|
||||
$groupId = $this->input('group_id');
|
||||
$exists = SysSiteConfigItemsModel::query()
|
||||
->where('group_id', $groupId)
|
||||
->where('key', $value)
|
||||
->exists();
|
||||
if ($exists) {
|
||||
$fail('该键名在此分组中已存在');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'title.required' => '标题字段是必填的',
|
||||
'title.string' => '标题字段必须是字符串',
|
||||
'key.required' => '键名字段是必填的',
|
||||
'key.string' => '键名字段必填是字符串',
|
||||
'key.min' => '键名至少需要 :min 个字符',
|
||||
'key.max' => '键名不能超过 :max 个字符',
|
||||
'group_id.required' => '分组ID是必填的',
|
||||
'group_id.exists' => '选择的分组不存在',
|
||||
'type.required' => '类型字段是必填的',
|
||||
'describe.string' => '描述必须是字符串',
|
||||
'options.regex' => '选项格式不正确,应为 key=value 格式,多个用换行分隔',
|
||||
'props.regex' => '属性格式不正确,应为 key=value 格式,多个用换行分隔',
|
||||
'sort.integer' => '排序必须是整数',
|
||||
'values.string' => '值必须是字符串',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
/**
|
||||
* 首页轮播图模型
|
||||
*/
|
||||
class SysCarouselModel extends Model
|
||||
{
|
||||
protected $table = 'sys_carousel';
|
||||
|
||||
protected $casts = [
|
||||
'status' => 'int',
|
||||
'sort' => 'int',
|
||||
'image_id' => 'array',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'image_id',
|
||||
'link',
|
||||
'status',
|
||||
'sort',
|
||||
];
|
||||
|
||||
protected $with = ['image'];
|
||||
|
||||
/**
|
||||
* 关联图片
|
||||
* @return HasOne
|
||||
*/
|
||||
public function image(): HasOne
|
||||
{
|
||||
return $this->hasOne(SysFileModel::class, 'id', 'image_id');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
namespace Modules\SystemTool\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 字典数据模型
|
||||
*/
|
||||
class SysDictItemModel extends Model
|
||||
{
|
||||
protected $table = 'sys_dict_item';
|
||||
|
||||
protected $fillable = [
|
||||
'dict_id',
|
||||
'label',
|
||||
'value',
|
||||
'color',
|
||||
'status',
|
||||
'sort',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'dict_id' => 'integer',
|
||||
'status' => 'integer',
|
||||
'sort' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime'
|
||||
];
|
||||
|
||||
/**
|
||||
* 字典项关联字典表
|
||||
*/
|
||||
public function dict(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SysDictModel::class, 'dict_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
namespace Modules\SystemTool\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* 字典类型模型
|
||||
*/
|
||||
class SysDictModel extends Model
|
||||
{
|
||||
protected $table = 'sys_dict';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'code',
|
||||
'describe',
|
||||
'status',
|
||||
'sort',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'status' => 'integer',
|
||||
'sort' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime'
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联字典子项
|
||||
*/
|
||||
public function dictItems(): HasMany
|
||||
{
|
||||
return $this->hasMany(SysDictItemModel::class, 'dict_id', 'id')
|
||||
->orderBy('sort')
|
||||
->orderBy('id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有字典及其子项
|
||||
* @return array
|
||||
*/
|
||||
public static function getAllDictWithItems(): array
|
||||
{
|
||||
return static::with('dictItems')
|
||||
->where('status', 0)
|
||||
->orderBy('sort')
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->map(function ($dict) {
|
||||
return [
|
||||
'id' => $dict->id,
|
||||
'name' => $dict->name,
|
||||
'code' => $dict->code,
|
||||
'describe' => $dict->describe,
|
||||
'status' => $dict->status,
|
||||
'sort' => $dict->sort,
|
||||
'dict_items' => $dict->dictItems->filter(fn($item) => $item->status === 0)->map(function ($item) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'label' => $item->label,
|
||||
'value' => $item->value,
|
||||
'color' => $item->color,
|
||||
'sort' => $item->sort,
|
||||
];
|
||||
})->values()->toArray(),
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
namespace Modules\SystemTool\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* Class FileGroup
|
||||
*/
|
||||
class SysFileGroupModel extends Model
|
||||
{
|
||||
protected $table = 'sys_file_group';
|
||||
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $casts = [
|
||||
'sort' => 'int',
|
||||
'parent_id' => 'int'
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'parent_id',
|
||||
'sort',
|
||||
'describe',
|
||||
];
|
||||
|
||||
protected $appends = ['countFiles'];
|
||||
|
||||
/**
|
||||
* 获取父级分组
|
||||
*/
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子级分组
|
||||
*/
|
||||
public function children(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分组下的文件
|
||||
*/
|
||||
public function files(): HasMany
|
||||
{
|
||||
return $this->hasMany(SysFileModel::class, 'group_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分组下的文件数量
|
||||
*/
|
||||
public function getCountFilesAttribute(): int
|
||||
{
|
||||
return $this->files()->count();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
namespace Modules\SystemTool\Models;
|
||||
|
||||
use App\Models\UserModel;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\SystemTool\Enum\FileType;
|
||||
use Modules\SystemUser\Models\SysUserModel;
|
||||
|
||||
/**
|
||||
* Class File
|
||||
*/
|
||||
class SysFileModel extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
protected $table = 'sys_file';
|
||||
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $casts = [
|
||||
'group_id' => 'int',
|
||||
'channel' => 'int',
|
||||
'file_type' => 'int',
|
||||
'file_size' => 'int',
|
||||
'uploader_id' => 'int',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'group_id',
|
||||
'disk',
|
||||
'channel',
|
||||
'file_type',
|
||||
'file_name',
|
||||
'file_path',
|
||||
'file_size',
|
||||
'file_ext',
|
||||
'uploader_id',
|
||||
];
|
||||
|
||||
protected $appends = ['preview_url', 'file_url'];
|
||||
|
||||
/**
|
||||
* 获取文件所属分组
|
||||
*/
|
||||
public function group(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SysFileGroupModel::class, 'group_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上传者
|
||||
* 根据channel字段判断是系统用户还是App用户
|
||||
*/
|
||||
public function uploader(): BelongsTo
|
||||
{
|
||||
// channel 10:系统用户 20:App用户端
|
||||
if ($this->channel == 10) {
|
||||
return $this->belongsTo(SysUserModel::class, 'uploader_id', 'id');
|
||||
} else {
|
||||
return $this->belongsTo(UserModel::class, 'uploader_id', 'id');
|
||||
}
|
||||
}
|
||||
|
||||
protected function previewUrl(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: function ($value, array $data) {
|
||||
try {
|
||||
// 图片类型:直接返回图片URL作为预览
|
||||
if ($data['file_type'] === FileType::IMAGE->value) {
|
||||
if($data['disk'] === 'local') {
|
||||
return Storage::disk($data['disk'])->url($data['file_path']);
|
||||
}
|
||||
return Storage::disk($data['disk'])->temporaryUrl(
|
||||
$data['file_path'], now()->plus(minutes: 5)
|
||||
);
|
||||
}
|
||||
$fileType = FileType::tryFrom($data['file_type']);
|
||||
// 其他类型:返回默认类型图标
|
||||
$previewPath = $fileType?->previewPath() ?? FileType::ANNEX->previewPath();
|
||||
return config('app.url') . '/' . $previewPath;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// 发生异常时返回默认图标
|
||||
return config('app.url') . '/' . FileType::ANNEX->previewPath();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件访问URL
|
||||
*/
|
||||
protected function fileUrl(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: function ($value, array $data) {
|
||||
try {
|
||||
return Storage::disk($data['disk'])->url($data['file_path']);
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
/**
|
||||
* 首页宫格导航模型
|
||||
*/
|
||||
class SysGridNavModel extends Model
|
||||
{
|
||||
protected $table = 'sys_grid_nav';
|
||||
|
||||
protected $casts = [
|
||||
'status' => 'int',
|
||||
'sort' => 'int',
|
||||
'image_id' => 'int',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'image_id',
|
||||
'link',
|
||||
'status',
|
||||
'sort',
|
||||
];
|
||||
|
||||
protected $with = ['image'];
|
||||
|
||||
/**
|
||||
* 关联图片
|
||||
* @return HasOne
|
||||
*/
|
||||
public function image(): HasOne
|
||||
{
|
||||
return $this->hasOne(SysFileModel::class, 'id', 'image_id');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
namespace Modules\SystemTool\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* Class SiteConfig Group
|
||||
*/
|
||||
class SysSiteConfigGroupModel extends Model
|
||||
{
|
||||
protected $table = 'sys_site_config_group';
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'key',
|
||||
'remark',
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联设置项目
|
||||
* @return HasMany
|
||||
*/
|
||||
public function configs(): HasMany
|
||||
{
|
||||
return $this->hasMany(SysSiteConfigItemsModel::class ,'group_id', 'id');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
namespace Modules\SystemTool\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Modules\SystemTool\Enum\SiteConfigType;
|
||||
|
||||
/**
|
||||
* Class Setting
|
||||
*/
|
||||
class SysSiteConfigItemsModel extends Model
|
||||
{
|
||||
protected $table = 'sys_site_config_items';
|
||||
|
||||
protected $casts = [
|
||||
'group_id' => 'int',
|
||||
'sort' => 'int',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'key',
|
||||
'title',
|
||||
'describe',
|
||||
'values',
|
||||
'type',
|
||||
'options',
|
||||
'props',
|
||||
'group_id',
|
||||
'sort',
|
||||
];
|
||||
|
||||
protected $appends = ['options_json', 'props_json'];
|
||||
|
||||
/**
|
||||
* 关联设置
|
||||
* @return BelongsTo
|
||||
*/
|
||||
public function group(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SysSiteConfigGroupModel::class, 'id', 'group_id');
|
||||
}
|
||||
|
||||
|
||||
protected function values(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function (mixed $value, array $attributes) {
|
||||
$type_enum = SiteConfigType::tryFrom($attributes['type']);
|
||||
if($type_enum) {
|
||||
return $type_enum->castValue($value);
|
||||
} else {
|
||||
return $value;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public function getOptionsJsonAttribute(): string
|
||||
{
|
||||
if(empty($this->options)) {
|
||||
return "{}";
|
||||
}
|
||||
$data = [];
|
||||
$value = explode("\n", $this->options);
|
||||
foreach ($value as $item) {
|
||||
$item = explode('=',$item);
|
||||
if(count($item) < 2) {
|
||||
continue;
|
||||
}
|
||||
$data[] = [
|
||||
'label' => $item[1],
|
||||
'value' => $item[0]
|
||||
];
|
||||
}
|
||||
return json_encode($data);
|
||||
}
|
||||
|
||||
public function getPropsJsonAttribute(): string
|
||||
{
|
||||
if(empty($this->props)) {
|
||||
return "{}";
|
||||
}
|
||||
$data = [];
|
||||
$value = explode("\n",$this->props);
|
||||
foreach ($value as $item) {
|
||||
$item = explode('=',$item);
|
||||
if(count($item) < 2) {
|
||||
continue;
|
||||
}
|
||||
if($item[1] === 'false') {
|
||||
$data[$item[0]] = false;
|
||||
}elseif ($item[1] === 'true') {
|
||||
$data[$item[0]] = true;
|
||||
}else {
|
||||
$data[$item[0]] = $item[1];
|
||||
}
|
||||
}
|
||||
return json_encode($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Providers;
|
||||
|
||||
use Laravel\Boost\Boost;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\AnnoRoute\AnnoRoute;
|
||||
use Modules\SystemTool\Ai\Boots\Reasonix;
|
||||
use Modules\SystemTool\Services\SysSiteConfigService;
|
||||
|
||||
class SystemToolServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(SysSiteConfigService::class, SysSiteConfigService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(AnnoRoute $annoRoute): void
|
||||
{
|
||||
Boost::registerAgent('reasonix', Reasonix::class);
|
||||
|
||||
// 注册路由
|
||||
$annoRoute->register(base_path('modules/SystemTool/Http/Controllers'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Rules;
|
||||
|
||||
use Closure;
|
||||
use Modules\SystemTool\Enum\SiteConfigType;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Translation\PotentiallyTranslatedString;
|
||||
|
||||
class SysSiteConfigTypeRule implements ValidationRule
|
||||
{
|
||||
/**
|
||||
* Run the validation rule.
|
||||
*
|
||||
* @param Closure(string, ?string=): PotentiallyTranslatedString $fail
|
||||
*/
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
if (SiteConfigType::tryFrom($value) == null) {
|
||||
$fail('设置类型不存在!');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Services;
|
||||
|
||||
use App\Exceptions\HttpResponseException;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\SystemTool\Enum\FileType;
|
||||
use Modules\SystemTool\Models\SysFileModel;
|
||||
use Modules\SystemTool\Settings\StorageSettings;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
* 文件服务类
|
||||
*/
|
||||
class SysFileService
|
||||
{
|
||||
|
||||
/**
|
||||
* 获取回收站列表
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
public function getTrashedList(array $params = []): array
|
||||
{
|
||||
$pageSize = $params['pageSize'] ?? 10;
|
||||
return SysFileModel::onlyTrashed()
|
||||
->paginate($pageSize)
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储磁盘实例
|
||||
*/
|
||||
protected function disk($disk = null): FilesystemAdapter
|
||||
{
|
||||
if(!$disk) {
|
||||
$disk = config('filesystems.default');
|
||||
}
|
||||
if ($disk === 's3' && !self::isS3Configured()) {
|
||||
throw new HttpResponseException(['success' => false, 'msg' => __('system.storage.s3_not_configured')]);
|
||||
}
|
||||
/** @var FilesystemAdapter */
|
||||
return Storage::disk($disk);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成存储路径
|
||||
*/
|
||||
protected function generateStoragePath(string $extension): string
|
||||
{
|
||||
return date('Ymd') . '/' . uniqid() . '.' . $extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param UploadedFile $file 文件
|
||||
* @param int $groupId 分组 ID
|
||||
* @param int $channel 上传来源 0:匿名用户,10:后台用户,20:APP用户
|
||||
* @param int|null $user_id 上传用户 ID
|
||||
* @return array
|
||||
*/
|
||||
public function upload(UploadedFile $file, int $groupId = 0, int $channel = 0, ?int $user_id = null): array
|
||||
{
|
||||
// 文件扩展名
|
||||
$fileExt = strtolower($file->getClientOriginalExtension() ?: $file->extension());
|
||||
|
||||
if (empty($fileExt)) {
|
||||
throw new HttpResponseException(['success' => false, 'msg' => '无法识别的文件扩展名']);
|
||||
}
|
||||
// 推断文件类型
|
||||
$fileType = FileType::guessFromExtension($fileExt);
|
||||
// 获取储存路径
|
||||
$storagePath = $this->generateStoragePath($fileExt);
|
||||
// 获取磁盘
|
||||
$disk = StorageSettings::get('filesystems.default', 'local');
|
||||
// 存储文件并设置可见性
|
||||
$stored = $this->disk($disk)->put($storagePath, $file->getContent(), 'public');
|
||||
if (!$stored) {
|
||||
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.upload_failed')]);
|
||||
}
|
||||
// 保存到数据库
|
||||
$model = new SysFileModel();
|
||||
$model->disk = $disk;
|
||||
$model->group_id = $groupId;
|
||||
$model->channel = $channel;
|
||||
$model->file_type = $fileType->value;
|
||||
$model->file_path = $storagePath;
|
||||
$model->file_name = $file->getClientOriginalName();
|
||||
$model->file_size = $file->getSize();
|
||||
$model->file_ext = $fileExt;
|
||||
$model->uploader_id = $user_id;
|
||||
$model->save();
|
||||
return $model->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除文件(移入回收站)
|
||||
*/
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
$file = SysFileModel::find($id);
|
||||
if (!$file) {
|
||||
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.not_found')]);
|
||||
}
|
||||
|
||||
return (bool) $file->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量软删除文件
|
||||
*/
|
||||
public function batchDelete(array $fileIds): int
|
||||
{
|
||||
return SysFileModel::whereIn('id', $fileIds)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复已删除的文件
|
||||
*/
|
||||
public function restore(int $fileId): bool
|
||||
{
|
||||
$file = SysFileModel::withTrashed()->find($fileId);
|
||||
if (!$file) {
|
||||
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.not_found')]);
|
||||
}
|
||||
|
||||
return $file->restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量恢复文件
|
||||
*/
|
||||
public function batchRestore(array $fileIds): int
|
||||
{
|
||||
return SysFileModel::withTrashed()->whereIn('id', $fileIds)->restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* 彻底删除文件(含物理删除)
|
||||
*/
|
||||
public function forceDelete(int $fileId): bool
|
||||
{
|
||||
$file = SysFileModel::withTrashed()->find($fileId);
|
||||
if (!$file) {
|
||||
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.not_found')]);
|
||||
}
|
||||
|
||||
// 删除物理文件
|
||||
$this->disk($file->disk)->delete($file->file_path);
|
||||
|
||||
return (bool) $file->forceDelete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量彻底删除文件
|
||||
*/
|
||||
public function batchForceDelete(array $fileIds): int
|
||||
{
|
||||
$files = SysFileModel::withTrashed()->whereIn('id', $fileIds)->get();
|
||||
$count = 0;
|
||||
|
||||
foreach ($files as $file) {
|
||||
$this->disk($file->disk)->delete($file->file_path);
|
||||
$file->forceDelete();
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
public function download(int $fileId, ?string $filename = null): StreamedResponse
|
||||
{
|
||||
$file = SysFileModel::find($fileId);
|
||||
if (!$file) {
|
||||
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.not_found')]);
|
||||
}
|
||||
|
||||
$downloadName = $filename ?? $file->file_name;
|
||||
return $this->disk($file->disk)->download($file->file_path, $downloadName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件流式响应(用于在线预览等场景)
|
||||
*/
|
||||
public function stream(int $fileId): StreamedResponse
|
||||
{
|
||||
$file = SysFileModel::find($fileId);
|
||||
if (!$file) {
|
||||
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.not_found')]);
|
||||
}
|
||||
|
||||
$mimeType = $this->getMimeType($file->file_ext);
|
||||
|
||||
return response()->stream(
|
||||
function () use ($file) {
|
||||
$stream = $this->disk($file->disk)->readStream($file->file_path);
|
||||
fpassthru($stream);
|
||||
if (is_resource($stream)) {
|
||||
fclose($stream);
|
||||
}
|
||||
},
|
||||
200,
|
||||
[
|
||||
'Content-Type' => $mimeType,
|
||||
'Content-Disposition' => 'inline; filename="' . $file->file_name . '"',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件内容
|
||||
*/
|
||||
public function getContent(int $fileId): string
|
||||
{
|
||||
$file = SysFileModel::find($fileId);
|
||||
if (!$file) {
|
||||
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.not_found')]);
|
||||
}
|
||||
|
||||
return $this->disk($file->disk)->get($file->file_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件访问URL
|
||||
*/
|
||||
public function getUrl(int $fileId): ?string
|
||||
{
|
||||
$file = SysFileModel::find($fileId);
|
||||
if (!$file) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->disk($file->disk)->url($file->file_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径获取访问URL
|
||||
*/
|
||||
public function getUrlByPath(string $path, ?string $disk = null): string
|
||||
{
|
||||
return $this->disk($disk)->url($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件元数据信息
|
||||
*/
|
||||
public function getMetadata(int $fileId): ?array
|
||||
{
|
||||
$file = SysFileModel::find($fileId);
|
||||
if (!$file) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$disk = $this->disk($file->disk);
|
||||
|
||||
return [
|
||||
'id' => $file->id,
|
||||
'name' => $file->file_name,
|
||||
'path' => $file->file_path,
|
||||
'disk' => $file->disk,
|
||||
'size' => $file->file_size,
|
||||
'extension' => $file->file_ext,
|
||||
'mime_type' => $this->getMimeType($file->file_ext),
|
||||
'last_modified' => $disk->exists($file->file_path)
|
||||
? date('Y-m-d H:i:s', $disk->lastModified($file->file_path))
|
||||
: null,
|
||||
'url' => $this->getUrl($file->id),
|
||||
'group_id' => $file->group_id,
|
||||
'uploader_id' => $file->uploader_id,
|
||||
'created_at' => $file->created_at?->format('Y-m-d H:i:s'),
|
||||
'updated_at' => $file->updated_at?->format('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制文件
|
||||
*/
|
||||
public function copy(int $fileId, int $targetGroupId = 0): array
|
||||
{
|
||||
$file = SysFileModel::find($fileId);
|
||||
if (!$file) {
|
||||
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.not_found')]);
|
||||
}
|
||||
|
||||
$newPath = $this->generateStoragePath($file->file_ext);
|
||||
|
||||
$this->disk($file->disk)->copy($file->file_path, $newPath);
|
||||
|
||||
// 创建新的文件记录
|
||||
$newFile = SysFileModel::create([
|
||||
'disk' => $file->disk,
|
||||
'group_id' => $targetGroupId,
|
||||
'channel' => $file->channel,
|
||||
'file_name' => $file->file_name,
|
||||
'file_type' => $file->file_type,
|
||||
'file_path' => $newPath,
|
||||
'file_size' => $file->file_size,
|
||||
'file_ext' => $file->file_ext,
|
||||
'uploader_id' => Auth::id(),
|
||||
]);
|
||||
|
||||
return $newFile->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量复制文件
|
||||
*/
|
||||
public function batchCopy(array $fileIds, int $targetGroupId = 0): bool
|
||||
{
|
||||
$files = SysFileModel::whereIn('id', $fileIds)->get();
|
||||
$fileArray = [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
|
||||
$newPath = $this->generateStoragePath($file->file_ext);
|
||||
|
||||
$this->disk($file->disk)->copy($file->file_path, $newPath);
|
||||
|
||||
$fileArray[] =[
|
||||
'disk' => $file->disk,
|
||||
'group_id' => $targetGroupId,
|
||||
'channel' => $file->channel,
|
||||
'file_name' => $file->file_name,
|
||||
'file_type' => $file->file_type,
|
||||
'file_path' => $newPath,
|
||||
'file_size' => $file->file_size,
|
||||
'file_ext' => $file->file_ext,
|
||||
'uploader_id' => Auth::id(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
return SysFileModel::insert($fileArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动文件
|
||||
*/
|
||||
public function move(int $fileId, int $groupId): bool
|
||||
{
|
||||
$file = SysFileModel::find($fileId);
|
||||
return $file->update(['group_id' => $groupId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量移动
|
||||
*/
|
||||
public function batchMove(array $fileIds, int $groupId): int
|
||||
{
|
||||
return SysFileModel::whereIn('id', $fileIds)->update(['group_id' => $groupId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名文件
|
||||
*/
|
||||
public function rename(int $fileId, string $newName): bool
|
||||
{
|
||||
$file = SysFileModel::find($fileId);
|
||||
if (!$file) {
|
||||
throw new HttpResponseException(['success' => false, 'msg' => __('system.file.not_found')]);
|
||||
}
|
||||
|
||||
return $file->update(['file_name' => $newName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
*/
|
||||
public function exists(int $fileId): bool
|
||||
{
|
||||
$file = SysFileModel::find($fileId);
|
||||
if (!$file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->disk($file->disk)->exists($file->file_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件大小(字节)
|
||||
*/
|
||||
public function getSize(int $fileId): ?int
|
||||
{
|
||||
$file = SysFileModel::find($fileId);
|
||||
if (!$file) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->disk($file->disk)->size($file->file_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取MIME类型
|
||||
*/
|
||||
protected function getMimeType(string $extension): string
|
||||
{
|
||||
$mimeTypes = [
|
||||
// 图片
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
'avif' => 'image/avif',
|
||||
'bmp' => 'image/bmp',
|
||||
'svg' => 'image/svg+xml',
|
||||
// 音频
|
||||
'mp3' => 'audio/mpeg',
|
||||
'wav' => 'audio/wav',
|
||||
'ogg' => 'audio/ogg',
|
||||
'flac' => 'audio/flac',
|
||||
'aac' => 'audio/aac',
|
||||
// 视频
|
||||
'mp4' => 'video/mp4',
|
||||
'webm' => 'video/webm',
|
||||
'mkv' => 'video/x-matroska',
|
||||
'mov' => 'video/quicktime',
|
||||
'avi' => 'video/x-msvideo',
|
||||
// 文档
|
||||
'pdf' => 'application/pdf',
|
||||
'doc' => 'application/msword',
|
||||
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'xls' => 'application/vnd.ms-excel',
|
||||
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
// 压缩包
|
||||
'zip' => 'application/zip',
|
||||
'rar' => 'application/vnd.rar',
|
||||
'7z' => 'application/x-7z-compressed',
|
||||
// 其他
|
||||
'json' => 'application/json',
|
||||
'xml' => 'application/xml',
|
||||
'txt' => 'text/plain',
|
||||
];
|
||||
|
||||
return $mimeTypes[strtolower($extension)] ?? 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空回收站文件
|
||||
*/
|
||||
public function cleanTrashed(): int
|
||||
{
|
||||
$expiredFiles = SysFileModel::onlyTrashed()->get();
|
||||
$count = 0;
|
||||
foreach ($expiredFiles as $file) {
|
||||
$this->disk($file->disk)->delete($file->file_path);
|
||||
$file->forceDelete();
|
||||
$count++;
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查 S3 配置是否有效
|
||||
* @return bool
|
||||
*/
|
||||
public static function isS3Configured(): bool
|
||||
{
|
||||
$storageConfig = config('filesystems.disks.s3');
|
||||
|
||||
if (empty($storageConfig)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !empty($storageConfig['key'])
|
||||
&& !empty($storageConfig['secret'])
|
||||
&& !empty($storageConfig['bucket'])
|
||||
&& !empty($storageConfig['region']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\SystemTool\Enum\SiteConfigType;
|
||||
use Modules\SystemTool\Models\SysSiteConfigGroupModel;
|
||||
use Modules\SystemTool\Models\SysSiteConfigItemsModel;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 系统设置服务
|
||||
*/
|
||||
class SysSiteConfigService
|
||||
{
|
||||
/**
|
||||
* 缓存过期时间(秒),默认30天
|
||||
*/
|
||||
private const int|float CACHE_TTL = 60 * 60 * 24 * 30;
|
||||
|
||||
/**
|
||||
* 缓存键
|
||||
*/
|
||||
private const string CACHE_KEY = 'site_config';
|
||||
|
||||
/**
|
||||
* 刷新系统设置缓存
|
||||
* @return bool
|
||||
*/
|
||||
public static function refreshSiteConfig(): bool
|
||||
{
|
||||
try {
|
||||
$configs = SysSiteConfigGroupModel::with('configs')
|
||||
->get()
|
||||
->mapWithKeys(fn($group) => [
|
||||
$group->key => $group->configs
|
||||
->mapWithKeys(fn($config) => [ $config->key => $config->values ])
|
||||
->toArray()
|
||||
])
|
||||
->toArray();
|
||||
|
||||
Cache::put(self::CACHE_KEY, $configs, self::CACHE_TTL);
|
||||
Log::info('系统配置缓存已刷新', ['settings_count' => $configs]);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
Log::error('刷新系统配置缓存失败', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设置
|
||||
* 格式:'group.key' 或 'group',为null时返回所有配置
|
||||
*/
|
||||
public static function getSiteConfig(?string $name = null, mixed $default = null): mixed
|
||||
{
|
||||
$configs = Cache::get(self::CACHE_KEY);
|
||||
|
||||
// 缓存不存在时重新加载
|
||||
if (empty($configs)) {
|
||||
self::refreshSiteConfig();
|
||||
$configs = Cache::get(self::CACHE_KEY, []);
|
||||
}
|
||||
|
||||
// 返回所有配置
|
||||
if (is_null($name)) {
|
||||
return $configs;
|
||||
}
|
||||
|
||||
// 解析配置路径
|
||||
$keys = explode('.', $name);
|
||||
|
||||
// 支持多级获取:group.key 或 group
|
||||
if (count($keys) === 2) {
|
||||
return $configs[$keys[0]][$keys[1]] ?? $default;
|
||||
} elseif (count($keys) === 1) {
|
||||
return $configs[$keys[0]] ?? $default;
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量保存设置项
|
||||
* [['id' => int, 'values' => mixed], ...]
|
||||
*/
|
||||
public static function batchSaveSiteConfig(array $configs): array
|
||||
{
|
||||
$errors = [];
|
||||
$ids = array_column($configs, 'id');
|
||||
$items = SysSiteConfigItemsModel::whereIn('id', $ids)->get()->keyBy('id');
|
||||
|
||||
// 先验证所有 ID 是否存在,收集缺失项信息
|
||||
foreach ($configs as $item) {
|
||||
$id = (int)$item['id'];
|
||||
if (!$items->has($id)) {
|
||||
$errors[] = [
|
||||
'key' => "unknown_{$id}",
|
||||
'title' => "未知设置(ID: {$id})",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
return ['success' => false, 'errors' => $errors];
|
||||
}
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($configs, $items) {
|
||||
foreach ($configs as $item) {
|
||||
$id = (int)$item['id'];
|
||||
$model = $items->get($id);
|
||||
$value = $item['value'];
|
||||
$model->values = is_array($value) || is_object($value) ? json_encode($value) : (string)$value;
|
||||
$model->save();
|
||||
}
|
||||
});
|
||||
return ['success' => true, 'errors' => []];
|
||||
} catch (Throwable $e) {
|
||||
return ['success' => false, 'errors' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Settings;
|
||||
|
||||
use Modules\SystemTool\Attributes\Setting;
|
||||
use Modules\SystemTool\Base\SettingsDefinition;
|
||||
use Modules\SystemTool\Enum\ESettingType;
|
||||
|
||||
/**
|
||||
* AI 服务配置定义
|
||||
*
|
||||
* 对应 config/ai.php 中的可配置项。
|
||||
* 敏感值(API Key、Token)使用 EncryptedString 类型存储。
|
||||
*/
|
||||
#[Setting(config: 'ai.default', type: ESettingType::String, description: '默认 AI 供应商')]
|
||||
#[Setting(config: 'ai.default_for_images', type: ESettingType::String, description: '默认图片生成供应商')]
|
||||
#[Setting(config: 'ai.default_for_audio', type: ESettingType::String, description: '默认音频生成供应商')]
|
||||
#[Setting(config: 'ai.default_for_transcription', type: ESettingType::String, description: '默认语音转文字供应商')]
|
||||
|
||||
// Anthropic
|
||||
#[Setting(config: 'ai.providers.anthropic.key', type: ESettingType::EncryptedString, description: 'Anthropic API Key')]
|
||||
#[Setting(config: 'ai.providers.anthropic.url', type: ESettingType::String, description: 'Anthropic API URL')]
|
||||
|
||||
// Azure
|
||||
#[Setting(config: 'ai.providers.azure.key', type: ESettingType::EncryptedString, description: 'Azure OpenAI API Key')]
|
||||
#[Setting(config: 'ai.providers.azure.url', type: ESettingType::String, description: 'Azure OpenAI URL')]
|
||||
#[Setting(config: 'ai.providers.azure.api_version', type: ESettingType::String, description: 'Azure API 版本')]
|
||||
#[Setting(config: 'ai.providers.azure.deployment', type: ESettingType::String, description: 'Azure 模型部署名')]
|
||||
#[Setting(config: 'ai.providers.azure.embedding_deployment', type: ESettingType::String, description: 'Azure Embedding 模型部署名')]
|
||||
#[Setting(config: 'ai.providers.azure.image_deployment', type: ESettingType::String, description: 'Azure 图片模型部署名')]
|
||||
|
||||
// Bedrock
|
||||
#[Setting(config: 'ai.providers.bedrock.region', type: ESettingType::String, description: 'Bedrock 区域')]
|
||||
#[Setting(config: 'ai.providers.bedrock.key', type: ESettingType::EncryptedString, description: 'Bedrock API Key')]
|
||||
#[Setting(config: 'ai.providers.bedrock.access_key_id', type: ESettingType::EncryptedString, description: 'Bedrock Access Key ID')]
|
||||
#[Setting(config: 'ai.providers.bedrock.secret_access_key', type: ESettingType::EncryptedString, description: 'Bedrock Secret Access Key')]
|
||||
#[Setting(config: 'ai.providers.bedrock.session_token', type: ESettingType::EncryptedString, description: 'Bedrock Session Token')]
|
||||
|
||||
// Cohere
|
||||
#[Setting(config: 'ai.providers.cohere.key', type: ESettingType::EncryptedString, description: 'Cohere API Key')]
|
||||
|
||||
// DeepSeek
|
||||
#[Setting(config: 'ai.providers.deepseek.key', type: ESettingType::EncryptedString, description: 'DeepSeek API Key')]
|
||||
|
||||
// ElevenLabs
|
||||
#[Setting(config: 'ai.providers.eleven.key', type: ESettingType::EncryptedString, description: 'ElevenLabs API Key')]
|
||||
|
||||
// Gemini
|
||||
#[Setting(config: 'ai.providers.gemini.key', type: ESettingType::EncryptedString, description: 'Gemini API Key')]
|
||||
#[Setting(config: 'ai.providers.gemini.url', type: ESettingType::String, description: 'Gemini API URL')]
|
||||
|
||||
// Groq
|
||||
#[Setting(config: 'ai.providers.groq.key', type: ESettingType::EncryptedString, description: 'Groq API Key')]
|
||||
|
||||
// Jina
|
||||
#[Setting(config: 'ai.providers.jina.key', type: ESettingType::EncryptedString, description: 'Jina API Key')]
|
||||
|
||||
// Mistral
|
||||
#[Setting(config: 'ai.providers.mistral.key', type: ESettingType::EncryptedString, description: 'Mistral API Key')]
|
||||
|
||||
// Ollama
|
||||
#[Setting(config: 'ai.providers.ollama.key', type: ESettingType::EncryptedString, description: 'Ollama API Key')]
|
||||
#[Setting(config: 'ai.providers.ollama.url', type: ESettingType::String, description: 'Ollama API URL')]
|
||||
|
||||
// OpenAI
|
||||
#[Setting(config: 'ai.providers.openai.key', type: ESettingType::EncryptedString, description: 'OpenAI API Key')]
|
||||
#[Setting(config: 'ai.providers.openai.url', type: ESettingType::String, description: 'OpenAI API URL')]
|
||||
|
||||
// OpenRouter
|
||||
#[Setting(config: 'ai.providers.openrouter.key', type: ESettingType::EncryptedString, description: 'OpenRouter API Key')]
|
||||
|
||||
// VoyageAI
|
||||
#[Setting(config: 'ai.providers.voyageai.key', type: ESettingType::EncryptedString, description: 'VoyageAI API Key')]
|
||||
|
||||
// xAI
|
||||
#[Setting(config: 'ai.providers.xai.key', type: ESettingType::EncryptedString, description: 'xAI API Key')]
|
||||
class AiSettings extends SettingsDefinition
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Settings;
|
||||
|
||||
use Modules\SystemTool\Attributes\Setting;
|
||||
use Modules\SystemTool\Base\SettingsDefinition;
|
||||
use Modules\SystemTool\Enum\ESettingType;
|
||||
|
||||
/**
|
||||
* 邮件与服务配置定义
|
||||
*
|
||||
* 对应 config/mail.php 和 config/services.php 中的可配置项。
|
||||
* 敏感值(密码、Token、API Key)使用 EncryptedString 类型存储。
|
||||
*/
|
||||
#[Setting(config: 'mail.default', type: ESettingType::String, description: '默认邮件驱动(smtp / failover / roundrobin / log 等)')]
|
||||
#[Setting(config: 'mail.mailers.smtp.host', type: ESettingType::String, description: 'SMTP 主机')]
|
||||
#[Setting(config: 'mail.mailers.smtp.port', type: ESettingType::Number, description: 'SMTP 端口')]
|
||||
#[Setting(config: 'mail.mailers.smtp.username', type: ESettingType::String, description: 'SMTP 用户名')]
|
||||
#[Setting(config: 'mail.mailers.smtp.password', type: ESettingType::EncryptedString, description: 'SMTP 密码')]
|
||||
#[Setting(config: 'mail.from.address', type: ESettingType::String, description: '发件人邮箱')]
|
||||
#[Setting(config: 'mail.from.name', type: ESettingType::String, description: '发件人名称')]
|
||||
#[Setting(config: 'mail.mailers.log.channel', type: ESettingType::String, description: '日志驱动通道')]
|
||||
|
||||
// Failover / RoundRobin
|
||||
#[Setting(config: 'mail.mailers.failover.mailers', type: ESettingType::Array, description: 'Failover 邮件驱动列表')]
|
||||
#[Setting(config: 'mail.mailers.roundrobin.mailers', type: ESettingType::Array, description: 'RoundRobin 邮件驱动列表')]
|
||||
|
||||
// 第三方服务
|
||||
#[Setting(config: 'services.postmark.token', type: ESettingType::EncryptedString, description: 'Postmark Token')]
|
||||
#[Setting(config: 'services.ses.key', type: ESettingType::EncryptedString, description: 'SES Access Key ID')]
|
||||
#[Setting(config: 'services.ses.secret', type: ESettingType::EncryptedString, description: 'SES Secret Access Key')]
|
||||
#[Setting(config: 'services.ses.region', type: ESettingType::String, description: 'SES 区域')]
|
||||
#[Setting(config: 'services.ses.token', type: ESettingType::EncryptedString, description: 'SES Session Token')]
|
||||
#[Setting(config: 'services.resend.key', type: ESettingType::EncryptedString, description: 'Resend API Key')]
|
||||
#[Setting(config: 'services.mailgun.domain', type: ESettingType::String, description: 'Mailgun 域名')]
|
||||
#[Setting(config: 'services.mailgun.secret', type: ESettingType::EncryptedString, description: 'Mailgun Secret')]
|
||||
#[Setting(config: 'services.mailgun.endpoint', type: ESettingType::String, description: 'Mailgun Endpoint')]
|
||||
class MailSettings extends SettingsDefinition
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Settings;
|
||||
|
||||
use Modules\SystemTool\Attributes\Setting;
|
||||
use Modules\SystemTool\Base\SettingsDefinition;
|
||||
use Modules\SystemTool\Enum\ESettingType;
|
||||
|
||||
/**
|
||||
* 文件存储配置定义
|
||||
*
|
||||
* 对应 config/filesystems.php 中的可配置项。
|
||||
* 敏感值(密钥、密码)使用 EncryptedString 类型存储。
|
||||
*/
|
||||
#[Setting(config: 'filesystems.default', type: ESettingType::String, description: '默认存储驱动(local / s3 / ftp / sftp)')]
|
||||
#[Setting(config: 'filesystems.disks.local.url', type: ESettingType::String, description: '本地存储 URL')]
|
||||
|
||||
// S3
|
||||
#[Setting(config: 'filesystems.disks.s3.key', type: ESettingType::EncryptedString, description: 'S3 Access Key ID')]
|
||||
#[Setting(config: 'filesystems.disks.s3.secret', type: ESettingType::EncryptedString, description: 'S3 Secret Access Key')]
|
||||
#[Setting(config: 'filesystems.disks.s3.region', type: ESettingType::String, description: 'S3 区域')]
|
||||
#[Setting(config: 'filesystems.disks.s3.bucket', type: ESettingType::String, description: 'S3 Bucket')]
|
||||
#[Setting(config: 'filesystems.disks.s3.url', type: ESettingType::String, description: 'S3 URL')]
|
||||
#[Setting(config: 'filesystems.disks.s3.endpoint', type: ESettingType::String, description: 'S3 Endpoint')]
|
||||
#[Setting(config: 'filesystems.disks.s3.use_path_style_endpoint', type: ESettingType::Bool, description: 'S3 使用路径样式端点')]
|
||||
|
||||
// FTP
|
||||
#[Setting(config: 'filesystems.disks.ftp.host', type: ESettingType::String, description: 'FTP 主机')]
|
||||
#[Setting(config: 'filesystems.disks.ftp.username', type: ESettingType::String, description: 'FTP 用户名')]
|
||||
#[Setting(config: 'filesystems.disks.ftp.password', type: ESettingType::EncryptedString, description: 'FTP 密码')]
|
||||
#[Setting(config: 'filesystems.disks.ftp.port', type: ESettingType::Number, description: 'FTP 端口')]
|
||||
#[Setting(config: 'filesystems.disks.ftp.root', type: ESettingType::String, description: 'FTP 根目录')]
|
||||
#[Setting(config: 'filesystems.disks.ftp.passive', type: ESettingType::Bool, description: 'FTP 被动模式')]
|
||||
#[Setting(config: 'filesystems.disks.ftp.ssl', type: ESettingType::Bool, description: 'FTP SSL')]
|
||||
#[Setting(config: 'filesystems.disks.ftp.timeout', type: ESettingType::Number, description: 'FTP 超时(秒)')]
|
||||
|
||||
// SFTP
|
||||
#[Setting(config: 'filesystems.disks.sftp.host', type: ESettingType::String, description: 'SFTP 主机')]
|
||||
#[Setting(config: 'filesystems.disks.sftp.username', type: ESettingType::String, description: 'SFTP 用户名')]
|
||||
#[Setting(config: 'filesystems.disks.sftp.password', type: ESettingType::EncryptedString, description: 'SFTP 密码')]
|
||||
#[Setting(config: 'filesystems.disks.sftp.port', type: ESettingType::Number, description: 'SFTP 端口')]
|
||||
#[Setting(config: 'filesystems.disks.sftp.root', type: ESettingType::String, description: 'SFTP 根目录')]
|
||||
#[Setting(config: 'filesystems.disks.sftp.timeout', type: ESettingType::Number, description: 'SFTP 超时(秒)')]
|
||||
#[Setting(config: 'filesystems.disks.sftp.privateKey', type: ESettingType::EncryptedString, description: 'SFTP 私钥')]
|
||||
#[Setting(config: 'filesystems.disks.sftp.passphrase', type: ESettingType::EncryptedString, description: 'SFTP 私钥口令')]
|
||||
class StorageSettings extends SettingsDefinition
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemTool\Services\SysFileService;
|
||||
use Modules\SystemUser\Http\Requests\SysUserUpdateRequest;
|
||||
use Modules\SystemUser\Models\SysLoginRecordModel;
|
||||
use Modules\SystemUser\Models\SysRuleModel;
|
||||
use Modules\SystemUser\Models\SysUserModel;
|
||||
|
||||
#[RequestAttribute('/system')]
|
||||
class IndexController extends BaseController
|
||||
{
|
||||
|
||||
/** 用户登录 */
|
||||
#[PostRoute('/login', false, 'login_log')]
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'username' => 'required|min:3|alphaDash',
|
||||
'password' => 'required|min:4|alphaDash',
|
||||
]);
|
||||
|
||||
if (Auth::guard('sys_users')->attempt($credentials)) {
|
||||
$userID = auth()->id();
|
||||
$user = SysUserModel::find($userID);
|
||||
$access = $user->access();
|
||||
if($request->input('remember', false)) {
|
||||
$expiration = null;
|
||||
} else {
|
||||
$expiration = now()->addDays(3);
|
||||
}
|
||||
$data = $request->user()
|
||||
->createToken($credentials['username'], $access, $expiration)
|
||||
->toArray();
|
||||
if(empty($data['plainTextToken'])) {
|
||||
return $this->error(__('user.login_error'));
|
||||
}
|
||||
$response = [
|
||||
'token' => $data['plainTextToken']
|
||||
];
|
||||
|
||||
SysUserModel::query()->where('id', $userID)->update([
|
||||
'login_time' => now(),
|
||||
'login_ip' => $request->ip(),
|
||||
]);
|
||||
return $this->success($response, __('user.login_success'));
|
||||
}
|
||||
return $this->error(__('user.login_error'));
|
||||
}
|
||||
|
||||
/** 退出登录 */
|
||||
#[PostRoute('/logout')]
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
$request->user()->currentAccessToken()->delete();
|
||||
return $this->success(__('user.logout_success'));
|
||||
}
|
||||
|
||||
/** 获取管理员信息 */
|
||||
#[GetRoute('/info')]
|
||||
public function info(): JsonResponse
|
||||
{
|
||||
$info = Auth::user();
|
||||
$access = $info->access();
|
||||
return $this->success(compact('access','info'));
|
||||
}
|
||||
|
||||
/** 获取菜单信息 */
|
||||
#[GetRoute('/menu')]
|
||||
public function menu(): JsonResponse
|
||||
{
|
||||
$id = Auth::id();
|
||||
if($id == 1) {
|
||||
$menus = SysRuleModel::query()
|
||||
->where('status', 1)
|
||||
->whereIn('type', ['menu','route'])
|
||||
->get()
|
||||
->toArray();
|
||||
} else {
|
||||
$roles = SysUserModel::with(['roles.rules' => function ($query) {
|
||||
$query->where('status', 1)->whereIn('type', ['menu','route']);
|
||||
}])->find($id)->roles->toArray();
|
||||
|
||||
$menus = collect($roles)
|
||||
->map(fn ($item) => $item['rules'])
|
||||
->collapse()
|
||||
->map(fn ($item) => collect($item)->forget(['pivot', 'updated_at', 'created_at', 'status']) )
|
||||
->unique('id')
|
||||
->toArray();
|
||||
}
|
||||
$menus = getTreeData($menus);
|
||||
return $this->success(compact('menus'));
|
||||
}
|
||||
|
||||
/** 更新管理员信息 */
|
||||
#[PutRoute('/updateInfo')]
|
||||
public function updateInfo(SysUserUpdateRequest $request): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
$id = auth()->id();
|
||||
$model = SysUserModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error(__('user.user_not_exist'));
|
||||
}
|
||||
return $this->success($model->update($data));
|
||||
}
|
||||
|
||||
/** 修改密码 */
|
||||
#[PutRoute('/updatePassword')]
|
||||
public function updatePassword(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'oldPassword' => 'required|string|min:6|max:20',
|
||||
'newPassword' => 'required|string|min:6|max:20',
|
||||
'rePassword' => 'required|same:newPassword',
|
||||
]);
|
||||
$user_id = auth()->id();
|
||||
$user = SysUserModel::find($user_id);
|
||||
if (! password_verify($validated['oldPassword'], $user->password)) {
|
||||
return $this->error(__('user.old_password_error'));
|
||||
}
|
||||
$user->password = Hash::make($validated['newPassword']);
|
||||
$user->save();
|
||||
return $this->success('ok');
|
||||
}
|
||||
|
||||
/** 上传头像 */
|
||||
#[PostRoute('/uploadAvatar')]
|
||||
public function uploadAvatar(Request $request): JsonResponse
|
||||
{
|
||||
$user_id = Auth::id();
|
||||
$file = $request->file('file');
|
||||
$service = new SysFileService();
|
||||
$data = $service->upload($file, 2, 20, $user_id);
|
||||
$user = SysUserModel::find($user_id);
|
||||
$user->avatar_id = $data['id'];
|
||||
$user->save();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/** 获取管理员登录日志 */
|
||||
#[GetRoute('/loginRecord')]
|
||||
public function loginRecord(): JsonResponse
|
||||
{
|
||||
$id = Auth::id();
|
||||
$data = SysLoginRecordModel::where('user_id', $id)
|
||||
->limit(10)
|
||||
->orderBy('id', 'desc')
|
||||
->get()
|
||||
->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemUser\Http\Requests\SysDeptFormRequest;
|
||||
use Modules\SystemUser\Models\SysDeptModel;
|
||||
|
||||
/**
|
||||
* 部门管理控制器
|
||||
*/
|
||||
#[RequestAttribute('/system/dept', 'system.dept')]
|
||||
class SysDeptController extends BaseController
|
||||
{
|
||||
|
||||
/** 部门列表 */
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(): JsonResponse
|
||||
{
|
||||
$data = SysDeptModel::orderBy('sort', 'desc')->get()->toArray();
|
||||
$data = getTreeData($data);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/** 创建部门 */
|
||||
#[PostRoute(authorize: 'create')]
|
||||
public function create(SysDeptFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysDeptModel::create($validated);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 编辑部门 */
|
||||
#[PutRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'update',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function update(int $id, SysDeptFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysDeptModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
$model->update($validated);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 删除部门 */
|
||||
#[DeleteRoute(authorize: 'delete')]
|
||||
public function delete(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'ids' => 'required|array',
|
||||
'ids.*' => 'integer|exists:sys_dept,id'
|
||||
]);
|
||||
|
||||
$ids = $request->input('ids');
|
||||
|
||||
$departmentsWithChildren = SysDeptModel::whereIn('id', $ids)
|
||||
->whereHas('children')
|
||||
->get();
|
||||
|
||||
if ($departmentsWithChildren->isNotEmpty()) {
|
||||
return $this->error('存在下级部门的部门无法删除');
|
||||
}
|
||||
|
||||
SysDeptModel::whereIn('id', $ids)->delete();
|
||||
return $this->success('部门删除成功');
|
||||
}
|
||||
|
||||
/** 获取部门用户列表 */
|
||||
#[GetRoute('/users/{id}', 'users')]
|
||||
public function users(int $id): JsonResponse
|
||||
{
|
||||
$model = SysDeptModel::query()->find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error('部门不存在');
|
||||
}
|
||||
$pageSize = request()->input('pageSize') ?? 10;
|
||||
$data = $model->users()
|
||||
->select(['id', 'username', 'nickname', 'email', 'mobile', 'status'])
|
||||
->paginate($pageSize)
|
||||
->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Http\Controllers;
|
||||
|
||||
use App\Exceptions\RepositoryException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemUser\Http\Requests\SysRoleFormRequest;
|
||||
use Modules\SystemUser\Models\SysRoleModel;
|
||||
use Modules\SystemUser\Models\SysRuleModel;
|
||||
|
||||
/**
|
||||
* 角色管理控制器
|
||||
*/
|
||||
#[RequestAttribute('/system/role', 'system.role')]
|
||||
class SysRoleController extends BaseController
|
||||
{
|
||||
protected array $quickSearchField = ['name', 'description'];
|
||||
protected array $searchField = [
|
||||
'status' => '=',
|
||||
'name' => 'like',
|
||||
];
|
||||
|
||||
/** 查询角色列表 */
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(Request $request): JsonResponse
|
||||
{
|
||||
$params = $request->all();
|
||||
$pageSize = $params['pageSize'] ?? 10;
|
||||
$query = SysRoleModel::query();
|
||||
$data = $this->buildSearch($params, $query)
|
||||
->paginate($pageSize)
|
||||
->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/** 创建角色 */
|
||||
#[PostRoute(authorize: 'create')]
|
||||
public function create(SysRoleFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysRoleModel::create($validated);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 编辑角色 */
|
||||
#[PutRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'update',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function update(int $id, SysRoleFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysRoleModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
$model->update($validated);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 删除角色 */
|
||||
#[DeleteRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'delete',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function delete(int $id): JsonResponse
|
||||
{
|
||||
$model = SysRoleModel::find($id);
|
||||
if (empty($model)) {
|
||||
throw new RepositoryException('角色不存在');
|
||||
}
|
||||
if ($model->countUser > 0) {
|
||||
throw new RepositoryException('该角色下存在用户,无法删除');
|
||||
}
|
||||
$model->delete();
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 获取角色用户列表 */
|
||||
#[GetRoute('/users/{id}', 'users')]
|
||||
public function users(int $id): JsonResponse
|
||||
{
|
||||
$model = SysRoleModel::query()->find($id);
|
||||
if (empty($model)) {
|
||||
throw new RepositoryException('角色不存在');
|
||||
}
|
||||
$pageSize = request()->input('pageSize', 10);
|
||||
$data = $model->users()
|
||||
->paginate($pageSize, ['id', 'username', 'nickname', 'email', 'mobile', 'status'])
|
||||
->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/** 设置启用状态 */
|
||||
#[PutRoute('/status/{id}', 'status')]
|
||||
public function status(int $id): JsonResponse
|
||||
{
|
||||
$model = SysRoleModel::find($id);
|
||||
if (!$model) {
|
||||
return $this->error(__('system.data_not_exist'));
|
||||
}
|
||||
$model->status = $model->status ? 0 : 1;
|
||||
$model->save();
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 获取权限选项 */
|
||||
#[GetRoute('/ruleList', 'ruleList')]
|
||||
public function ruleList(): JsonResponse
|
||||
{
|
||||
$data = SysRuleModel::query()
|
||||
->where("status", 1)
|
||||
->get(['name as title', 'parent_id', 'id as key', 'id', 'local'])
|
||||
->toArray();
|
||||
$data = getTreeData($data);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/** 设置角色权限 */
|
||||
#[PostRoute('/setRule', 'setRule')]
|
||||
public function setRule(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'role_id' => 'required|exists:sys_role,id',
|
||||
'rule_ids' => 'required|array|exists:sys_rule,id',
|
||||
]);
|
||||
if ($validated['role_id'] == 1) {
|
||||
throw new RepositoryException('超级管理员不能修改权限');
|
||||
}
|
||||
$model = SysRoleModel::findOrFail($validated['role_id']);
|
||||
$model->rules()->sync($validated['rule_ids']);
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemUser\Http\Requests\SysRuleFormRequest;
|
||||
use Modules\SystemUser\Models\SysRuleModel;
|
||||
|
||||
/**
|
||||
* 管理员权限控制器
|
||||
*/
|
||||
#[RequestAttribute('/system/rule', 'system.rule')]
|
||||
class SysRuleController extends BaseController
|
||||
{
|
||||
protected array $quickSearchField = ['name', 'key', 'path'];
|
||||
protected array $searchField = [
|
||||
'type' => '=',
|
||||
'status' => '=',
|
||||
'show' => '=',
|
||||
'parent_id' => '=',
|
||||
];
|
||||
|
||||
/** 获取权限列表(树形) */
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(): JsonResponse
|
||||
{
|
||||
$rules = SysRuleModel::all();
|
||||
$data = $rules->toArray();
|
||||
$data = getTreeData($data);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/** 创建权限 */
|
||||
#[PostRoute(authorize: 'create')]
|
||||
public function create(SysRuleFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysRuleModel::create($validated);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 编辑权限 */
|
||||
#[PutRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'update',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function update(int $id, SysRuleFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysRuleModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
$model->update($validated);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 删除权限 */
|
||||
#[DeleteRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'delete',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function delete(int $id): JsonResponse
|
||||
{
|
||||
$model = SysRuleModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
$model->delete();
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 获取父级权限 */
|
||||
#[GetRoute('/parent', authorize: 'parentQuery')]
|
||||
public function getRulesParent(): JsonResponse
|
||||
{
|
||||
$data = SysRuleModel::query()
|
||||
->whereIn('type', ['menu', 'route'])
|
||||
->get(['name', 'id', 'parent_id'])
|
||||
->toArray();
|
||||
$data = getTreeData($data);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/** 设置显示状态 */
|
||||
#[PutRoute('/show/{id}', authorize: 'show')]
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$model = SysRuleModel::find($id);
|
||||
if (!$model) {
|
||||
return $this->error(__('system.data_not_exist'));
|
||||
}
|
||||
$model->hidden = $model->hidden ? 0 : 1;
|
||||
$model->save();
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 设置启用状态 */
|
||||
#[PutRoute('/status/{id}', authorize: 'status')]
|
||||
public function status(int $id): JsonResponse
|
||||
{
|
||||
$model = SysRuleModel::find($id);
|
||||
if (!$model) {
|
||||
return $this->error(__('system.data_not_exist'));
|
||||
}
|
||||
$model->status = $model->status ? 0 : 1;
|
||||
$model->save();
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemUser\Http\Requests\SysUserFormRequest;
|
||||
use Modules\SystemUser\Models\SysDeptModel;
|
||||
use Modules\SystemUser\Models\SysRoleModel;
|
||||
use Modules\SystemUser\Models\SysUserModel;
|
||||
|
||||
/**
|
||||
* 管理员用户控制器
|
||||
*/
|
||||
#[RequestAttribute('/system/user', 'system.user')]
|
||||
class SysUserController extends BaseController
|
||||
{
|
||||
|
||||
/** 查询管理员用户列表 */
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(Request $request): JsonResponse
|
||||
{
|
||||
$params = $request->all();
|
||||
$pageSize = $params['pageSize'] ?? 10;
|
||||
$query = SysUserModel::query();
|
||||
$data = $this->buildSearch($params, $query)
|
||||
->paginate($pageSize)
|
||||
->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/** 创建管理员用户 */
|
||||
#[PostRoute(authorize: 'create')]
|
||||
public function create(SysUserFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$user = SysUserModel::create([
|
||||
'username' => $validated['username'],
|
||||
'nickname' => $validated['nickname'],
|
||||
'email' => $validated['email'],
|
||||
'mobile' => $validated['mobile'],
|
||||
'password' => Hash::make($validated['password']),
|
||||
'status' => $validated['status'] ?? 1,
|
||||
'dept_id' => $validated['dept_id'] ?? null,
|
||||
'sex' => $validated['sex'] ?? 0
|
||||
]);
|
||||
if(empty($user)) {
|
||||
return $this->error();
|
||||
}
|
||||
$user->roles()->sync($validated['role_id'] ?? []);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 编辑管理员用户 */
|
||||
#[PutRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'update',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function update(int $id, SysUserFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$model = SysUserModel::find($id);
|
||||
if (empty($model)) {
|
||||
return $this->error();
|
||||
}
|
||||
$model->roles()->sync($validated['role_id'] ?? []);
|
||||
$model->update($validated);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 删除管理员用户 */
|
||||
#[DeleteRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'delete',
|
||||
where: ['id' => '[0-9]+']
|
||||
)]
|
||||
public function delete(int $id): JsonResponse
|
||||
{
|
||||
if($id == 1) {
|
||||
$this->error('不能删除系统用户!');
|
||||
}
|
||||
$user = SysUserModel::find($id);
|
||||
if (empty($user)) {
|
||||
$this->error('Model not found');
|
||||
}
|
||||
$user->roles()->detach();
|
||||
$user->delete();
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 重置用户密码 */
|
||||
#[PutRoute('/resetPassword', 'resetPassword')]
|
||||
public function resetPassword(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'id' => 'required|exists:sys_user,id',
|
||||
'password' => 'required|string|min:6|max:20',
|
||||
'rePassword' => 'required|same:password',
|
||||
], [
|
||||
'id.required' => '请选择管理员用户!',
|
||||
'id.exists' => '管理员用户不存在!',
|
||||
'password.required' => '请输入管理员密码!',
|
||||
'password.min' => '密码最短为6个字符!',
|
||||
'password.max' => '密码最长伟20个字符!',
|
||||
'rePassword.required' => '请重复输入密码!',
|
||||
'rePassword.same' => '两次输入的密码不同!',
|
||||
]);
|
||||
$user = SysUserModel::find($data['id']);
|
||||
if (!$user) {
|
||||
return $this->error(__('user.user_not_exist'));
|
||||
}
|
||||
$user->password = Hash::make($data['password']);
|
||||
$user->save();
|
||||
return $this->success('ok');
|
||||
}
|
||||
|
||||
/** 获取用户角色选项栏数据 */
|
||||
#[GetRoute('/role', 'role')]
|
||||
public function role(): JsonResponse
|
||||
{
|
||||
$data = SysRoleModel::where('status', 1)
|
||||
->get(['id as role_id', 'name'])
|
||||
->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/** 获取用户部门选项栏数据 */
|
||||
#[GetRoute('/dept', 'dept')]
|
||||
public function dept(): JsonResponse
|
||||
{
|
||||
$field = SysDeptModel::where('status', 0)
|
||||
->select(['id as dept_id', 'name', 'parent_id'])
|
||||
->get()
|
||||
->toArray();
|
||||
$data = $this->buildTree($field);
|
||||
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
private function buildTree(array $items, $parentId = 0): array
|
||||
{
|
||||
$tree = [];
|
||||
foreach ($items as $item) {
|
||||
if ($item['parent_id'] == $parentId) {
|
||||
$children = $this->buildTree($items, $item['dept_id']);
|
||||
$node = [
|
||||
'dept_id' => $item['dept_id'],
|
||||
'name' => $item['name'],
|
||||
'children' => $children
|
||||
];
|
||||
$tree[] = $node;
|
||||
}
|
||||
}
|
||||
return $tree;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\SystemUser\Models\SysAccessToken;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AuthGuardMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, ...$guards): Response
|
||||
{
|
||||
$token = $request->bearerToken();
|
||||
if (!$token) {
|
||||
return response()->json(['msg' => 'Token not provided', 'success' => false], 401);
|
||||
}
|
||||
// 查找 token
|
||||
$accessToken = SysAccessToken::findToken($token);
|
||||
if (!$accessToken) {
|
||||
return response()->json(['msg' => 'Invalid token', 'success' => false], 401);
|
||||
}
|
||||
if (empty($guards)) {
|
||||
$guards = ['sys_users'];
|
||||
}
|
||||
Log::info('Guards: ', $guards);
|
||||
Log::info('Auth Providers: ', config('auth.providers'));
|
||||
foreach ($guards as $guard) {
|
||||
Log::info('Auth Providers Model: ' . config('auth.providers.' . $guard . '.model'));
|
||||
if ($accessToken->tokenable_type == config('auth.providers.' . $guard . '.model')) {
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
return response()->json([
|
||||
'msg' => __('user.not_login'),
|
||||
'success' => false
|
||||
], 401);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\SystemUser\Models\SysLoginRecordModel;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class LoginLogMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
try {
|
||||
// 获取用户名(假设用户名通过请求体传递)
|
||||
$userAgent = $request->userAgent();
|
||||
// 继续处理请求
|
||||
$response = $next($request);
|
||||
$user_id = auth()->id();
|
||||
$username = auth()->user()['username'];
|
||||
// 获取响应状态和消息
|
||||
$content = json_decode($response->getContent(), true); // 响应内容
|
||||
$message = $content['msg'] ?? 'No message'; // 从响应中提取消息
|
||||
SysLoginRecordModel::create([
|
||||
'ipaddr' => $request->ip(),
|
||||
'browser' => $this->getBrowser($userAgent),
|
||||
'os' => $this->getOs($userAgent),
|
||||
'username' => $username,
|
||||
'user_id' => $user_id,
|
||||
'login_location' => $this->getLocation($request->ip()),
|
||||
'status' => $content['success'] ? '0' : '1',
|
||||
'msg' => $message,
|
||||
'login_time' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}catch (\Exception $e) {
|
||||
// 记录错误日志
|
||||
Log::error('Failed to log user login info: ' . $e->getMessage());
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器信息
|
||||
* @param string $userAgent
|
||||
* @return string
|
||||
*/
|
||||
private function getBrowser(string $userAgent): string
|
||||
{
|
||||
$browser = 'XXX';
|
||||
// 简单的解析逻辑(可以根据需要扩展)
|
||||
if (str_contains($userAgent, 'Firefox')) {
|
||||
$browser = 'Firefox';
|
||||
} elseif (str_contains($userAgent, 'Chrome')) {
|
||||
$browser = 'Chrome';
|
||||
} elseif (str_contains($userAgent, 'Safari')) {
|
||||
$browser = 'Safari';
|
||||
} elseif (str_contains($userAgent, 'MSIE') || str_contains($userAgent, 'Trident')) {
|
||||
$browser = 'Internet Explorer';
|
||||
}
|
||||
return $browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作系统信息
|
||||
* @param string $userAgent
|
||||
* @return string
|
||||
*/
|
||||
private function getOs(string $userAgent): string
|
||||
{
|
||||
$os = 'Unknown';
|
||||
if (str_contains($userAgent, 'Windows')) {
|
||||
$os = 'Windows';
|
||||
} elseif (str_contains($userAgent, 'Macintosh')) {
|
||||
$os = 'Mac OS';
|
||||
} elseif (str_contains($userAgent, 'Linux')) {
|
||||
$os = 'Linux';
|
||||
} elseif (str_contains($userAgent, 'Android')) {
|
||||
$os = 'Android';
|
||||
} elseif (str_contains($userAgent, 'iOS')) {
|
||||
$os = 'iOS';
|
||||
}
|
||||
return $os;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 IP 地址对应的地理位置
|
||||
*
|
||||
* @param string $ip
|
||||
* @return string
|
||||
*/
|
||||
private function getLocation(string $ip): string
|
||||
{
|
||||
if($ip == '127.0.0.1') {
|
||||
return '本地';
|
||||
}
|
||||
// 这里可以使用第三方 API(如 IPStack 或 IPInfo)来获取地理位置
|
||||
try {
|
||||
$response = file_get_contents("https://ipinfo.io/{$ip}/json");
|
||||
$data = json_decode($response, true);
|
||||
return $data['city'] . ', ' . $data['country'];
|
||||
}catch (\Exception $e) {
|
||||
return 'XXX';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Http\Requests;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Common\Http\Requests\BaseFormRequest;
|
||||
|
||||
class SysDeptFormRequest extends BaseFormRequest
|
||||
{
|
||||
protected $stopOnFirstFailure = true;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
if (!$this->isUpdate()) {
|
||||
return [
|
||||
'name' => 'required|unique:sys_dept,name',
|
||||
'code' => 'required|unique:sys_dept,code',
|
||||
'type' => 'required|integer|in:0,1,2',
|
||||
'parent_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
function ($attribute, $value, $fail) {
|
||||
if ($value != 0 && !DB::table('sys_dept')->where('id', $value)->exists()) {
|
||||
$fail('选择的上级部门不存在。');
|
||||
}
|
||||
},
|
||||
],
|
||||
'sort' => 'required|integer',
|
||||
'phone' => 'nullable',
|
||||
'address' => 'nullable',
|
||||
'email' => 'nullable|email',
|
||||
'status' => 'required|in:0,1',
|
||||
'remark' => 'nullable',
|
||||
];
|
||||
} else {
|
||||
$id = $this->route('id');
|
||||
return [
|
||||
'name' => [
|
||||
'required',
|
||||
Rule::unique('sys_dept', 'name')->ignore($id)
|
||||
],
|
||||
'code' => [
|
||||
'required',
|
||||
Rule::unique('sys_dept', 'code')->ignore($id)
|
||||
],
|
||||
'type' => 'required|integer|in:0,1,2',
|
||||
'sort' => 'required|integer',
|
||||
'phone' => 'nullable',
|
||||
'address' => 'nullable',
|
||||
'email' => 'nullable|email',
|
||||
'status' => 'required|in:0,1',
|
||||
'remark' => 'nullable',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => '部门名称不能为空',
|
||||
'name.unique' => '部门名称已存在',
|
||||
'code.required' => '部门编码不能为空',
|
||||
'code.unique' => '部门编码已存在',
|
||||
'type.required' => '部门类型不能为空',
|
||||
'type.integer' => '部门类型必须是整数',
|
||||
'type.in' => '部门类型错误',
|
||||
'parent_id.required' => '上级部门不能为空',
|
||||
'parent_id.integer' => '上级部门ID必须是整数',
|
||||
'parent_id.exists' => '选择的上级部门不存在',
|
||||
'sort.required' => '排序字段不能为空',
|
||||
'sort.integer' => '排序字段必须是整数',
|
||||
'email.email' => '请输入有效的邮箱地址',
|
||||
'status.required' => '状态不能为空',
|
||||
'status.in' => '状态类型错误',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Http\Requests;
|
||||
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Common\Http\Requests\BaseFormRequest;
|
||||
|
||||
class SysRoleFormRequest extends BaseFormRequest
|
||||
{
|
||||
protected $stopOnFirstFailure = true;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
if (!$this->isUpdate()) {
|
||||
return [
|
||||
'name' => 'required|unique:sys_role,name',
|
||||
'sort' => 'required|integer|min:0',
|
||||
'description' => 'nullable|string',
|
||||
'status' => 'required|integer|in:0,1',
|
||||
];
|
||||
} else {
|
||||
$id = $this->route('id');
|
||||
return [
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
Rule::unique('sys_role', 'name')->ignore($id),
|
||||
],
|
||||
'sort' => 'required|integer|min:0',
|
||||
'description' => 'nullable|string',
|
||||
'status' => 'required|integer|in:0,1',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => '角色名称不能为空',
|
||||
'name.unique' => '角色名称已存在',
|
||||
'sort.required' => '排序不能为空',
|
||||
'sort.integer' => '排序必须为整数',
|
||||
'status.required' => '状态不能为空',
|
||||
'status.in' => '状态格式错误',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Http\Requests;
|
||||
|
||||
use App\Exceptions\RepositoryException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Common\Http\Requests\BaseFormRequest;
|
||||
|
||||
class SysRuleFormRequest extends BaseFormRequest
|
||||
{
|
||||
protected $stopOnFirstFailure = true;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$type = $this->input('type');
|
||||
if (empty($type)) {
|
||||
throw new RepositoryException('权限类型为必填项!');
|
||||
}
|
||||
if (!in_array($type, ['menu', 'route', 'rule'])) {
|
||||
throw new RepositoryException('权限类型错误!');
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'parent_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
'numeric',
|
||||
function ($attribute, $value, $fail) {
|
||||
if ($value != 0 && !DB::table('sys_rule')->where('id', $value)->exists()) {
|
||||
$fail('选择的上级权限不存在。');
|
||||
}
|
||||
},
|
||||
],
|
||||
'order' => 'required|integer',
|
||||
'name' => 'required',
|
||||
];
|
||||
|
||||
if (!$this->isUpdate()) {
|
||||
$rules['key'] = 'required|unique:sys_rule,key';
|
||||
} else {
|
||||
$rules['key'] = [
|
||||
'required',
|
||||
Rule::unique('sys_rule', 'key')->ignore($this->route('id')),
|
||||
];
|
||||
}
|
||||
|
||||
if ($type == 'menu') {
|
||||
$rules += [
|
||||
'type' => 'required|string|in:menu',
|
||||
'local' => 'nullable|string',
|
||||
'icon' => 'nullable|string',
|
||||
];
|
||||
} elseif ($type == 'route') {
|
||||
$rules += [
|
||||
'type' => 'required|string|in:route',
|
||||
'path' => 'required|string',
|
||||
'local' => 'nullable|string',
|
||||
'icon' => 'nullable|string',
|
||||
'link' => 'required|integer|numeric|in:0,1',
|
||||
];
|
||||
} else {
|
||||
$rules += [
|
||||
'type' => 'required|string|in:rule',
|
||||
];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => '权限名称不能为空',
|
||||
'type.required' => '类型不能为空',
|
||||
'type.in' => '类型格式错误',
|
||||
'order.required' => '排序不能为空',
|
||||
'order.integer' => '排序必须为整数',
|
||||
'key.required' => '唯一标识不能为空',
|
||||
'key.unique' => '唯一标识已存在',
|
||||
'path.required' => '路径不能为空',
|
||||
'parent_id.required' => '父级权限不能为空',
|
||||
'parent_id.integer' => '父级权限格式错误',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Http\Requests;
|
||||
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Common\Http\Requests\BaseFormRequest;
|
||||
|
||||
class SysUserFormRequest extends BaseFormRequest
|
||||
{
|
||||
protected $stopOnFirstFailure = true;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
if(!$this->isUpdate()) {
|
||||
return [
|
||||
'username' => 'required|unique:sys_user,username',
|
||||
'nickname' => 'required',
|
||||
'sex' => 'in:0,1',
|
||||
'mobile' => 'required',
|
||||
'email' => 'required|email|unique:sys_user,email',
|
||||
'dept_id' => 'required|exists:sys_dept,id',
|
||||
'role_id' => 'required|array|exists:sys_role,id',
|
||||
'status' => 'required|int|in:1,0',
|
||||
'password' => 'required|min:6',
|
||||
'rePassword' => 'required|same:password',
|
||||
];
|
||||
} else {
|
||||
$id = $this->route('id');
|
||||
return [
|
||||
'username' => [
|
||||
'required',
|
||||
Rule::unique('sys_user', 'username')->ignore($id)
|
||||
],
|
||||
'nickname' => 'required',
|
||||
'sex' => 'in:0,1',
|
||||
'mobile' => 'required',
|
||||
'email' => [
|
||||
'required',
|
||||
Rule::unique('sys_user', 'email')->ignore($id)
|
||||
],
|
||||
'role_id' => 'required|array|exists:sys_role,id',
|
||||
'dept_id' => 'required|exists:sys_dept,id',
|
||||
'status' => 'required|int|in:1,0',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'username.required' => '用户名不能为空',
|
||||
'username.unique' => '用户名已存在',
|
||||
'nickname.required' => '昵称不能为空',
|
||||
'sex.in' => '性别格式错误',
|
||||
'mobile.required' => '手机号不能为空',
|
||||
'email.required' => '邮箱不能为空',
|
||||
'email.email' => '邮箱格式错误',
|
||||
'email.unique' => '邮箱已存在',
|
||||
'dept_id.exists' => '部门不存在',
|
||||
'role_id.exists' => '角色不存在',
|
||||
'status.required' => '状态不能为空',
|
||||
'status.int' => '状态值错误',
|
||||
'status.in' => '状态格式错误',
|
||||
'password.required' => '密码不能为空',
|
||||
'password.min' => '密码至少6位',
|
||||
'rePassword.required' => '确认密码不能为空',
|
||||
'rePassword.same' => '两次密码不一致',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class SysUserUpdateRequest extends FormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'nickname' => [
|
||||
'required',
|
||||
Rule::unique('sys_user', 'username')->ignore($id)
|
||||
],
|
||||
'sex' => 'required|in:0,1',
|
||||
'bio' => 'sometimes|max:255',
|
||||
'mobile' => [
|
||||
'required',
|
||||
Rule::unique('sys_user', 'mobile')->ignore($id)
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
Rule::unique('sys_user', 'email')->ignore($id)
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'nickname.required' => '昵称不能为空',
|
||||
'sex.required' => '性别不能为空',
|
||||
'sex.in' => '性别格式错误',
|
||||
'bio.max' => '个人简介最大不能超过255个字符',
|
||||
'mobile.required' => '手机号不能为空',
|
||||
'email.required' => '邮箱不能为空',
|
||||
'email.email' => '邮箱格式错误'
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Models;
|
||||
|
||||
use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;
|
||||
|
||||
class SysAccessToken extends SanctumPersonalAccessToken
|
||||
{
|
||||
protected $table = 'sys_access_token';
|
||||
|
||||
public function can($ability): bool
|
||||
{
|
||||
if (
|
||||
$this->tokenable_type == SysUserModel::class
|
||||
&& $this->tokenable_id == 1
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return in_array('*', $this->abilities) ||
|
||||
array_key_exists($ability, array_flip($this->abilities));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 系统用户部门模型
|
||||
*/
|
||||
class SysDeptModel extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
protected $table = 'sys_dept';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $fillable = [
|
||||
'parent_id',
|
||||
'name',
|
||||
'code',
|
||||
'type',
|
||||
'sort',
|
||||
'phone',
|
||||
'email',
|
||||
'status',
|
||||
'address',
|
||||
'remark'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'parent_id' => 'integer',
|
||||
'sort' => 'integer',
|
||||
'status' => 'integer',
|
||||
];
|
||||
|
||||
protected $hidden = [ 'deleted_at' ];
|
||||
|
||||
/**
|
||||
* 定义与用户的关联关系(一个部门有多个用户)
|
||||
*/
|
||||
public function users(): HasMany
|
||||
{
|
||||
return $this->hasMany(SysUserModel::class, 'dept_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义父级部门关联
|
||||
*/
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SysDeptModel::class, 'parent_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义子部门关联
|
||||
*/
|
||||
public function children(): HasMany
|
||||
{
|
||||
return $this->hasMany(SysDeptModel::class, 'parent_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 系统用户登录日志模型
|
||||
*/
|
||||
class SysLoginRecordModel extends Model
|
||||
{
|
||||
protected $table = 'sys_login_record';
|
||||
protected $primaryKey = 'id';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'username',
|
||||
'ipaddr',
|
||||
'login_location',
|
||||
'browser',
|
||||
'os',
|
||||
'status',
|
||||
'msg',
|
||||
'login_time'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'user_id' => 'integer',
|
||||
'login_time' => 'datetime'
|
||||
];
|
||||
|
||||
/**
|
||||
* 定义与用户的关联关系
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SysUserModel::class, 'user_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
/**
|
||||
* 系统角色模型
|
||||
*/
|
||||
class SysRoleModel extends Model
|
||||
{
|
||||
protected $table = 'sys_role';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'sort',
|
||||
'rules',
|
||||
'description',
|
||||
'status'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'sort' => 'integer',
|
||||
'status' => 'integer'
|
||||
];
|
||||
|
||||
protected $appends = ['countUser', 'ruleIds'];
|
||||
|
||||
/**
|
||||
* 角色用户关联
|
||||
*/
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(SysUserModel::class, 'sys_user_role', 'role_id', 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色权限关联中间表
|
||||
*/
|
||||
public function rules(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(SysRuleModel::class, 'sys_role_rule', 'role_id', 'rule_id');
|
||||
}
|
||||
|
||||
/** 用户总数 */
|
||||
public function getCountUserAttribute(): int
|
||||
{
|
||||
return $this->users()->count();
|
||||
}
|
||||
|
||||
/** 拥有的权限ID */
|
||||
public function getRuleIdsAttribute(): array
|
||||
{
|
||||
if(!empty($this->id) && $this->id == 1) {
|
||||
return SysRuleModel::query()->pluck('id')->toArray();
|
||||
}
|
||||
return $this->rules()->pluck('id')->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* 系统规则模型
|
||||
*/
|
||||
class SysRuleModel extends Model
|
||||
{
|
||||
protected $table = 'sys_rule';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $fillable = [
|
||||
'parent_id',
|
||||
'type',
|
||||
'order',
|
||||
'name',
|
||||
'key',
|
||||
'path',
|
||||
'icon',
|
||||
'local',
|
||||
'link',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'parent_id' => 'integer',
|
||||
'sort' => 'integer',
|
||||
'status' => 'integer',
|
||||
'show' => 'integer'
|
||||
];
|
||||
|
||||
/**
|
||||
* 定义子权限关联
|
||||
*/
|
||||
public function children(): HasMany
|
||||
{
|
||||
return $this->hasMany(SysRuleModel::class, 'parent_id', 'id')
|
||||
->orderBy('sort');
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义父权限关联
|
||||
*/
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SysRuleModel::class, 'parent_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色权限关联中间表
|
||||
*/
|
||||
public function roles(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(SysRoleModel::class, 'sys_role_rule', 'rule_id', 'role_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Ai\Concerns\HasConversations;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Modules\SystemTool\Models\SysFileModel;
|
||||
|
||||
/**
|
||||
* 系统用户模型
|
||||
*/
|
||||
class SysUserModel extends User
|
||||
{
|
||||
use SoftDeletes, HasConversations, HasApiTokens, HasFactory, Notifiable;
|
||||
|
||||
protected $table = 'sys_user';
|
||||
protected $primaryKey = 'id';
|
||||
protected $fillable = [
|
||||
'username',
|
||||
'password',
|
||||
'nickname',
|
||||
'avatar_id',
|
||||
'bio',
|
||||
'sex',
|
||||
'mobile',
|
||||
'email',
|
||||
'email_verified_at',
|
||||
'dept_id',
|
||||
'login_ip',
|
||||
'login_time',
|
||||
'status'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
'login_time' => 'datetime',
|
||||
'status' => 'integer',
|
||||
'sex' => 'integer',
|
||||
'avatar_id' => 'integer',
|
||||
'dept_id' => 'integer'
|
||||
];
|
||||
|
||||
protected $appends = ['role_id', 'dept_name', 'avatar_url'];
|
||||
|
||||
protected $with = ['dept', 'avatar'];
|
||||
|
||||
protected $hidden = [
|
||||
'dept',
|
||||
'avatar',
|
||||
'password',
|
||||
'remember_token',
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* 定义与部门的归属关系
|
||||
*/
|
||||
public function dept(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SysDeptModel::class, 'dept_id', 'id');
|
||||
}
|
||||
|
||||
/** 部门名称 */
|
||||
public function getDeptNameAttribute(): string
|
||||
{
|
||||
return $this->dept->name ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义与角色的关联
|
||||
*/
|
||||
public function roles(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(SysRoleModel::class, 'sys_user_role', 'user_id', 'role_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户角色列表
|
||||
*/
|
||||
public function getRoleIdAttribute()
|
||||
{
|
||||
return $this->roles()
|
||||
->pluck('id')->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义与登录日志的关联
|
||||
*/
|
||||
public function loginRecords(): HasMany
|
||||
{
|
||||
return $this->hasMany(SysLoginRecordModel::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联用户头像
|
||||
* @return HasOne
|
||||
*/
|
||||
public function avatar(): HasOne
|
||||
{
|
||||
return $this->hasOne(SysFileModel::class, 'id', 'avatar_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户角色列表
|
||||
*/
|
||||
public function getAvatarUrlAttribute()
|
||||
{
|
||||
if($this->avatar) {
|
||||
return $this->avatar->preview_url;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户所有权限
|
||||
* @return array
|
||||
*/
|
||||
public function access(): array
|
||||
{
|
||||
if($this->id == 1) {
|
||||
return SysRuleModel::query()
|
||||
->where('status', 1)
|
||||
->pluck('key')
|
||||
->toArray();
|
||||
}
|
||||
$roles = SysUserModel::with(['roles.rules' => function ($query) {
|
||||
$query->where('status', 1);
|
||||
}])->find($this->id)->roles->toArray();
|
||||
|
||||
return collect($roles)
|
||||
->map(fn ($item) => $item['rules'] )
|
||||
->collapse()
|
||||
->map(fn ($item) => $item['key'] )
|
||||
->unique()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemUser\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Modules\AnnoRoute\AnnoRoute;
|
||||
use Modules\SystemUser\Models\SysAccessToken;
|
||||
|
||||
class SystemUserServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//...
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(AnnoRoute $annoRoute): void
|
||||
{
|
||||
// 注册路由
|
||||
$annoRoute->register(base_path('modules/SystemUser/Http/Controllers'));
|
||||
|
||||
//
|
||||
Sanctum::usePersonalAccessTokenModel(SysAccessToken::class);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user