first commit

This commit is contained in:
liu
2026-07-13 15:23:29 +08:00
commit 50885a98c8
473 changed files with 33772 additions and 0 deletions
@@ -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());
}
}
}