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,262 @@
<?php
namespace Modules\Common\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Modules\AnnoRoute\Attribute\DeleteRoute;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PostRoute;
use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use ReflectionClass;
use ReflectionException;
use Symfony\Component\Finder\Finder;
class GenerateRouteHelperCommand extends Command
{
protected $signature = 'route:helper';
protected $description = 'Scan annotation routes and generate IDE route helper files for Laravel Idea';
private static array $mapping = [
GetRoute::class,
PostRoute::class,
PutRoute::class,
DeleteRoute::class,
];
private static array $httpMethodMap = [
GetRoute::class => 'get',
PostRoute::class => 'post',
PutRoute::class => 'put',
DeleteRoute::class => 'delete',
];
/** @var array<string, list<array{method: string, uri: string, controller: string, action: string, middleware: array}>> */
private array $controllerRoutes = [];
public function handle(): int
{
$paths = [
app_path('Http/Controllers'),
base_path('modules'),
];
$totalRoutes = 0;
foreach ($paths as $path) {
if (!is_dir($path)) {
continue;
}
$finder = new Finder();
$finder->files()->in($path)->name('*Controller.php');
foreach ($finder as $file) {
$className = $this->getClassNameFromFile(
$file->getRealPath(),
$file->getPath()
);
if ($className && class_exists($className)) {
$routes = $this->scanController($className);
if (!empty($routes)) {
$this->controllerRoutes[$className] = $routes;
$totalRoutes += count($routes);
}
}
}
}
if (empty($this->controllerRoutes)) {
$this->warn('No annotation routes found.');
return self::SUCCESS;
}
$this->writeHelperFile();
$this->info("Routes generated to: " . base_path('routes/api.php'));
$this->info("Found {$totalRoutes} routes.");
return self::SUCCESS;
}
private function scanController(string $className): array
{
$routes = [];
try {
$classRef = new ReflectionClass($className);
} catch (ReflectionException) {
return $routes;
}
$classAttrs = collect($classRef->getAttributes());
if ($classAttrs->isEmpty()) {
return $routes;
}
$hasRequestAttr = $classAttrs->some(
fn($attr) => $attr->getName() === RequestAttribute::class
);
if (!$hasRequestAttr) {
return $routes;
}
$requestAttr = $classAttrs->first(
fn($attr) => $attr->getName() === RequestAttribute::class
);
$requestInstance = $requestAttr->newInstance();
$routePrefix = $requestInstance->routePrefix ?? '';
$authGuard = $requestInstance->authGuard ?? null;
$abilitiesPrefix = $requestInstance->abilitiesPrefix ?? '';
$classMiddleware = $this->normalizeMiddleware($requestInstance->middleware);
foreach ($classRef->getMethods() as $method) {
$attrs = $method->getAttributes();
if (empty($attrs)) {
continue;
}
$methodName = $method->getName();
foreach ($attrs as $attr) {
if (in_array($attr->getName(), self::$mapping)) {
$instance = $attr->newInstance();
$authMiddleware = $this->buildAuthMiddleware(
$instance->authorize,
$authGuard,
$abilitiesPrefix,
);
$allMiddleware = array_values(array_unique(array_merge(
$authMiddleware,
$this->normalizeMiddleware($instance->middleware),
$classMiddleware,
)));
$routes[] = [
'method' => self::$httpMethodMap[$attr->getName()],
'uri' => $instance->route,
'prefix' => trim($routePrefix, '/'),
'controller' => $className,
'action' => $methodName,
'middleware' => $allMiddleware,
];
}
}
}
return $routes;
}
private function buildAuthMiddleware(string|bool $authorize, ?string $authGuard, string $abilitiesPrefix): array
{
if (empty($authorize) || $authorize === false) {
return [];
}
$authMiddleware = ['auth:sanctum'];
if (!empty($authGuard)) {
$authMiddleware[] = 'authGuard:' . $authGuard;
} else {
$authMiddleware[] = 'authGuard';
}
if (is_string($authorize) && !empty($abilitiesPrefix)) {
$authMiddleware[] = 'abilities:' . $abilitiesPrefix . '.' . $authorize;
} else if ($authorize !== true) {
$authMiddleware[] = 'abilities:' . $authorize ;
}
return $authMiddleware;
}
private function normalizeMiddleware(string|array $middleware): array
{
if (empty($middleware)) {
return [];
}
if (is_array($middleware)) {
return $middleware;
}
return [$middleware];
}
private function getClassNameFromFile(string $filePath, string $fileDir): ?string
{
$content = file_get_contents($filePath);
if (!preg_match('/namespace\s+([^;]+);/', $content, $namespaceMatch)) {
return $this->guessClassNameFromPath($filePath, $fileDir);
}
$namespace = trim($namespaceMatch[1]);
$className = basename($filePath, '.php');
return $namespace . '\\' . $className;
}
private function guessClassNameFromPath(string $filePath, string $fileDir): ?string
{
$basePath = base_path();
$relativePath = str_replace($basePath, '', $fileDir);
$namespace = str_replace('/', '\\', ltrim($relativePath, '/'));
$namespace = trim($namespace, '\\');
$className = basename($filePath, '.php');
return empty($namespace) ? $className : $namespace . '\\' . $className;
}
private function writeHelperFile(): void
{
$outputPath = base_path('routes/api.php');
$lines = [
'<?php',
'',
'use Illuminate\Support\Facades\Route;',
'',
'Route::get(\'/\', function () {',
' return "Hello, thank you for using XinAdmin. ";',
'});',
'',
];
// Each controller gets its own Route::controller group
foreach ($this->controllerRoutes as $className => $routes) {
$prefix = $routes[0]['prefix'];
$shortName = substr(strrchr($className, '\\'), 1);
$lines[] = "// {$shortName}";
$chain = "Route::controller({$className}::class)";
if ($prefix !== '') {
$chain .= "->prefix('{$prefix}')";
}
$chain .= '->group(function () {';
$lines[] = $chain;
foreach ($routes as $route) {
$uri = $route['uri'];
if ($uri === '') {
$uri = '/';
}
$middlewareStr = '';
if (!empty($route['middleware'])) {
$middlewareStr = "->middleware(['" . implode("', '", $route['middleware']) . "'])";
}
$lines[] = " Route::{$route['method']}('{$uri}', '{$route['action']}'){$middlewareStr};";
}
$lines[] = '});';
$lines[] = '';
}
File::put($outputPath, implode("\n", $lines));
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace Modules\Common\Enum;
/**
* 响应状态枚举类
*/
enum ShowType: int
{
// 成功响应
case SUCCESS_MESSAGE = 0;
// 警告响应
case WARN_MESSAGE = 1;
// 错误响应
case ERROR_MESSAGE = 2;
// 成功通知
case SUCCESS_NOTIFICATION = 3;
// 警告通知
case WARN_NOTIFICATION = 4;
// 错误通知
case ERROR_NOTIFICATION = 5;
// 静默响应
case SILENT = 99;
}
@@ -0,0 +1,119 @@
<?php
namespace Modules\Common\Http\Controllers;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Routing\Controller;
use Modules\Common\Trait\RequestJson;
abstract class BaseController extends Controller
{
use RequestJson;
/**
* 查询字符串
* The fields queried by the current model
*
* @var array
*/
protected array $searchField = [];
/**
* 快速搜索字段
* Quick search field
*
* @var array
*/
protected array $quickSearchField = [];
/** 构建查询方法 */
protected function buildSearch(array $params, Builder $model): Builder
{
// 构建筛选
if (isset($params['filter']) && $params['filter'] != '') {
if(is_array($params['filter'])) {
$filter = $params['filter'];
} else {
$filter = json_decode($params['filter'], true);
}
foreach ($filter as $k => $v) {
if (! $v) {
continue;
}
$model->whereIn($k, $v);
}
unset($params['filter']);
}
// 构建查询
foreach ($this->searchField ?? [] as $key => $op) {
if (isset($params[$key]) && $params[$key] != '') {
if (in_array($op, ['=', '>', '<>', '<', '>=', '<='])) {
$model->where($key, $op, $params[$key]);
continue;
}
if ($op == 'like') {
$model->where($key, $op, '%'.$params[$key].'%');
continue;
}
if ($op == 'afterLike') {
$model->where($key, $op, $params[$key].'%');
continue;
}
if ($op == 'beforeLike') {
$model->where($key, $op, '%'.$params[$key]);
continue;
}
if ($op == 'date') {
$date = date('Y-m-d', strtotime($params[$key]));
$model->whereDate($key, $date);
continue;
}
if ($op == 'betweenDate') {
if (is_array($params[$key])) {
$start = $params[$key][0];
$end = $params[$key][1];
$model->whereDate($key, '>=', $start);
$model->whereDate($key, '<=', $end);
}
}
}
}
// 快速搜索
if (isset($params['keywordSearch']) && $params['keywordSearch'] != '') {
$quickSearchArr = $this->quickSearchField ?? [];
if (count($quickSearchArr) > 0) {
$model->whereAny(
$quickSearchArr,
'like',
'%'.str_replace('%', '\%', $params['keywordSearch']).'%'
);
}
}
// 构建排序
if (isset($params['sorter']) && $params['sorter']) {
if(is_array($params['sorter'])) {
$sorter = $params['sorter'];
} else {
$sorter = json_decode($params['sorter'], true);
}
if (count($sorter) > 0) {
$column = array_keys($sorter)[0];
$direction = $sorter[$column] == 'ascend' ? 'asc' : 'desc';
$model->orderBy($column, $direction);
}
}
return $model;
}
}
@@ -0,0 +1,23 @@
<?php
namespace Modules\Common\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class BaseFormRequest extends FormRequest
{
/**
* 是否为更新请求
* @return bool
*/
protected function isUpdate(): bool
{
if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
return true;
}
return false;
}
}
@@ -0,0 +1,34 @@
<?php
namespace Modules\Common\Middlewares;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class AllowCrossDomainMiddleware
{
/**
* Handle an incoming request.
*
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// 添加跨域头
$response->headers->set('Access-Control-Allow-Origin', '*');
$response->headers->set('Access-Control-Allow-Credentials', 'true');
$response->headers->set('Access-Control-Max-Age', 1800);
$response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
$response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, User-Language');
// 如果是预检请求, 返回 204
if ($request->isMethod('OPTIONS')) {
return response()->json([], 204, $response->headers->all());
}
return $response;
}
}
@@ -0,0 +1,91 @@
<?php
namespace Modules\Common\Middlewares;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Session;
class LanguageMiddleware
{
/**
* 支持的语言列表
*/
protected array $supportedLanguages = [
'en' => 'en', // 英语
'zh' => 'zh', // 简体中文
'jp' => 'ja', // 日语
];
/**
* 默认语言
*/
protected string $defaultLanguage = 'zh';
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next)
{
// 获取当前语言
$locale = $this->getLocale($request);
// 设置应用语言
App::setLocale($locale);
// 让请求继续处理
return $next($request);
}
/**
* 获取当前语言设置
*/
protected function getLocale(Request $request): string
{
// 优先级 1: URL 参数 (例如 ?lang=en)
if ($request->has('lang')) {
$lang = $request->get('lang');
if ($this->isSupported($lang)) {
return $lang;
}
}
// 优先级 2: User-Language 头
$browserLocale = $this->getBrowserLocale($request);
if ($browserLocale && $this->isSupported($browserLocale)) {
return $browserLocale;
}
// 优先级 3: Session 中存储的语言
if (Session::has('locale')) {
$lang = Session::get('locale');
if ($this->isSupported($lang)) {
return $lang;
}
}
// 优先级 4: 配置文件中的默认语言
return config('app.locale', $this->defaultLanguage);
}
/**
* 从 User-Language 头中获取浏览器偏好语言
*/
protected function getBrowserLocale(Request $request): ?string
{
$acceptLanguage = $request->header('User-Language');
if (!$acceptLanguage) {
return null;
}
return $acceptLanguage;
}
/**
* 检查语言是否被支持
*/
protected function isSupported(string $locale): bool
{
return array_key_exists($locale, $this->supportedLanguages);
}
}
@@ -0,0 +1,35 @@
<?php
namespace Modules\Common\Providers;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\ServiceProvider;
class PaginationProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->app->bind('Illuminate\Pagination\LengthAwarePaginator', function ($app, $options) {
return new class(
$options['items'],
$options['total'],
$options['perPage'],
$options['currentPage'],
$options['options']
) extends LengthAwarePaginator {
public function toArray(): array
{
return [
'data' => $this->items(),
'total' => $this->total(),
'pageSize' => $this->perPage(),
'current' => $this->currentPage(),
];
}
};
});
}
}
+155
View File
@@ -0,0 +1,155 @@
<?php
namespace Modules\Common\Trait;
use App\Exceptions\HttpResponseException;
use Illuminate\Http\JsonResponse;
use Modules\Common\Enum\ShowType as ShopTypeEnum;
/**
* 响应 trait
* 支持 throw 响应
*/
trait RequestJson
{
/**
* 成功响应
*
* @param string|array $data 响应数据
* @param string $message 响应内容
*/
protected function success(string|array $data = [], string $message = 'ok'): JsonResponse
{
if (is_array($data)) {
return self::renderJson(true, $data, $message);
}
return self::renderJson(true, [], $data);
}
/**
* 抛出成功响应,中断程序运行
*
* @param string|array $data 响应数据
* @param string $message 响应内容
*/
protected function throwSuccess(string|array $data = [], string $message = 'ok'): void
{
if (is_array($data)) {
self::renderThrow(true, $data, $message);
}
self::renderThrow(true, [], $data);
}
/**
* 返回失败响应
*
* @param string|array $data 响应数据
* @param string $message 响应内容
*/
protected function error(string|array $data = [], string $message = ''): JsonResponse
{
if (is_array($data)) {
return self::renderJson(false, $data, $message, ShopTypeEnum::ERROR_MESSAGE);
}
return self::renderJson(false, [], $data, ShopTypeEnum::ERROR_MESSAGE);
}
/**
* 抛出失败响应,中断程序运行
*
* @param string|array $data 响应数据
* @param string $message 响应内容
*/
protected function throwError(string|array $data = [], string $message = ''): void
{
if (is_array($data)) {
self::renderThrow(false, $data, $message, ShopTypeEnum::ERROR_MESSAGE);
}
self::renderThrow(false, [], $data, ShopTypeEnum::ERROR_MESSAGE);
}
/**
* 返回警告响应
*
* @param string|array $data 响应数据
* @param string $message 响应内容
*/
protected function warn(string|array $data = [], string $message = ''): JsonResponse
{
if (is_array($data)) {
return self::renderJson(false, $data, $message, ShopTypeEnum::WARN_MESSAGE);
}
return self::renderJson(false, [], $data, ShopTypeEnum::WARN_MESSAGE);
}
/**
* 抛出失败警告,中断程序运行
*
* @param string|array $data 响应数据
* @param string $message 响应内容
*/
protected function throwWarn(string|array $data = [], string $message = ''): void
{
if (is_array($data)) {
self::renderThrow(false, $data, $message, ShopTypeEnum::WARN_MESSAGE);
}
self::renderThrow(false, [], $data, ShopTypeEnum::WARN_MESSAGE);
}
/**
* 通知响应
*
* @param string $msg 通知标题
* @param string $description 通知描述
* @param string $placement 通知位置 top topLeft topRight bottom bottomLeft bottomRight
* @param ShopTypeEnum $showTypeEnum 通知类型
*/
protected function notification(
string $msg,
string $description,
ShopTypeEnum $showTypeEnum = ShopTypeEnum::SUCCESS_NOTIFICATION,
string $placement = 'topRight'
): JsonResponse {
$showType = $showTypeEnum->value;
$success = false;
return response()->json(compact('description', 'success', 'msg', 'showType', 'placement'));
}
/**
* 返回 Json 响应
*
* @param bool $success 响应状态
* @param array $data 响应数据
* @param string $msg 响应内容
*/
protected static function renderJson(
bool $success = true,
array $data = [],
string $msg = '',
ShopTypeEnum $showTypeEnum = ShopTypeEnum::SUCCESS_MESSAGE
): JsonResponse {
$showType = $showTypeEnum->value;
return response()->json(compact('data', 'success', 'msg', 'showType'));
}
/**
* 抛出 API 数据
*
* @param bool $success 响应状态
* @param mixed $data 返回数据
* @param string $msg 响应内容
* @param ShopTypeEnum $showTypeEnum
*/
public static function renderThrow(
bool $success = true,
array $data = [],
string $msg = '',
ShopTypeEnum $showTypeEnum = ShopTypeEnum::SUCCESS_MESSAGE
) {
$showType = $showTypeEnum->value;
throw new HttpResponseException(compact('data', 'success', 'msg', 'showType'));
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
use Modules\SystemTool\Services\SysSiteConfigService;
if (! function_exists('site_config')) {
/**
* 获取或设置系统配置
*
* @param string|null $name 配置名称,格式:'group.key' 或 'group',为null时返回所有配置
* @param mixed|null $default 默认值(仅在获取时使用)
* @return mixed
*
* @example
* site_config('site.name') // 获取配置
* site_config('site.name', 'Default') // 带默认值获取
* site_config('site') // 获取整个组
* site_config() // 获取所有配置
*/
function site_config(?string $name = null, mixed $default = null): mixed
{
return SysSiteConfigService::getSiteConfig($name, $default);
}
}
if (! function_exists('web_path')) {
/**
* Get the path to the web of the install.
*
* @param string $path
* @return string
*/
function web_path(string $path = ''): string
{
return base_path('web'. DIRECTORY_SEPARATOR . $path);
}
}
if (! function_exists('getTreeData')) {
/**
* 获取树形数据
*
* @param array $list
* @param int $parentId
* @param string[] $params
* @return array
*/
function getTreeData(
array &$list,
int $parentId = 0,
array $params = []
): array
{
$params = array_merge($params, [
'id' => 'id',
'parent_id' => 'parent_id',
'children' => 'children'
]);
$data = [];
foreach ($list as $k => $item) {
if ($item[$params['parent_id']] == $parentId) {
$children = getTreeData($list, $item[$params['id']]);
!empty($children) && $item[$params['children']] = $children;
$data[] = $item;
unset($list[$k]);
}
}
return $data;
}
}