Compare commits

...

2 Commits

Author SHA1 Message Date
c1d8f02491 fix : update menu 2025-09-16 14:30:37 +08:00
2613b031ae feat : admin log 2025-09-16 14:30:12 +08:00
18 changed files with 663 additions and 11 deletions

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace App\Aspect;
use App\Lib\Log\Logger;
use App\Model\AdminUserLoginLog;
use App\Repository\AdminUserRepository;
use App\Service\Admin\Login\LoginService;
use App\Trait\ClientIpTrait;
use App\Trait\ClientOsTrait;
use Hyperf\Coroutine\Coroutine;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Exception\Exception;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Throwable;
#[Aspect]
class AdminLoginLogAspect extends AbstractAspect
{
use ClientIpTrait;
use ClientOsTrait;
/**
* 切入类
* @var array|\class-string[]
*/
public array $classes = [
LoginService::class,
];
/**
* @var bool
*/
private bool $loginSuccess = false;
/**
* @var string
*/
private string $loginMsg = '';
/**
* @param Logger $logger
* @param RequestInterface $request
* @param AdminUserRepository $adminUserRepository
* @param AdminUserLoginLog $adminUserLoginLogModel
*/
public function __construct(
protected readonly Logger $logger,
protected readonly RequestInterface $request,
protected readonly AdminUserRepository $adminUserRepository,
protected readonly AdminUserLoginLog $adminUserLoginLogModel,
) {}
/**
* @param ProceedingJoinPoint $proceedingJoinPoint
* @return mixed
* @throws Exception
* @throws Throwable
*/
public function process(ProceedingJoinPoint $proceedingJoinPoint)
{
// 写日志
try {
$res = $proceedingJoinPoint->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,
]);
});
}
}

View File

@@ -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'],
]);
});
}
}

View File

@@ -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;
}

View File

@@ -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
@@ -55,7 +56,7 @@ class AdminMenuController
#[Permission(code: 'permission:menu:save')]
public function updateMenu(int $id): array
{
return $this->service->update();
return $this->service->update($id);
}
/**

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin\Log;
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\Log\AdminUserLoginLogService;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Annotation\RequestMapping;
#[Controller(prefix: "admin/user-login-log")]
#[ResponseFormat('admin')]
#[Middleware(middleware: AdminTokenMiddleware::class, priority: 100)]
#[Middleware(middleware: PermissionMiddleware::class, priority: 99)]
class AdminUserLoginLogController extends AbstractController
{
/**
* @var AdminUserLoginLogService
*/
#[Inject]
protected AdminUserLoginLogService $service;
/**
* @return array
*/
#[RequestMapping(path: "list", methods: "GET")]
#[Permission(code: 'log:userLogin:list')]
public function pageList(): array
{
return $this->service->handle();
}
/**
* @return array
*/
#[RequestMapping(path: "", methods: "DELETE")]
#[Permission(code: 'log:userLogin:delete')]
public function delete(): array
{
return $this->service->deleteLog();
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin\Log;
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\Log\AdminUserOperationLogService;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Annotation\RequestMapping;
#[Controller(prefix: "admin/user-operation-log")]
#[ResponseFormat('admin')]
#[Middleware(middleware: AdminTokenMiddleware::class, priority: 100)]
#[Middleware(middleware: PermissionMiddleware::class, priority: 99)]
class AdminUserOperationLogController extends AbstractController
{
/**
* @var AdminUserOperationLogService
*/
#[Inject]
protected AdminUserOperationLogService $service;
/**
* @return array
*/
#[RequestMapping(path: "list", methods: "GET")]
#[Permission(code: 'log:userOperation:list')]
public function pageList(): array
{
return $this->service->handle();
}
/**
* @return array
*/
#[RequestMapping(path: "", methods: "DELETE")]
#[Permission(code: 'log:userOperation:delete')]
public function delete(): array
{
return $this->service->deleteLog();
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Annotation\Permission;
use App\Annotation\ResponseFormat;
use App\Controller\AbstractController;
use App\Middleware\Admin\AdminTokenMiddleware;
use App\Service\Admin\AdminUser\PermissionService;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Annotation\RequestMapping;
#[Controller(prefix: "admin/permission")]
#[ResponseFormat('admin')]
#[Middleware(middleware: AdminTokenMiddleware::class, priority: 100)]
class PermissionController extends AbstractController
{
/**
* @var PermissionService
*/
#[Inject]
protected PermissionService $service;
/**
* @return array
*/
#[RequestMapping(path: "menus", methods: "GET")]
public function menus(): array
{
return $this->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();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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.

View File

@@ -0,0 +1,57 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Repository;
use App\Model\AdminUserLoginLog;
use Hyperf\Collection\Arr;
use Hyperf\Database\Model\Builder;
final class AdminUserLoginLogRepository extends BaseRepository
{
/**
* @param AdminUserLoginLog $model
*/
public function __construct(protected readonly AdminUserLoginLog $model) {}
/**
* @param Builder $query
* @param array $params
* @return Builder
*/
public function handleSearch(Builder $query, array $params): Builder
{
return $query
->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);
});
}
}

View File

@@ -0,0 +1,18 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Repository;
use App\Model\AdminUserOperationLog;
final class AdminUserOperationLogRepository extends BaseRepository
{
public function __construct(protected readonly AdminUserOperationLog $model) {}
}

View File

@@ -0,0 +1,132 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Admin\AdminUser;
use App\Constants\Model\AdminUser\AdminMenuStatusCode;
use App\Constants\Model\AdminUser\AdminRoleStatusCode;
use App\Constants\ResultCode;
use App\Exception\ErrException;
use App\Repository\AdminMenuRepository;
use App\Repository\AdminRoleRepository;
use App\Service\Admin\BaseAdminService;
use App\Trait\AdminUserTrait;
use Hyperf\Collection\Arr;
class PermissionService extends BaseAdminService
{
use AdminUserTrait;
/**
* @var AdminRoleRepository
*/
protected AdminRoleRepository $adminRoleRepository;
/**
* @var AdminMenuRepository
*/
protected AdminMenuRepository $adminMenuRepository;
/**
* @return array
*/
public function handle(): array
{
return $this->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();
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Admin\Log;
use App\Exception\ErrException;
use App\Repository\AdminUserLoginLogRepository;
use App\Service\Admin\BaseAdminService;
use Hyperf\Di\Annotation\Inject;
class AdminUserLoginLogService extends BaseAdminService
{
/**
* @var AdminUserLoginLogRepository
*/
#[Inject]
protected AdminUserLoginLogRepository $adminUserLoginLogRepository;
/**
* @return array
*/
public function handle(): array
{
return $this->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();
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Admin\Log;
use App\Exception\ErrException;
use App\Repository\AdminUserOperationLogRepository;
use App\Service\Admin\BaseAdminService;
use Hyperf\Di\Annotation\Inject;
class AdminUserOperationLogService extends BaseAdminService
{
/**
* @var AdminUserOperationLogRepository
*/
#[Inject]
protected AdminUserOperationLogRepository $adminUserOperationLogRepository;
/**
* @return array
*/
public function handle(): array
{
return $this->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();
}
}

View File

@@ -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;

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Trait;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
trait ClientOsTrait
{
/**
* @var RequestInterface
*/
#[Inject]
protected readonly RequestInterface $request;
/**
* @return string
*/
public function getClientOs(): string
{
$userAgent = $this->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',
};
}
}