初始化项目

This commit is contained in:
2026-02-08 22:38:13 +08:00
commit 334d2c6312
201 changed files with 32724 additions and 0 deletions

View File

@@ -0,0 +1,198 @@
<?php
namespace App\Services\System;
use App\Models\System\City;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
class CityService
{
public function getList(array $params): array
{
$query = City::query();
if (!empty($params['parent_id'])) {
$query->where('parent_id', $params['parent_id']);
}
if (!empty($params['level'])) {
$query->where('level', $params['level']);
}
if (!empty($params['keyword'])) {
$query->where(function ($q) use ($params) {
$q->where('name', 'like', '%' . $params['keyword'] . '%')
->orWhere('code', 'like', '%' . $params['keyword'] . '%')
->orWhere('pinyin', 'like', '%' . $params['keyword'] . '%');
});
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
$query->orderBy('sort')->orderBy('id');
$pageSize = $params['page_size'] ?? 20;
$list = $query->paginate($pageSize);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $list->currentPage(),
'page_size' => $list->perPage(),
];
}
public function getTree(): array
{
return $this->buildTree(City::where('status', true)->orderBy('sort')->get());
}
public function getChildren(int $parentId): array
{
return City::where('parent_id', $parentId)
->where('status', true)
->orderBy('sort')
->get()
->toArray();
}
public function getByCode(string $code): ?City
{
return City::where('code', $code)->first();
}
public function getById(int $id): ?City
{
return City::find($id);
}
public function getByPinyin(string $pinyin): array
{
return City::where('pinyin', 'like', '%' . $pinyin . '%')
->where('status', true)
->get()
->toArray();
}
public function create(array $data): City
{
Validator::make($data, [
'name' => 'required|string|max:100',
'code' => 'required|string|max:50|unique:system_cities,code',
'level' => 'required|integer|in:1,2,3',
'parent_id' => 'sometimes|exists:system_cities,id',
])->validate();
$city = City::create($data);
$this->clearCache();
return $city;
}
public function update(int $id, array $data): City
{
$city = City::findOrFail($id);
Validator::make($data, [
'name' => 'sometimes|required|string|max:100',
'code' => 'sometimes|required|string|max:50|unique:system_cities,code,' . $id,
'level' => 'sometimes|required|integer|in:1,2,3',
'parent_id' => 'sometimes|exists:system_cities,id',
])->validate();
$city->update($data);
$this->clearCache();
return $city;
}
public function delete(int $id): bool
{
$city = City::findOrFail($id);
if ($city->children()->exists()) {
throw new \Exception('该城市下有子级数据,不能删除');
}
$city->delete();
$this->clearCache();
return true;
}
public function batchDelete(array $ids): bool
{
City::whereIn('id', $ids)->delete();
$this->clearCache();
return true;
}
public function batchUpdateStatus(array $ids, bool $status): bool
{
City::whereIn('id', $ids)->update(['status' => $status]);
$this->clearCache();
return true;
}
private function buildTree(array $cities, int $parentId = 0): array
{
$tree = [];
foreach ($cities as $city) {
if ($city['parent_id'] == $parentId) {
$children = $this->buildTree($cities, $city['id']);
if (!empty($children)) {
$city['children'] = $children;
}
$tree[] = $city;
}
}
return $tree;
}
private function clearCache(): void
{
Cache::forget('system:cities:tree');
}
public function getCachedTree(): array
{
$cacheKey = 'system:cities:tree';
$tree = Cache::get($cacheKey);
if ($tree === null) {
$tree = $this->getTree();
Cache::put($cacheKey, $tree, 3600);
}
return $tree;
}
public function getProvinces(): array
{
return City::where('level', 1)
->where('status', true)
->orderBy('sort')
->get()
->toArray();
}
public function getCities(int $provinceId): array
{
return City::where('parent_id', $provinceId)
->where('level', 2)
->where('status', true)
->orderBy('sort')
->get()
->toArray();
}
public function getDistricts(int $cityId): array
{
return City::where('parent_id', $cityId)
->where('level', 3)
->where('status', true)
->orderBy('sort')
->get()
->toArray();
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace App\Services\System;
use App\Models\System\Config;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
class ConfigService
{
public function getList(array $params): array
{
$query = Config::query();
if (!empty($params['group'])) {
$query->where('group', $params['group']);
}
if (!empty($params['keyword'])) {
$query->where(function ($q) use ($params) {
$q->where('name', 'like', '%' . $params['keyword'] . '%')
->orWhere('key', 'like', '%' . $params['keyword'] . '%');
});
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
$pageSize = $params['page_size'] ?? 20;
$list = $query->orderBy('sort')->orderBy('id')->paginate($pageSize);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $list->currentPage(),
'page_size' => $list->perPage(),
];
}
public function getById(int $id): ?Config
{
return Config::find($id);
}
public function getByKey(string $key): ?Config
{
return Config::where('key', $key)->first();
}
public function getByGroup(string $group): array
{
return Config::where('group', $group)
->where('status', true)
->orderBy('sort')
->get()
->toArray();
}
public function getAllConfig(): array
{
$cacheKey = 'system:configs:all';
$configs = Cache::get($cacheKey);
if ($configs === null) {
$configs = Config::where('status', true)
->orderBy('sort')
->get()
->keyBy('key')
->toArray();
Cache::put($cacheKey, $configs, 3600);
}
return $configs;
}
public function getConfigValue(string $key, $default = null)
{
$config = $this->getByKey($key);
if (!$config) {
return $default;
}
return $config->value ?? $config->default_value ?? $default;
}
public function create(array $data): Config
{
Validator::make($data, [
'group' => 'required|string|max:50',
'key' => 'required|string|max:100|unique:system_configs,key',
'name' => 'required|string|max:100',
'type' => 'required|string|in:string,text,number,boolean,select,radio,checkbox,file,json',
])->validate();
$config = Config::create($data);
$this->clearCache();
return $config;
}
public function update(int $id, array $data): Config
{
$config = Config::findOrFail($id);
Validator::make($data, [
'group' => 'sometimes|required|string|max:50',
'key' => 'sometimes|required|string|max:100|unique:system_configs,key,' . $id,
'name' => 'sometimes|required|string|max:100',
'type' => 'sometimes|required|string|in:string,text,number,boolean,select,radio,checkbox,file,json',
])->validate();
$config->update($data);
$this->clearCache();
return $config;
}
public function delete(int $id): bool
{
$config = Config::findOrFail($id);
if ($config->is_system) {
throw new \Exception('系统配置不能删除');
}
$config->delete();
$this->clearCache();
return true;
}
public function batchDelete(array $ids): bool
{
$configs = Config::whereIn('id', $ids)->where('is_system', false)->get();
Config::whereIn('id', $configs->pluck('id'))->delete();
$this->clearCache();
return true;
}
public function batchUpdateStatus(array $ids, bool $status): bool
{
Config::whereIn('id', $ids)->update(['status' => $status]);
$this->clearCache();
return true;
}
private function clearCache(): void
{
Cache::forget('system:configs:all');
}
public function getGroups(): array
{
return Config::where('status', true)
->select('group')
->distinct()
->pluck('group')
->toArray();
}
}

View File

@@ -0,0 +1,210 @@
<?php
namespace App\Services\System;
use App\Models\System\Dictionary;
use App\Models\System\DictionaryItem;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
class DictionaryService
{
public function getList(array $params): array
{
$query = Dictionary::query();
if (!empty($params['keyword'])) {
$query->where(function ($q) use ($params) {
$q->where('name', 'like', '%' . $params['keyword'] . '%')
->orWhere('code', 'like', '%' . $params['keyword'] . '%');
});
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
$pageSize = $params['page_size'] ?? 20;
$list = $query->orderBy('sort')->orderBy('id')->paginate($pageSize);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $list->currentPage(),
'page_size' => $list->perPage(),
];
}
public function getAll(): array
{
return Dictionary::where('status', true)
->orderBy('sort')
->get()
->toArray();
}
public function getById(int $id): ?Dictionary
{
return Dictionary::with('items')->find($id);
}
public function getByCode(string $code): ?Dictionary
{
return Dictionary::where('code', $code)->first();
}
public function getItemsByCode(string $code): array
{
$cacheKey = 'system:dictionary:' . $code;
$items = Cache::get($cacheKey);
if ($items === null) {
$dictionary = Dictionary::where('code', $code)->first();
if ($dictionary) {
$items = DictionaryItem::where('dictionary_id', $dictionary->id)
->where('status', true)
->orderBy('sort')
->get()
->toArray();
Cache::put($cacheKey, $items, 3600);
} else {
$items = [];
}
}
return $items;
}
public function create(array $data): Dictionary
{
Validator::make($data, [
'name' => 'required|string|max:100',
'code' => 'required|string|max:50|unique:system_dictionaries,code',
])->validate();
$dictionary = Dictionary::create($data);
$this->clearCache();
return $dictionary;
}
public function update(int $id, array $data): Dictionary
{
$dictionary = Dictionary::findOrFail($id);
Validator::make($data, [
'name' => 'sometimes|required|string|max:100',
'code' => 'sometimes|required|string|max:50|unique:system_dictionaries,code,' . $id,
])->validate();
$dictionary->update($data);
$this->clearCache();
return $dictionary;
}
public function delete(int $id): bool
{
$dictionary = Dictionary::findOrFail($id);
DictionaryItem::where('dictionary_id', $id)->delete();
$dictionary->delete();
$this->clearCache();
return true;
}
public function batchDelete(array $ids): bool
{
DictionaryItem::whereIn('dictionary_id', $ids)->delete();
Dictionary::whereIn('id', $ids)->delete();
$this->clearCache();
return true;
}
public function batchUpdateStatus(array $ids, bool $status): bool
{
Dictionary::whereIn('id', $ids)->update(['status' => $status]);
$this->clearCache();
return true;
}
private function clearCache(): void
{
$codes = Dictionary::pluck('code')->toArray();
foreach ($codes as $code) {
Cache::forget('system:dictionary:' . $code);
}
}
public function getItemsList(array $params): array
{
$query = DictionaryItem::query();
if (!empty($params['dictionary_id'])) {
$query->where('dictionary_id', $params['dictionary_id']);
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
$query->orderBy('sort')->orderBy('id');
$pageSize = $params['page_size'] ?? 20;
$list = $query->paginate($pageSize);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $list->currentPage(),
'page_size' => $list->perPage(),
];
}
public function createItem(array $data): DictionaryItem
{
Validator::make($data, [
'dictionary_id' => 'required|exists:system_dictionaries,id',
'label' => 'required|string|max:100',
'value' => 'required|string|max:100',
])->validate();
$item = DictionaryItem::create($data);
$this->clearCache();
return $item;
}
public function updateItem(int $id, array $data): DictionaryItem
{
$item = DictionaryItem::findOrFail($id);
Validator::make($data, [
'dictionary_id' => 'sometimes|required|exists:system_dictionaries,id',
'label' => 'sometimes|required|string|max:100',
'value' => 'sometimes|required|string|max:100',
])->validate();
$item->update($data);
$this->clearCache();
return $item;
}
public function deleteItem(int $id): bool
{
$item = DictionaryItem::findOrFail($id);
$item->delete();
$this->clearCache();
return true;
}
public function batchDeleteItems(array $ids): bool
{
DictionaryItem::whereIn('id', $ids)->delete();
$this->clearCache();
return true;
}
public function batchUpdateItemsStatus(array $ids, bool $status): bool
{
DictionaryItem::whereIn('id', $ids)->update(['status' => $status]);
$this->clearCache();
return true;
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace App\Services\System;
use App\Models\System\Log;
use Illuminate\Support\Facades\Auth;
class LogService
{
public function create(array $data): Log
{
return Log::create($data);
}
public function getList(array $params): array
{
$query = $this->buildQuery($params);
$query->orderBy('created_at', 'desc');
$pageSize = $params['page_size'] ?? 20;
$list = $query->paginate($pageSize);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $list->currentPage(),
'page_size' => $list->perPage(),
];
}
/**
* 构建查询(用于导出等场景)
*
* @param array $params
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getListQuery(array $params)
{
return $this->buildQuery($params);
}
/**
* 构建基础查询
*
* @param array $params
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function buildQuery(array $params)
{
$query = Log::query()->with('user:id,name,username');
if (!empty($params['user_id'])) {
$query->where('user_id', $params['user_id']);
}
if (!empty($params['username'])) {
$query->where('username', 'like', '%' . $params['username'] . '%');
}
if (!empty($params['module'])) {
$query->where('module', $params['module']);
}
if (!empty($params['action'])) {
$query->where('action', $params['action']);
}
if (!empty($params['status'])) {
$query->where('status', $params['status']);
}
if (!empty($params['start_date']) && !empty($params['end_date'])) {
$query->whereBetween('created_at', [$params['start_date'], $params['end_date']]);
}
if (!empty($params['ip'])) {
$query->where('ip', 'like', '%' . $params['ip'] . '%');
}
return $query;
}
public function getById(int $id): ?Log
{
return Log::with('user')->find($id);
}
public function delete(int $id): bool
{
$log = Log::findOrFail($id);
return $log->delete();
}
public function clearLogs(string $days = '30'): bool
{
Log::where('created_at', '<', now()->subDays($days))->delete();
return true;
}
public function batchDelete(array $ids): bool
{
Log::whereIn('id', $ids)->delete();
return true;
}
public function getStatistics(array $params = []): array
{
$query = Log::query();
if (!empty($params['start_date']) && !empty($params['end_date'])) {
$query->whereBetween('created_at', [$params['start_date'], $params['end_date']]);
}
$total = $query->count();
$successCount = (clone $query)->where('status', 'success')->count();
$errorCount = (clone $query)->where('status', 'error')->count();
return [
'total' => $total,
'success' => $successCount,
'error' => $errorCount,
];
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace App\Services\System;
use App\Models\System\Task;
use Illuminate\Support\Facades\Validator;
class TaskService
{
public function getList(array $params): array
{
$query = Task::query();
if (!empty($params['keyword'])) {
$query->where(function ($q) use ($params) {
$q->where('name', 'like', '%' . $params['keyword'] . '%')
->orWhere('command', 'like', '%' . $params['keyword'] . '%');
});
}
if (isset($params['is_active']) && $params['is_active'] !== '') {
$query->where('is_active', $params['is_active']);
}
if (!empty($params['type'])) {
$query->where('type', $params['type']);
}
$query->orderBy('id');
$pageSize = $params['page_size'] ?? 20;
$list = $query->paginate($pageSize);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $list->currentPage(),
'page_size' => $list->perPage(),
];
}
public function getAll(): array
{
return Task::all()->toArray();
}
public function getById(int $id): ?Task
{
return Task::find($id);
}
public function create(array $data): Task
{
Validator::make($data, [
'name' => 'required|string|max:100',
'command' => 'required|string|max:255',
'type' => 'required|string|in:command,job,closure',
'expression' => 'required|string',
])->validate();
return Task::create($data);
}
public function update(int $id, array $data): Task
{
$task = Task::findOrFail($id);
Validator::make($data, [
'name' => 'sometimes|required|string|max:100',
'command' => 'sometimes|required|string|max:255',
'type' => 'sometimes|required|string|in:command,job,closure',
'expression' => 'sometimes|required|string',
])->validate();
$task->update($data);
return $task;
}
public function delete(int $id): bool
{
$task = Task::findOrFail($id);
return $task->delete();
}
public function batchDelete(array $ids): bool
{
Task::whereIn('id', $ids)->delete();
return true;
}
public function batchUpdateStatus(array $ids, bool $status): bool
{
Task::whereIn('id', $ids)->update(['is_active' => $status]);
return true;
}
public function run(int $id): array
{
$task = Task::findOrFail($id);
$startTime = microtime(true);
$output = '';
$status = 'success';
$errorMessage = '';
try {
switch ($task->type) {
case 'command':
$command = $task->command;
if ($task->run_in_background) {
$command .= ' > /dev/null 2>&1 &';
}
exec($command, $output, $statusCode);
$output = implode("\n", $output);
if ($statusCode !== 0) {
throw new \Exception('Command failed with status code: ' . $statusCode);
}
break;
case 'job':
$jobClass = $task->command;
if (class_exists($jobClass)) {
dispatch(new $jobClass());
} else {
throw new \Exception('Job class not found: ' . $jobClass);
}
break;
case 'closure':
throw new \Exception('Closure type tasks cannot be run directly');
}
$task->increment('run_count');
} catch (\Exception $e) {
$status = 'error';
$errorMessage = $e->getMessage();
$task->increment('failed_count');
}
$executionTime = round((microtime(true) - $startTime) * 1000);
$task->update([
'last_run_at' => now(),
'last_output' => substr($output, 0, 10000),
]);
return [
'status' => $status,
'output' => $output,
'error_message' => $errorMessage,
'execution_time' => $executionTime,
];
}
public function getStatistics(): array
{
$total = Task::count();
$active = Task::where('is_active', true)->count();
$inactive = Task::where('is_active', false)->count();
return [
'total' => $total,
'active' => $active,
'inactive' => $inactive,
];
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace App\Services\System;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Intervention\Image\Facades\Image;
class UploadService
{
protected $disk;
protected $allowedImageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'];
protected $allowedFileTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'zip', 'rar', 'txt'];
protected $maxFileSize = 10 * 1024 * 1024; // 10MB
public function __construct()
{
$this->disk = Storage::disk('public');
}
public function upload(UploadedFile $file, string $directory = 'uploads', array $options = []): array
{
$extension = strtolower($file->getClientOriginalExtension());
if (!$this->validateFile($file, $extension)) {
throw new \Exception('文件验证失败');
}
$fileName = $this->generateFileName($file, $extension);
$filePath = $directory . '/' . date('Ymd') . '/' . $fileName;
if (in_array($extension, $this->allowedImageTypes) && isset($options['compress'])) {
$this->compressImage($file, $filePath, $options);
} else {
$this->disk->put($filePath, file_get_contents($file));
}
$url = $this->disk->url($filePath);
$fullPath = $this->disk->path($filePath);
return [
'url' => $url,
'path' => $filePath,
'name' => $file->getClientOriginalName(),
'size' => $file->getSize(),
'mime_type' => $file->getMimeType(),
'extension' => $extension,
];
}
public function uploadMultiple(array $files, string $directory = 'uploads', array $options = []): array
{
$results = [];
foreach ($files as $file) {
if ($file instanceof UploadedFile) {
$results[] = $this->upload($file, $directory, $options);
}
}
return $results;
}
public function uploadBase64(string $base64, string $directory = 'uploads', string $fileName = null): array
{
if (preg_match('/^data:image\/(\w+);base64,/', $base64, $matches)) {
$type = $matches[1];
$extension = $type;
$data = substr($base64, strpos($base64, ',') + 1);
$data = base64_decode($data);
if (!$data) {
throw new \Exception('Base64解码失败');
}
$fileName = $fileName ?: $this->generateUniqueFileName($extension);
$filePath = $directory . '/' . date('Ymd') . '/' . $fileName;
$this->disk->put($filePath, $data);
return [
'url' => $this->disk->url($filePath),
'path' => $filePath,
'name' => $fileName,
'size' => strlen($data),
'mime_type' => 'image/' . $type,
'extension' => $extension,
];
}
throw new \Exception('无效的Base64图片数据');
}
public function delete(string $path): bool
{
if ($this->disk->exists($path)) {
return $this->disk->delete($path);
}
return false;
}
public function deleteMultiple(array $paths): bool
{
foreach ($paths as $path) {
$this->delete($path);
}
return true;
}
private function validateFile(UploadedFile $file, string $extension): bool
{
if ($file->getSize() > $this->maxFileSize) {
throw new \Exception('文件大小超过限制');
}
$allowedTypes = array_merge($this->allowedImageTypes, $this->allowedFileTypes);
if (!in_array($extension, $allowedTypes)) {
throw new \Exception('不允许的文件类型');
}
return true;
}
private function generateFileName(UploadedFile $file, string $extension): string
{
return uniqid() . '_' . Str::random(6) . '.' . $extension;
}
private function generateUniqueFileName(string $extension): string
{
return uniqid() . '_' . Str::random(6) . '.' . $extension;
}
private function compressImage(UploadedFile $file, string $filePath, array $options): void
{
$quality = $options['quality'] ?? 80;
$width = $options['width'] ?? null;
$height = $options['height'] ?? null;
$image = Image::make($file);
if ($width || $height) {
$image->resize($width, $height, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
}
$image->encode(null, $quality);
$this->disk->put($filePath, (string) $image);
}
public function getFileUrl(string $path): string
{
return $this->disk->url($path);
}
public function fileExists(string $path): bool
{
return $this->disk->exists($path);
}
}