diff --git a/app/Aspect/AdminLoginLogAspect.php b/app/Aspect/AdminLoginLogAspect.php new file mode 100644 index 0000000..b6ed509 --- /dev/null +++ b/app/Aspect/AdminLoginLogAspect.php @@ -0,0 +1,114 @@ +process(); + $this->loginSuccess = true; + $this->loginMsg = 'success'; + } catch (Throwable $throwable) { + $this->loginSuccess = false; + $this->loginMsg = $throwable->getMessage(); + throw $throwable; + } finally { + $this->writeLoginLog(); + } + + // 返回 + return $res; + } + + + /** + * @return void + */ + private function writeLoginLog(): void + { + Coroutine::create(function () { + $userInfo = $this->adminUserRepository->findByUserName($this->request->input('username')); + $context = [ + 'username' => $this->request->input('username',''), + 'password' => $this->request->input('password',''), + 'user_info' => $userInfo?->toArray() ?? [], + 'ip' => $this->getClientIp(), + 'os' => $this->getClientOs(), + 'browser' => $this->request->header('User-Agent') ?: 'unknown', + ]; + + $this->logger->request()->info('admin_login_log', $context); + + $this->adminUserLoginLogModel->save([ + 'admin_user_id' => $userInfo?->id ?? 0, + 'username' => $userInfo?->username ?? '', + 'ip' => $context['ip'], + 'os' => $context['os'], + 'browser' => $context['browser'] ?? '', + 'status' => $this->loginSuccess ? 1 : 2, + 'message' => $this->loginMsg, + ]); + }); + } +} diff --git a/app/Aspect/AdminReturnAspect.php b/app/Aspect/AdminReturnLogAspect.php similarity index 63% rename from app/Aspect/AdminReturnAspect.php rename to app/Aspect/AdminReturnLogAspect.php index 56c100a..cab3f93 100644 --- a/app/Aspect/AdminReturnAspect.php +++ b/app/Aspect/AdminReturnLogAspect.php @@ -6,6 +6,10 @@ namespace App\Aspect; use App\Lib\Log\Logger; use App\Lib\Return\AdminReturn; +use App\Model\AdminUser; +use App\Model\AdminUserOperationLog; +use App\Service\Admin\Login\LoginService; +use App\Trait\AdminUserTrait; use App\Trait\ClientIpTrait; use Hyperf\Context\Context; use Hyperf\Coroutine\Coroutine; @@ -16,9 +20,10 @@ use Psr\Container\ContainerInterface; use Hyperf\Di\Aop\ProceedingJoinPoint; #[Aspect] -class AdminReturnAspect extends AbstractAspect +class AdminReturnLogAspect extends AbstractAspect { use ClientIpTrait; + use AdminUserTrait; /** * 切入类 @@ -33,15 +38,23 @@ class AdminReturnAspect extends AbstractAspect */ protected int $adminId = 0; + /** + * @var ?AdminUser + */ + protected ?AdminUser $adminUserInfo = null; + /** * @param Logger $logger * @param RequestInterface $request + * @param AdminUserOperationLog $adminUserOperationLogModel */ public function __construct( protected readonly Logger $logger, protected readonly RequestInterface $request, + protected readonly AdminUserOperationLog $adminUserOperationLogModel, ) { $this->adminId = Context::get('current_admin_id',0); + if ($this->adminId > 0) $this->adminUserInfo = $this->getAdminUserInfo($this->adminId); } public function process(ProceedingJoinPoint $proceedingJoinPoint) @@ -53,7 +66,7 @@ class AdminReturnAspect extends AbstractAspect $responseData = $proceedingJoinPoint->process(); // 写日志 - $this->writeLog($requestData, $responseData); + $this->writeOperationLog($requestData, $responseData); // 返回 return $requestData; @@ -64,7 +77,7 @@ class AdminReturnAspect extends AbstractAspect * @param array $responseData * @return void */ - private function writeLog(array $requestData = [], array $responseData = []): void + private function writeOperationLog(array $requestData = [], array $responseData = []): void { Coroutine::create(function () use ($requestData, $responseData) { $context = [ @@ -77,6 +90,15 @@ class AdminReturnAspect extends AbstractAspect ]; $this->logger->request()->info('admin_request_log', $context); + + $this->adminUserOperationLogModel->save([ + 'admin_user_id' => $this->adminId, + 'username' => $this->adminUserInfo?->username ?? '', + 'method' => $context['method'], + 'router' => $context['router'], + 'service_name' => $context['service_name'] ?? '', + 'ip' => $context['ip'], + ]); }); } } diff --git a/app/Constants/ResultCode.php b/app/Constants/ResultCode.php index dcb75b9..a767592 100644 --- a/app/Constants/ResultCode.php +++ b/app/Constants/ResultCode.php @@ -25,4 +25,7 @@ class ResultCode extends AbstractConstants #[Message("token错误")] final public const int JWT_ERROR = 10002; + + #[Message("旧密码错误")] + final public const int OLD_PASSWORD_ERROR = 10003; } diff --git a/app/Controller/Admin/AdminMenuController.php b/app/Controller/Admin/AdminMenuController.php index e17713c..b4346ac 100644 --- a/app/Controller/Admin/AdminMenuController.php +++ b/app/Controller/Admin/AdminMenuController.php @@ -6,6 +6,7 @@ namespace App\Controller\Admin; use App\Annotation\Permission; use App\Annotation\ResponseFormat; +use App\Controller\AbstractController; use App\Middleware\Admin\AdminTokenMiddleware; use App\Middleware\Admin\PermissionMiddleware; use App\Service\Admin\AdminUser\MenuService; @@ -19,7 +20,7 @@ use Hyperf\HttpServer\Annotation\RequestMapping; #[ResponseFormat('admin')] #[Middleware(middleware: AdminTokenMiddleware::class, priority: 100)] #[Middleware(middleware: PermissionMiddleware::class, priority: 99)] -class AdminMenuController +class AdminMenuController extends AbstractController { /** * @var MenuService diff --git a/app/Controller/Admin/AdminRoleController.php b/app/Controller/Admin/AdminRoleController.php index 1d93f4c..7572a12 100644 --- a/app/Controller/Admin/AdminRoleController.php +++ b/app/Controller/Admin/AdminRoleController.php @@ -6,6 +6,7 @@ namespace App\Controller\Admin; use App\Annotation\Permission; use App\Annotation\ResponseFormat; +use App\Controller\AbstractController; use App\Middleware\Admin\AdminTokenMiddleware; use App\Middleware\Admin\PermissionMiddleware; use App\Service\Admin\AdminUser\RoleService; @@ -19,7 +20,7 @@ use Hyperf\HttpServer\Annotation\RequestMapping; #[ResponseFormat('admin')] #[Middleware(middleware: AdminTokenMiddleware::class, priority: 100)] #[Middleware(middleware: PermissionMiddleware::class, priority: 99)] -class AdminRoleController +class AdminRoleController extends AbstractController { /** * @var RoleService diff --git a/app/Controller/Admin/AdminUserController.php b/app/Controller/Admin/AdminUserController.php index 8848cc4..cc2cebd 100644 --- a/app/Controller/Admin/AdminUserController.php +++ b/app/Controller/Admin/AdminUserController.php @@ -6,6 +6,7 @@ namespace App\Controller\Admin; use App\Annotation\Permission; use App\Annotation\ResponseFormat; +use App\Controller\AbstractController; use App\Middleware\Admin\AdminTokenMiddleware; use App\Middleware\Admin\PermissionMiddleware; use App\Service\Admin\AdminUser\UserService; @@ -19,7 +20,7 @@ use Hyperf\HttpServer\Annotation\RequestMapping; #[ResponseFormat('admin')] #[Middleware(middleware: AdminTokenMiddleware::class, priority: 100)] #[Middleware(middleware: PermissionMiddleware::class, priority: 99)] -class AdminUserController +class AdminUserController extends AbstractController { /** * @var UserService diff --git a/app/Controller/Admin/Log/AdminUserLoginLogController.php b/app/Controller/Admin/Log/AdminUserLoginLogController.php new file mode 100644 index 0000000..5387ce8 --- /dev/null +++ b/app/Controller/Admin/Log/AdminUserLoginLogController.php @@ -0,0 +1,49 @@ +service->handle(); + } + + /** + * @return array + */ + #[RequestMapping(path: "", methods: "DELETE")] + #[Permission(code: 'log:userLogin:delete')] + public function delete(): array + { + return $this->service->deleteLog(); + } +} diff --git a/app/Controller/Admin/Log/AdminUserOperationLogController.php b/app/Controller/Admin/Log/AdminUserOperationLogController.php new file mode 100644 index 0000000..9841e94 --- /dev/null +++ b/app/Controller/Admin/Log/AdminUserOperationLogController.php @@ -0,0 +1,49 @@ +service->handle(); + } + + /** + * @return array + */ + #[RequestMapping(path: "", methods: "DELETE")] + #[Permission(code: 'log:userOperation:delete')] + public function delete(): array + { + return $this->service->deleteLog(); + } +} diff --git a/app/Controller/Admin/PermissionController.php b/app/Controller/Admin/PermissionController.php new file mode 100644 index 0000000..e877584 --- /dev/null +++ b/app/Controller/Admin/PermissionController.php @@ -0,0 +1,54 @@ +service->handle(); + } + + /** + * @return array + */ + #[RequestMapping(path: "roles", methods: "GET")] + public function roles(): array + { + return $this->service->getRoleByAdminUser(); + } + + /** + * @return array + */ + #[RequestMapping(path: "update", methods: "POST")] + public function update(): array + { + return $this->service->update(); + } +} diff --git a/app/Model/AdminUserLoginLog.php b/app/Model/AdminUserLoginLog.php index efe73cc..833e4a4 100644 --- a/app/Model/AdminUserLoginLog.php +++ b/app/Model/AdminUserLoginLog.php @@ -6,6 +6,9 @@ namespace App\Model; +use Carbon\Carbon; +use Hyperf\Database\Model\Events\Creating; + /** * @property int $id * @property int $admin_user_id @@ -20,6 +23,9 @@ namespace App\Model; */ class AdminUserLoginLog extends Model { + + public bool $timestamps = false; + /** * The table associated with the model. */ @@ -28,10 +34,21 @@ class AdminUserLoginLog extends Model /** * The attributes that are mass assignable. */ - protected array $fillable = []; + protected array $fillable = ['id', 'username', 'ip', 'os', 'browser', 'status', 'message', 'login_time', 'remark']; /** * The attributes that should be cast to native types. */ protected array $casts = ['id' => 'integer', 'admin_user_id' => 'integer', 'status' => 'integer']; + + /** + * @param Creating $event + * @return void + */ + public function creating(Creating $event): void + { + if ($event->getModel()->login_time === null) { + $event->getModel()->login_time = Carbon::now(); + } + } } diff --git a/app/Model/AdminUserOperationLog.php b/app/Model/AdminUserOperationLog.php index f80a079..b0d4606 100644 --- a/app/Model/AdminUserOperationLog.php +++ b/app/Model/AdminUserOperationLog.php @@ -28,7 +28,7 @@ class AdminUserOperationLog extends Model /** * The attributes that are mass assignable. */ - protected array $fillable = []; + protected array $fillable = ['id', 'username', 'method', 'router', 'service_name', 'ip', 'ip_location', 'created_at', 'updated_at', 'remark']; /** * The attributes that should be cast to native types. diff --git a/app/Repository/AdminUserLoginLogRepository.php b/app/Repository/AdminUserLoginLogRepository.php new file mode 100644 index 0000000..40942ef --- /dev/null +++ b/app/Repository/AdminUserLoginLogRepository.php @@ -0,0 +1,57 @@ +when(Arr::get($params, 'username'), static function (Builder $query, $username) { + $query->where('username', $username); + }) + ->when(Arr::get($params, 'ip'), static function (Builder $query, $ip) { + $query->where('ip', $ip); + }) + ->when(Arr::get($params, 'os'), static function (Builder $query, $os) { + $query->where('os', $os); + }) + ->when(Arr::get($params, 'browser'), static function (Builder $query, $browser) { + $query->where('browser', $browser); + }) + ->when(Arr::get($params, 'status'), static function (Builder $query, $status) { + $query->where('status', $status); + }) + ->when(Arr::get($params, 'message'), static function (Builder $query, $message) { + $query->where('message', $message); + }) + ->when(Arr::get($params, 'login_time'), static function (Builder $query, $login_time) { + $query->whereBetween('login_time', $login_time); + }) + ->when(Arr::get($params, 'remark'), static function (Builder $query, $remark) { + $query->where('remark', $remark); + }); + } +} \ No newline at end of file diff --git a/app/Repository/AdminUserOperationLogRepository.php b/app/Repository/AdminUserOperationLogRepository.php new file mode 100644 index 0000000..011e3ba --- /dev/null +++ b/app/Repository/AdminUserOperationLogRepository.php @@ -0,0 +1,18 @@ +adminReturn->success( + 'success', + $this->getAdminUserInfo($this->adminId)->isSuperAdmin() ? + $this->getAdminMenuBySuperAdmin() : + $this->getAdminMenuByAdminId() + ); + } + + /** + * @return array + */ + private function getAdminMenuByAdminId(): array + { + $permissions = $this->getAdminUserInfo($this->adminId)->getPermissions()->pluck('name')->unique(); + + $menuList = $permissions->isEmpty() ? + [] : + $this->adminMenuRepository->list([ + 'status' => AdminMenuStatusCode::Normal, + 'name' => $permissions->toArray() + ])->toArray(); + + $tree = []; + $map = []; + + foreach ($menuList as &$menu) { + $menu['children'] = []; + $map[$menu['id']] = &$menu; + } + unset($menu); + + foreach ($menuList as &$menu) { + $pid = $menu['parent_id']; + if ($pid === 0 || !isset($map[$pid])) { + $tree[] = &$menu; + } else { + $map[$pid]['children'][] = &$menu; + } + } + unset($menu); + + return $tree; + } + + /** + * @return array + */ + private function getAdminMenuBySuperAdmin(): array + { + return $this->adminMenuRepository->list([ + 'status' => AdminMenuStatusCode::Normal, + 'children' => true, + 'parent_id' => 0, + ])?->toArray() ?? []; + } + + /** + * @return array + */ + public function getRoleByAdminUser(): array + { + return $this->adminReturn->success( + 'success', + $this->getAdminUserInfo($this->adminId)->isSuperAdmin() ? + $this->adminRoleRepository->list(['status' => AdminRoleStatusCode::Normal])->toArray() : + $this->getAdminUserInfo($this->adminId)->getRoles(['name', 'code', 'remark'])->toArray() + ); + } + + /** + * @return array + */ + public function update(): array + { + $userInfo = $this->getAdminUserInfo($this->adminId); + if (!$userInfo) throw new ErrException('用户不存在'); + + $data = $this->getRequestData(); + + if (Arr::exists($data, 'new_password')) { + if (!$userInfo->verifyPassword(Arr::get($data,'old_password'))) + throw new ErrException('旧密码错误',ResultCode::OLD_PASSWORD_ERROR); + + $data['password'] = $data['new_password']; + } + + if (!$this->adminUserRepository->updateById($userInfo->id, $data)) throw new ErrException('更新失败'); + + return $this->adminReturn->success(); + } +} \ No newline at end of file diff --git a/app/Service/Admin/Log/AdminUserLoginLogService.php b/app/Service/Admin/Log/AdminUserLoginLogService.php new file mode 100644 index 0000000..0e1c515 --- /dev/null +++ b/app/Service/Admin/Log/AdminUserLoginLogService.php @@ -0,0 +1,51 @@ +adminReturn->success( + 'success', + $this->adminUserLoginLogRepository->page( + $this->getRequestData(), + $this->getCurrentPage(), + $this->getPageSize() + ) + ); + } + + /** + * @return array + */ + public function deleteLog(): array + { + if (!$this->adminUserLoginLogRepository->deleteById($this->request->input('ids'))) + throw new ErrException('删除失败'); + + return $this->adminReturn->success(); + } +} \ No newline at end of file diff --git a/app/Service/Admin/Log/AdminUserOperationLogService.php b/app/Service/Admin/Log/AdminUserOperationLogService.php new file mode 100644 index 0000000..6fc33b6 --- /dev/null +++ b/app/Service/Admin/Log/AdminUserOperationLogService.php @@ -0,0 +1,51 @@ +adminReturn->success( + 'success', + $this->adminUserOperationLogRepository->page( + $this->getRequestData(), + $this->getCurrentPage(), + $this->getPageSize() + ) + ); + } + + /** + * @return array + */ + public function deleteLog(): array + { + if (!$this->adminUserOperationLogRepository->deleteById($this->request->input('ids'))) + throw new ErrException('删除失败'); + + return $this->adminReturn->success(); + } +} \ No newline at end of file diff --git a/app/Service/Admin/Login/LoginService.php b/app/Service/Admin/Login/LoginService.php index d0a50f6..8f0601c 100644 --- a/app/Service/Admin/Login/LoginService.php +++ b/app/Service/Admin/Login/LoginService.php @@ -12,8 +12,6 @@ namespace App\Service\Admin\Login; use App\Constants\Model\AdminUser\AdminUserStatusCode; use App\Exception\ErrException; -use App\Interface\JwtInterface; -use App\Lib\Jwt\JwtFactory; use App\Repository\AdminUserRepository; use App\Service\Admin\BaseAdminService; use App\Service\BaseTokenService; diff --git a/app/Trait/ClientOsTrait.php b/app/Trait/ClientOsTrait.php new file mode 100644 index 0000000..d206218 --- /dev/null +++ b/app/Trait/ClientOsTrait.php @@ -0,0 +1,34 @@ +request->header('user-agent'); + + if (empty($userAgent)) return 'Unknown'; + + return match (true) { + preg_match('/win/i', $userAgent) => 'Windows', + preg_match('/mac/i', $userAgent) => 'MAC', + preg_match('/linux/i', $userAgent) => 'Linux', + preg_match('/unix/i', $userAgent) => 'Unix', + preg_match('/bsd/i', $userAgent) => 'BSD', + default => 'Other', + }; + } +} \ No newline at end of file