新增用户权限信息获取功能,重构设备删除逻辑,优化验证规则,更新路由以支持新功能。同时,添加慢查询日志记录功能,调整缓存配置,完善字典数据服务,更新基础模型以支持软删除。

This commit is contained in:
zijing 2025-07-15 19:49:34 +08:00
parent b43bf56e8e
commit 9b96bae23e
12 changed files with 554 additions and 205 deletions

View File

@ -295,29 +295,128 @@ class AuthController extends BaseController
$validator = Validator::make($request->all(), [
'token_id' => ['required', 'integer', 'exists:personal_access_tokens,id']
], [
'token_id.required' => '请指定要删除的Token ID',
'token_id.exists' => 'Token不存在'
'token_id.required' => '请选择要删除的设备',
'token_id.exists' => '设备不存在',
]);
if ($validator->fails()) {
return $this->Field($validator->errors()->first(), 422);
}
$tokenId = $request->token_id;
$currentTokenId = $request->user()->currentAccessToken()->id;
$user = $request->user();
$tokenId = $request->input('token_id');
// 不能删除当前正在使用的token
if ($tokenId == $currentTokenId) {
return $this->Field('不能删除当前正在使用的设备', 400);
// 验证Token属于当前用户
$tokenToDelete = $user->tokens()->find($tokenId);
if (!$tokenToDelete) {
return $this->Field('无权删除此设备Token', 403);
}
// 只能删除自己的token
$deleted = $request->user()->tokens()->where('id', $tokenId)->delete();
$tokenToDelete->delete();
if ($deleted) {
return $this->Success(['message' => '设备删除成功']);
} else {
return $this->Field('设备删除失败', 500);
}
/**
* 获取用户权限信息
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function permissionInfo(Request $request): JsonResponse
{
$user = $request->user();
// 获取用户角色
$userRoles = $user->roles()->where('status', 0)->get();
$roleCodes = $userRoles->pluck('code')->toArray();
// 判断是否为超级管理员username为admin或角色编码包含admin相关
$isSuperAdmin = $user->username === 'admin' ||
in_array('admin', $roleCodes) ||
in_array('super_admin', $roleCodes);
// 获取菜单权限
if ($isSuperAdmin) {
// 超级管理员获取所有菜单
$menus = \App\Models\System\SystemMenu::where('status', 0)
->where('visible', 1)
->whereIn('type', [1, 2])
->orderBy('sort', 'asc')
->orderBy('id', 'asc')
->get();
} else {
// 普通用户根据角色权限获取菜单
$roleIds = $userRoles->pluck('id')->toArray();
if (empty($roleIds)) {
$menus = collect([]);
} else {
$menuIds = \App\Models\System\SystemRoleMenu::whereIn('role_id', $roleIds)
->pluck('menu_id')
->unique()
->toArray();
$menus = \App\Models\System\SystemMenu::whereIn('id', $menuIds)
->where('status', 0)
->where('visible', 1)
->whereIn('type', [1, 2])
->orderBy('sort', 'asc')
->orderBy('id', 'asc')
->get();
}
}
// 构建菜单树形结构,使用表字段名称而不是驼峰命名
$menuTree = $this->buildMenuTree($menus->toArray());
return $this->SuccessObject([
'menus' => $menuTree,
'roles' => $roleCodes,
'user' => [
'id' => $user->id,
'username' => $user->username,
'nickname' => $user->nickname,
'avatar' => $user->avatar ?? '',
'dept_id' => $user->dept_id,
]
]);
}
/**
* 构建菜单树形结构
*
* @param array $menus
* @param int $parentId
* @return array
*/
private function buildMenuTree(array $menus, int $parentId = 0): array
{
$tree = [];
foreach ($menus as $menu) {
if ($menu['parent_id'] == $parentId) {
// 使用表字段名称,不使用驼峰命名
$menuItem = [
'id' => $menu['id'],
'parentId' => $menu['parent_id'],
'name' => $menu['name'],
'component' => $menu['component'],
'componentName' => $menu['component_name'],
'icon' => $menu['icon'],
'keepAlive' => $menu['keep_alive'],
'visible' => $menu['visible'],
'alwaysShow' => $menu['always_show'],
'path' => $menu['path'],
];
// 递归获取子菜单
$children = $this->buildMenuTree($menus, $menu['id']);
$menuItem['children'] = $children;
$tree[] = $menuItem;
}
}
return $tree;
}
}

View File

@ -32,8 +32,9 @@ class SystemDictDataController extends BaseController
*/
public function simpleList(SystemDictDataRequest $request): JsonResponse
{
// 不需要验证
$params = $request->validated();
$result = $this->systemDictDataService->getByType($params['dict_type'], true);
$result = $this->systemDictDataService->getByType($params['dict_type'] ?? '', $params['only_active'] ?? false);
return $this->Success($result);
}

View File

@ -229,7 +229,7 @@ class SystemDictDataRequest extends BaseRequest
private function simpleListRules(): array
{
return [
'dict_type' => ['required', 'string', 'max:100', 'exists:system_dict_type,type'],
'dict_type' => ['sometimes', 'string', 'max:100'],
];
}

View File

@ -3,16 +3,12 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class BaseModel extends Model
{
use SoftDeletes;
// 自定义时间戳字段
const CREATED_AT = 'create_time';
const UPDATED_AT = 'update_time';
const DELETED_AT = 'deleted';
/**
* 是否启用系统字段自动维护
@ -88,6 +84,54 @@ class BaseModel extends Model
return $user ? $user->tenant_id : null;
}
/**
* 软删除将deleted字段设置为1
*/
public function delete()
{
if ($this->enableSystemFields) {
return $this->update(['deleted' => 1]);
}
return parent::delete();
}
/**
* 强制删除(真正删除记录)
*/
public function forceDelete()
{
return parent::delete();
}
/**
* 恢复软删除的记录
*/
public function restore()
{
if ($this->enableSystemFields) {
return $this->update(['deleted' => 0]);
}
return false;
}
/**
* 查询包含已删除记录的作用域
*/
public function scopeWithDeleted($query)
{
return $query->withoutGlobalScope('not_deleted');
}
/**
* 只查询已删除记录的作用域
*/
public function scopeOnlyDeleted($query)
{
return $query->withoutGlobalScope('not_deleted')->where('deleted', 1);
}
/**
* 设置系统字段(创建时)
*/

View File

@ -2,6 +2,9 @@
namespace App\Providers;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@ -19,6 +22,49 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
//
$this->setupSlowQueryLogging();
}
/**
* 设置慢查询日志记录
*/
private function setupSlowQueryLogging(): void
{
try {
DB::listen(function (QueryExecuted $query) {
$this->logSlowQuery($query);
});
} catch (\Exception $e) {
// 如果SQL日志系统初始化失败记录到默认日志但不影响主要业务
}
}
/**
* 记录慢查询
*/
private function logSlowQuery(QueryExecuted $query): void
{
try {
// 根据环境设置不同的慢查询阈值, 本地环境所有查询都记录, 正式环境慢查询阈值500ms
$slowQueryThreshold = $this->app->environment(['local','development']) ? 0 : 500;
// 只记录慢查询
if ($query->time > $slowQueryThreshold) {
Log::channel('sql')->info('Slow Query: ' . $query->sql, [
'bindings' => $query->bindings,
'time' => $query->time . 'ms',
'environment' => $this->app->environment()
]);
}
} catch (\Exception $e) {
// 慢查询日志记录失败,降级处理
Log::error('慢查询日志记录失败', [
'query' => $query->sql,
'bindings' => $query->bindings,
'time' => $query->time . 'ms'
]);
}
}
}

View File

@ -105,7 +105,6 @@ class TokenAuthService
try {
// 使用Sanctum验证token
$accessToken = PersonalAccessToken::findToken($token);
if (!$accessToken) {
return null;
}
@ -123,7 +122,7 @@ class TokenAuthService
}
// 检查用户状态
if (!$user->is_active) {
if ($user->status != 0) {
return null;
}

View File

@ -72,15 +72,19 @@ class SystemDictDataService extends BaseService
* @param bool $onlyActive
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getByType(string $dictType, bool $onlyActive = true)
public function getByType(string $dictType = '', bool $onlyActive = true)
{
$query = SystemDictData::where('dict_type', $dictType);
$query = SystemDictData::query();
if ($dictType) {
$query->where('dict_type', $dictType);
}
if ($onlyActive) {
$query->where('status', SystemDictData::STATUS_NORMAL);
}
return $query->orderBy('sort')->get();
return $query->select('id', 'label', 'value', 'dict_type')->get();
}
/**

View File

@ -15,7 +15,7 @@ return [
|
*/
'default' => env('CACHE_STORE', 'file'),
'default' => env('CACHE_STORE', 'redis'),
/*
|--------------------------------------------------------------------------

View File

@ -51,18 +51,18 @@ return [
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
'channels' => ['daily'],
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/default/laravel.log'),
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
'permission' => 0664,
],
'daily' => [

View File

@ -0,0 +1,290 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\System\SystemMenu;
use App\Models\System\SystemRole;
use App\Models\System\SystemRoleMenu;
use App\Models\System\SystemUserRole;
use App\Models\User;
use Carbon\Carbon;
class SystemMenuSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$now = Carbon::now();
$creator = 'system';
// 清空现有数据
SystemRoleMenu::query()->delete();
SystemUserRole::query()->delete();
SystemMenu::query()->delete();
SystemRole::query()->delete();
// 创建角色
$adminRole = SystemRole::create([
'name' => '超级管理员',
'code' => 'superadmin',
'sort' => 1,
'data_scope' => 1,
'status' => 0,
'type' => 1,
'remark' => '超级管理员角色',
'creator' => $creator,
'create_time' => $now,
'updater' => $creator,
'update_time' => $now,
'deleted' => 0,
'tenant_id' => 1,
]);
$testRole = SystemRole::create([
'name' => '测试角色',
'code' => 'test',
'sort' => 2,
'data_scope' => 2,
'status' => 0,
'type' => 1,
'remark' => '测试角色',
'creator' => $creator,
'create_time' => $now,
'updater' => $creator,
'update_time' => $now,
'deleted' => 0,
'tenant_id' => 1,
]);
// 创建菜单数据参考test.json的结构
$menus = [
// 练习模块
[
'id' => 2972,
'name' => '练习',
'parent_id' => 0,
'type' => 1,
'path' => '/exercise',
'icon' => 'fa:tree',
'component' => null,
'component_name' => null,
'status' => 0,
'visible' => 0,
'keep_alive' => 1,
'always_show' => 1,
'sort' => 1,
],
[
'id' => 2892,
'name' => '同步教材详情',
'parent_id' => 2972,
'type' => 2,
'path' => 'textbook/catalog',
'icon' => 'ep:list',
'component' => '/textbook/catalog/index',
'component_name' => 'TextbookCatalog',
'status' => 0,
'visible' => 0,
'keep_alive' => 1,
'always_show' => 1,
'sort' => 1,
],
[
'id' => 2898,
'name' => '同步教材',
'parent_id' => 2972,
'type' => 2,
'path' => 'textbook/catalogList',
'icon' => 'fa-solid:book',
'component' => '/textbook/catalog/list',
'component_name' => null,
'status' => 0,
'visible' => 0,
'keep_alive' => 1,
'always_show' => 1,
'sort' => 2,
],
// 基础数据管理模块
[
'id' => 2808,
'name' => '基础数据管理',
'parent_id' => 0,
'type' => 1,
'path' => '/infra-data',
'icon' => 'fa-solid:database',
'component' => null,
'component_name' => null,
'status' => 0,
'visible' => 0,
'keep_alive' => 1,
'always_show' => 1,
'sort' => 2,
],
[
'id' => 2930,
'name' => '题目列表',
'parent_id' => 2808,
'type' => 2,
'path' => 'question/list',
'icon' => 'fa-solid:list-ul',
'component' => '/question/index',
'component_name' => 'InfraDataQuestion',
'status' => 0,
'visible' => 0,
'keep_alive' => 1,
'always_show' => 1,
'sort' => 1,
],
// 系统管理模块
[
'id' => 1,
'name' => '系统管理',
'parent_id' => 0,
'type' => 1,
'path' => '/system',
'icon' => 'ep:tools',
'component' => null,
'component_name' => null,
'status' => 0,
'visible' => 0,
'keep_alive' => 1,
'always_show' => 1,
'sort' => 10,
],
[
'id' => 100,
'name' => '用户管理',
'parent_id' => 1,
'type' => 2,
'path' => 'user',
'icon' => 'ep:avatar',
'component' => 'system/user/index',
'component_name' => 'SystemUser',
'status' => 0,
'visible' => 0,
'keep_alive' => 1,
'always_show' => 1,
'sort' => 1,
],
[
'id' => 101,
'name' => '角色管理',
'parent_id' => 1,
'type' => 2,
'path' => 'role',
'icon' => 'ep:user',
'component' => 'system/role/index',
'component_name' => 'SystemRole',
'status' => 0,
'visible' => 0,
'keep_alive' => 1,
'always_show' => 1,
'sort' => 2,
],
[
'id' => 102,
'name' => '菜单管理',
'parent_id' => 1,
'type' => 2,
'path' => 'menu',
'icon' => 'ep:menu',
'component' => 'system/menu/index',
'component_name' => 'SystemMenu',
'status' => 0,
'visible' => 0,
'keep_alive' => 1,
'always_show' => 1,
'sort' => 3,
],
[
'id' => 105,
'name' => '字典管理',
'parent_id' => 1,
'type' => 2,
'path' => 'dict',
'icon' => 'ep:collection',
'component' => 'system/dict/index',
'component_name' => 'SystemDictType',
'status' => 0,
'visible' => 0,
'keep_alive' => 1,
'always_show' => 1,
'sort' => 4,
],
];
// 批量插入菜单数据
foreach ($menus as $menuData) {
$menuData['creator'] = $creator;
$menuData['create_time'] = $now;
$menuData['updater'] = $creator;
$menuData['update_time'] = $now;
$menuData['deleted'] = 0;
$menuData['tenant_id'] = 1;
SystemMenu::create($menuData);
}
// 为超级管理员分配所有菜单权限
$allMenuIds = SystemMenu::pluck('id')->toArray();
foreach ($allMenuIds as $menuId) {
SystemRoleMenu::create([
'role_id' => $adminRole->id,
'menu_id' => $menuId,
'creator' => $creator,
'create_time' => $now,
'deleted' => 0,
'tenant_id' => 1,
]);
}
// 为测试角色只分配部分菜单权限(系统管理模块)
$testMenuIds = [1, 100, 101, 102, 105]; // 系统管理相关菜单
foreach ($testMenuIds as $menuId) {
SystemRoleMenu::create([
'role_id' => $testRole->id,
'menu_id' => $menuId,
'creator' => $creator,
'create_time' => $now,
'deleted' => 0,
'tenant_id' => 1,
]);
}
// 为现有用户分配角色
$adminUser = User::where('username', 'admin')->first();
if ($adminUser) {
SystemUserRole::create([
'user_id' => $adminUser->id,
'role_id' => $adminRole->id,
'creator' => $creator,
'create_time' => $now,
'deleted' => 0,
'tenant_id' => 1,
]);
}
$testUser = User::where('username', 'test')->first();
if ($testUser) {
SystemUserRole::create([
'user_id' => $testUser->id,
'role_id' => $testRole->id,
'creator' => $creator,
'create_time' => $now,
'deleted' => 0,
'tenant_id' => 1,
]);
}
$this->command->info('菜单和角色权限数据创建成功!');
$this->command->info('- 创建了超级管理员角色superadmin拥有所有菜单权限');
$this->command->info('- 创建了测试角色test仅拥有系统管理模块权限');
$this->command->info('- admin用户 -> 超级管理员角色');
$this->command->info('- test用户 -> 测试角色');
}
}

View File

@ -39,6 +39,9 @@ Route::middleware('admin.auth')->group(function () {
// 获取当前用户信息
Route::get('/me', [AuthController::class, 'me'])->name('admin.me');
// 获取用户权限信息
Route::get('/permission/info', [AuthController::class, 'permissionInfo'])->name('admin.permission.info');
// 刷新Token
Route::post('/refresh', [AuthController::class, 'refresh'])->name('admin.refresh');
@ -52,21 +55,8 @@ Route::middleware('admin.auth')->group(function () {
Route::delete('/cache', [AuthController::class, 'clearAuthCache'])->name('admin.cache.clear');
Route::get('/cache/stats', [AuthController::class, 'getCacheStats'])->name('admin.cache.stats');
// 缓存测试(仅开发环境)
if (config('app.debug')) {
Route::get('/cache/test', function () {
$tokenAuthService = app(\App\Services\Auth\TokenAuthService::class);
//
$stats = $tokenAuthService->getCacheStats();
return response()->json([
'success' => true,
'message' => '缓存测试结果',
'data' => $stats,
'timestamp' => now()->toDateTimeString()
]);
})->name('admin.cache.test');
}
});
// 系统管理模块
@ -80,63 +70,4 @@ Route::middleware('admin.auth')->group(function () {
Route::delete('role/batch', [\App\Http\Controllers\Admin\System\SystemRoleController::class, 'batchDestroy'])->name('admin.system.role.batch.destroy');
});
// 仪表盘数据
Route::get('/dashboard', function () {
$user = Auth::user();
return response()->json([
'success' => true,
'data' => [
'message' => '欢迎来到后台管理系统',
'user' => [
'id' => $user->id,
'username' => $user->username,
'nickname' => $user->nickname,
],
'stats' => [
'total_users' => \App\Models\User::count(),
'online_users' => 1,
'system_info' => [
'php_version' => PHP_VERSION,
'laravel_version' => app()->version(),
]
]
],
'code' => 200,
'message' => 'success'
]);
})->name('admin.dashboard');
// 异常处理测试路由(仅开发环境)
if (config('app.debug')) {
Route::prefix('test')->group(function () {
// 测试参数验证异常
Route::post('/validation', function () {
request()->validate([
'required_field' => 'required|string',
'email_field' => 'required|email',
]);
return response()->json(['message' => '验证通过']);
})->name('admin.test.validation');
// 测试业务异常
Route::get('/business-exception', function () {
Handler::throw('测试数据不存在', 404);
})->name('admin.test.business');
// 测试系统异常
Route::get('/system-exception', function () {
throw new \Exception('测试系统异常');
})->name('admin.test.system');
// 测试参数错误异常
Route::get('/param-error', function () {
Handler::error('测试参数错误');
})->name('admin.test.param');
// 测试操作失败异常
Route::get('/fail', function () {
Handler::fail('测试操作失败');
})->name('admin.test.fail');
});
}
});

View File

@ -1,118 +1,53 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Services\Auth\TokenAuthService;
use App\Http\Middleware\AdminApiAuthenticate;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
Route::get('/', function () {
return view('welcome');
});
// 认证缓存测试路由
Route::get('/test-auth-cache', function (TokenAuthService $tokenAuthService) {
$output = [];
$output[] = "=== 认证缓存测试 ===";
$output[] = "";
// 1. 测试服务实例化
$output[] = "1. 测试TokenAuthService...";
$output[] = "✅ TokenAuthService实例化成功";
$output[] = "";
// 2. 测试缓存状态
$output[] = "2. 测试缓存健康状态...";
$stats = $tokenAuthService->getCacheStats();
$output[] = "缓存驱动: " . ($stats['cache_store'] ?? 'unknown');
$output[] = "缓存可用: " . ($stats['cache_available'] ? '✅ 是' : '❌ 否');
$output[] = "缓存健康: " . ($stats['cache_health'] ? '✅ 正常' : '❌ 异常');
$output[] = "缓存前缀: " . $stats['cache_prefix'];
$output[] = "默认缓存时间: " . $stats['default_cache_minutes'] . ' 分钟';
$output[] = "最大缓存时间: " . $stats['max_cache_minutes'] . ' 分钟';
$output[] = "";
// 3. 测试token验证
$output[] = "3. 测试token验证...";
$testToken = 'test_invalid_token_123';
$user = $tokenAuthService->validateTokenAndGetUser($testToken);
if ($user === null) {
$output[] = "✅ 无效token正确返回null";
} else {
$output[] = "❌ 无效token验证失败";
}
$output[] = "";
// 4. 测试中间件
$output[] = "4. 测试中间件功能...";
// Redis缓存测试路由
Route::get('/redis-test', function () {
try {
// 创建模拟请求
$request = Request::create('/admin/auth/me', 'GET');
$request->headers->set('Authorization', 'Bearer invalid_token');
$request->headers->set('Accept', 'application/json');
$results = [];
// 实例化中间件
$middleware = new AdminApiAuthenticate($tokenAuthService);
// 测试配置
$results['cache_driver'] = config('cache.default');
$results['redis_config'] = config('database.redis.cache');
// 测试中间件
$response = $middleware->handle($request, function ($req) {
return response()->json(['message' => 'Success']);
});
// 测试缓存写入
$testKey = 'redis_test_' . time();
$testValue = 'Hello Redis from Laravel at ' . now();
if ($response->getStatusCode() === 401) {
$output[] = "✅ 中间件正确返回401错误";
$responseData = json_decode($response->getContent(), true);
$output[] = "响应消息: " . ($responseData['message'] ?? 'unknown');
} else {
$output[] = "❌ 中间件未正确处理无效token";
Cache::put($testKey, $testValue, 60);
$results['write_test'] = '✓ 成功';
// 测试缓存读取
$readValue = Cache::get($testKey);
$results['read_test'] = $readValue === $testValue ? '✓ 成功' : '✗ 失败';
$results['read_value'] = $readValue;
// 测试缓存删除
$deleted = Cache::forget($testKey);
$results['delete_test'] = $deleted ? '✓ 成功' : '✗ 失败';
// 验证删除
$afterDelete = Cache::get($testKey);
$results['delete_verification'] = $afterDelete === null ? '✓ 成功' : '✗ 失败';
return response()->json([
'success' => true,
'message' => 'Redis缓存测试完成',
'results' => $results
]);
} catch (Exception $e) {
return response()->json([
'success' => false,
'message' => 'Redis缓存测试失败',
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
], 500);
}
} catch (\Exception $e) {
$output[] = "❌ 中间件测试失败: " . $e->getMessage();
}
$output[] = "";
// 5. 缓存性能测试
$output[] = "5. 缓存性能测试...";
if ($stats['cache_health']) {
$startTime = microtime(true);
// 模拟多次缓存操作
for ($i = 0; $i < 10; $i++) {
$tokenAuthService->validateTokenAndGetUser('test_token_' . $i);
}
$endTime = microtime(true);
$duration = round(($endTime - $startTime) * 1000, 2);
$output[] = "✅ 完成10次token验证耗时: {$duration}ms";
} else {
$output[] = "⚠️ 缓存不可用,跳过性能测试";
}
$output[] = "";
$output[] = "=== 测试完成 ===";
// 总结
$cacheStatus = $stats['cache_health'] ? '正常工作' : '使用数据库回退';
$output[] = "";
$output[] = "组件状态总结:";
$output[] = "- 缓存功能: " . $cacheStatus;
$output[] = "- 中间件: ✅ 正常工作";
$output[] = "- Token验证: ✅ 正常工作";
$output[] = "- 异常处理: ✅ 正常工作";
$output[] = "";
$output[] = "提示如果要启用Redis缓存请确保";
$output[] = "1. Redis服务正在运行";
$output[] = "2. 在.env文件中设置 CACHE_STORE=redis";
$output[] = "3. 在.env文件中设置 REDIS_CLIENT=predis";
return response('<pre>' . implode("\n", $output) . '</pre>')
->header('Content-Type', 'text/html; charset=utf-8');
});
})->name('redis.test');