first commit
This commit is contained in:
@@ -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
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user