初始化项目

This commit is contained in:
2026-02-08 22:38:13 +08:00
commit 334d2c6312
201 changed files with 32724 additions and 0 deletions
+248
View File
@@ -0,0 +1,248 @@
<?php
namespace App\Services\Auth;
use App\Models\Auth\User;
use App\Services\Auth\PermissionService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthService
{
protected $permissionService;
public function __construct(PermissionService $permissionService)
{
$this->permissionService = $permissionService;
}
/**
* 管理员登录
*/
public function login(array $credentials): array
{
$user = User::where('username', $credentials['username'])->first();
if (!$user || !Hash::check($credentials['password'], $user->password)) {
throw ValidationException::withMessages([
'username' => ['用户名或密码错误'],
]);
}
if ($user->status !== 1) {
throw ValidationException::withMessages([
'username' => ['账号已被禁用'],
]);
}
// 更新登录信息
$user->update([
'last_login_at' => now(),
'last_login_ip' => request()->ip(),
]);
// 生成token
$token = Auth::guard('admin')->login($user);
// 生成refresh token
$refreshToken = Auth::guard('admin')->refresh();
// 获取用户菜单
$menu = $this->getUserMenu($user);
// 获取用户权限列表
$permissions = $this->getUserPermissions($user);
return [
'token' => $token,
'refreshToken' => $refreshToken,
'user' => $this->getUserInfo($user),
'menu' => $menu,
'permissions' => $permissions,
];
}
/**
* 管理员登出
*/
public function logout(): void
{
Auth::guard('admin')->logout();
}
/**
* 刷新token
*/
public function refresh(): array
{
$newToken = Auth::guard('admin')->refresh();
$user = Auth::guard('admin')->user();
// 生成新的refresh token
$newRefreshToken = Auth::guard('admin')->refresh();
// 获取用户菜单
$menu = $this->getUserMenu($user);
// 获取用户权限列表
$permissions = $this->getUserPermissions($user);
return [
'token' => $newToken,
'refreshToken' => $newRefreshToken,
'user' => $this->getUserInfo($user),
'menu' => $menu,
'permissions' => $permissions,
];
}
/**
* 获取当前用户信息
*/
public function me(): array
{
$user = Auth::guard('admin')->user();
return $this->getUserInfo($user);
}
/**
* 找回密码
*/
public function resetPassword(array $data): void
{
$user = User::where('username', $data['username'])
->orWhere('email', $data['username'])
->orWhere('phone', $data['username'])
->first();
if (!$user) {
throw ValidationException::withMessages([
'username' => ['用户不存在'],
]);
}
$user->update([
'password' => Hash::make($data['password']),
]);
}
/**
* 修改密码
*/
public function changePassword(array $data): void
{
$user = Auth::guard('admin')->user();
if (!Hash::check($data['old_password'], $user->password)) {
throw ValidationException::withMessages([
'old_password' => ['原密码错误'],
]);
}
$user->update([
'password' => Hash::make($data['password']),
]);
}
/**
* 获取用户信息详情
*/
private function getUserInfo(User $user): array
{
$user->load(['department', 'roles.permissions']);
return [
'id' => $user->id,
'username' => $user->username,
'real_name' => $user->real_name,
'email' => $user->email,
'phone' => $user->phone,
'avatar' => $user->avatar,
'department' => $user->department ? [
'id' => $user->department->id,
'name' => $user->department->name,
] : null,
'roles' => $user->roles->pluck('name')->toArray(),
'permissions' => $this->getUserPermissions($user),
'status' => $user->status,
'last_login_at' => $user->last_login_at ? $user->last_login_at->toDateTimeString() : null,
];
}
/**
* 获取用户菜单
*/
private function getUserMenu(User $user): array
{
// 获取用户的所有权限
$permissionIds = [];
foreach ($user->roles as $role) {
foreach ($role->permissions as $permission) {
$permissionIds[] = $permission->id;
}
}
// 查询菜单类型的权限
$menuPermissions = \App\Models\Auth\Permission::whereIn('id', $permissionIds)
->where('type', 'menu')
->where('status', 1)
->orderBy('sort', 'asc')
->get();
// 构建菜单树
return $this->buildMenuTree($menuPermissions);
}
/**
* 构建菜单树
*/
private function buildMenuTree($permissions, $parentId = 0): array
{
$tree = [];
foreach ($permissions as $permission) {
if ($permission->parent_id == $parentId) {
$node = [
'path' => $permission->route,
'name' => $permission->code,
'meta' => $permission->meta ? json_decode($permission->meta, true) : [],
];
// 添加组件路径
if ($permission->component) {
$node['component'] = $permission->component;
}
// 添加重定向
if (!empty($node['meta']['redirect'])) {
$node['redirect'] = $node['meta']['redirect'];
}
// 递归构建子菜单
$children = $this->buildMenuTree($permissions, $permission->id);
if (!empty($children)) {
$node['children'] = $children;
}
$tree[] = $node;
}
}
return $tree;
}
/**
* 获取用户权限列表
*/
private function getUserPermissions(User $user): array
{
$permissions = [];
foreach ($user->roles as $role) {
foreach ($role->permissions as $permission) {
if (!in_array($permission->code, $permissions)) {
$permissions[] = $permission->code;
}
}
}
return $permissions;
}
}
+319
View File
@@ -0,0 +1,319 @@
<?php
namespace App\Services\Auth;
use App\Models\Auth\Department;
use Illuminate\Validation\ValidationException;
class DepartmentService
{
/**
* 获取部门列表
*/
public function getList(array $params): array
{
$query = Department::query();
// 搜索条件
if (!empty($params['keyword'])) {
$query->where(function ($q) use ($params) {
$q->where('name', 'like', '%' . $params['keyword'] . '%')
->orWhere('leader', 'like', '%' . $params['keyword'] . '%')
->orWhere('phone', 'like', '%' . $params['keyword'] . '%');
});
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
if (isset($params['parent_id']) && $params['parent_id'] !== '') {
$query->where('parent_id', $params['parent_id']);
}
// 排序
$orderBy = $params['order_by'] ?? 'sort';
$orderDirection = $params['order_direction'] ?? 'asc';
$query->orderBy($orderBy, $orderDirection);
// 分页
$page = $params['page'] ?? 1;
$pageSize = $params['page_size'] ?? 20;
$list = $query->paginate($pageSize, ['*'], 'page', $page);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $page,
'page_size' => $pageSize,
];
}
/**
* 获取部门树
*/
public function getTree(array $params = []): array
{
$query = Department::query();
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
$departments = $query->orderBy('sort', 'asc')->get();
return $this->buildTree($departments);
}
/**
* 获取所有部门(不分页)
*/
public function getAll(): array
{
$departments = Department::where('status', 1)->orderBy('sort', 'asc')->get();
return $departments->map(function ($department) {
return [
'id' => $department->id,
'name' => $department->name,
'parent_id' => $department->parent_id,
];
})->toArray();
}
/**
* 获取部门详情
*/
public function getById(int $id): array
{
$department = Department::with(['parent', 'children'])->find($id);
if (!$department) {
throw ValidationException::withMessages([
'id' => ['部门不存在'],
]);
}
return [
'id' => $department->id,
'name' => $department->name,
'parent_id' => $department->parent_id,
'parent' => $department->parent ? [
'id' => $department->parent->id,
'name' => $department->parent->name,
] : null,
'leader' => $department->leader,
'phone' => $department->phone,
'sort' => $department->sort,
'status' => $department->status,
'children_count' => $department->children()->count(),
'users_count' => $department->users()->count(),
'created_at' => $department->created_at->toDateTimeString(),
'updated_at' => $department->updated_at->toDateTimeString(),
];
}
/**
* 创建部门
*/
public function create(array $data): Department
{
// 检查部门名称是否已存在
$query = Department::where('name', $data['name']);
if (!empty($data['parent_id'])) {
$query->where('parent_id', $data['parent_id']);
} else {
$query->where('parent_id', 0);
}
if ($query->exists()) {
throw ValidationException::withMessages([
'name' => ['同级部门名称已存在'],
]);
}
// 如果有父级ID,检查父级是否存在
if (!empty($data['parent_id'])) {
$parent = Department::find($data['parent_id']);
if (!$parent) {
throw ValidationException::withMessages([
'parent_id' => ['父级部门不存在'],
]);
}
}
return Department::create([
'name' => $data['name'],
'parent_id' => $data['parent_id'] ?? 0,
'leader' => $data['leader'] ?? null,
'phone' => $data['phone'] ?? null,
'sort' => $data['sort'] ?? 0,
'status' => $data['status'] ?? 1,
]);
}
/**
* 更新部门
*/
public function update(int $id, array $data): Department
{
$department = Department::find($id);
if (!$department) {
throw ValidationException::withMessages([
'id' => ['部门不存在'],
]);
}
// 检查部门名称是否已被其他部门使用
if (isset($data['name']) && $data['name'] !== $department->name) {
$query = Department::where('name', $data['name'])
->where('id', '!=', $id);
$parentId = isset($data['parent_id']) ? $data['parent_id'] : $department->parent_id;
if ($parentId) {
$query->where('parent_id', $parentId);
} else {
$query->where('parent_id', 0);
}
if ($query->exists()) {
throw ValidationException::withMessages([
'name' => ['同级部门名称已存在'],
]);
}
}
// 如果有父级ID,检查父级是否存在
if (isset($data['parent_id']) && !empty($data['parent_id'])) {
$parent = Department::find($data['parent_id']);
if (!$parent) {
throw ValidationException::withMessages([
'parent_id' => ['父级部门不存在'],
]);
}
// 不能将部门设置为自己的子级
if ($data['parent_id'] == $id) {
throw ValidationException::withMessages([
'parent_id' => ['不能将部门设置为自己的子级'],
]);
}
// 不能将部门设置为自己的子孙级
if ($this->isDescendant($id, $data['parent_id'])) {
throw ValidationException::withMessages([
'parent_id' => ['不能将部门设置为自己的子孙级'],
]);
}
}
$updateData = [
'name' => $data['name'] ?? $department->name,
'parent_id' => $data['parent_id'] ?? $department->parent_id,
'leader' => $data['leader'] ?? $department->leader,
'phone' => $data['phone'] ?? $department->phone,
'sort' => $data['sort'] ?? $department->sort,
'status' => $data['status'] ?? $department->status,
];
$department->update($updateData);
return $department;
}
/**
* 删除部门
*/
public function delete(int $id): void
{
$department = Department::find($id);
if (!$department) {
throw ValidationException::withMessages([
'id' => ['部门不存在'],
]);
}
// 检查是否有子部门
if ($department->children()->exists()) {
throw ValidationException::withMessages([
'id' => ['该部门下还有子部门,无法删除'],
]);
}
// 检查部门下是否有用户
if ($department->users()->exists()) {
throw ValidationException::withMessages([
'id' => ['该部门下还有用户,无法删除'],
]);
}
$department->delete();
}
/**
* 批量删除部门
*/
public function batchDelete(array $ids): int
{
// 检查是否有子部门
$hasChildren = Department::whereIn('id', $ids)->whereHas('children')->exists();
if ($hasChildren) {
throw ValidationException::withMessages([
'ids' => ['选中的部门中还有子部门,无法删除'],
]);
}
// 检查部门下是否有用户
$hasUsers = Department::whereIn('id', $ids)->whereHas('users')->exists();
if ($hasUsers) {
throw ValidationException::withMessages([
'ids' => ['选中的部门中还有用户,无法删除'],
]);
}
return Department::whereIn('id', $ids)->delete();
}
/**
* 批量更新部门状态
*/
public function batchUpdateStatus(array $ids, int $status): int
{
return Department::whereIn('id', $ids)->update(['status' => $status]);
}
/**
* 构建部门树
*/
private function buildTree($departments, $parentId = 0): array
{
$tree = [];
foreach ($departments as $department) {
if ($department->parent_id == $parentId) {
$node = [
'id' => $department->id,
'name' => $department->name,
'parent_id' => $department->parent_id,
'leader' => $department->leader,
'phone' => $department->phone,
'sort' => $department->sort,
'status' => $department->status,
'children' => $this->buildTree($departments, $department->id),
];
$tree[] = $node;
}
}
return $tree;
}
/**
* 检查是否为子孙部门
*/
private function isDescendant($id, $childId): bool
{
if ($id == $childId) {
return true;
}
$child = Department::find($childId);
if (!$child || $child->parent_id == 0) {
return false;
}
return $this->isDescendant($id, $child->parent_id);
}
}
+201
View File
@@ -0,0 +1,201 @@
<?php
namespace App\Services\Auth;
use App\Models\Auth\User;
use App\Models\Auth\Department;
use App\Models\Auth\Role;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use Maatwebsite\Excel\Facades\Excel;
use App\Exports\UserExport;
use App\Exports\DepartmentExport;
use App\Imports\UserImport;
use App\Imports\DepartmentImport;
class ImportExportService
{
/**
* 下载用户导入模板
*/
public function downloadUserTemplate(): string
{
$filename = 'user_import_template_' . date('YmdHis') . '.xlsx';
$path = storage_path('app/exports/' . $filename);
// 确保目录存在
if (!is_dir(dirname($path))) {
mkdir(dirname($path), 0755, true);
}
// 使用模板数据创建Excel
$templateData = [
[
'用户名*',
'密码*',
'真实姓名*',
'邮箱',
'手机号',
'部门名称',
'角色名称(多个用逗号分隔)',
'备注',
],
[
'test001',
'123456',
'测试用户001',
'test001@example.com',
'13800138001',
'技术部',
'管理员',
'示例数据',
],
];
Excel::store(new \App\Exports\GenericExport($templateData), 'exports/' . $filename);
return $filename;
}
/**
* 下载部门导入模板
*/
public function downloadDepartmentTemplate(): string
{
$filename = 'department_import_template_' . date('YmdHis') . '.xlsx';
// 确保目录存在
if (!is_dir(storage_path('app/exports'))) {
mkdir(storage_path('app/exports'), 0755, true);
}
$templateData = [
[
'部门名称*',
'上级部门名称',
'负责人',
'联系电话',
'排序',
'备注',
],
[
'前端开发组',
'技术部',
'张三',
'13800138001',
'1',
'示例数据',
],
];
Excel::store(new \App\Exports\GenericExport($templateData), 'exports/' . $filename);
return $filename;
}
/**
* 导出用户数据
*/
public function exportUsers(array $userIds = []): string
{
$filename = 'users_export_' . date('YmdHis') . '.xlsx';
// 确保目录存在
if (!is_dir(storage_path('app/exports'))) {
mkdir(storage_path('app/exports'), 0755, true);
}
Excel::store(new UserExport($userIds), 'exports/' . $filename);
return $filename;
}
/**
* 导出部门数据
*/
public function exportDepartments(array $departmentIds = []): string
{
$filename = 'departments_export_' . date('YmdHis') . '.xlsx';
// 确保目录存在
if (!is_dir(storage_path('app/exports'))) {
mkdir(storage_path('app/exports'), 0755, true);
}
Excel::store(new DepartmentExport($departmentIds), 'exports/' . $filename);
return $filename;
}
/**
* 导入用户数据
*/
public function importUsers(string $filePath, string $realPath): array
{
if (!file_exists($realPath)) {
throw ValidationException::withMessages([
'file' => ['文件不存在'],
]);
}
$import = new UserImport();
Excel::import($import, $realPath);
// 删除临时文件
if (file_exists($realPath)) {
unlink($realPath);
}
return [
'success_count' => $import->getSuccessCount(),
'error_count' => $import->getErrorCount(),
'errors' => $import->getErrors(),
];
}
/**
* 导入部门数据
*/
public function importDepartments(string $filePath, string $realPath): array
{
if (!file_exists($realPath)) {
throw ValidationException::withMessages([
'file' => ['文件不存在'],
]);
}
$import = new DepartmentImport();
Excel::import($import, $realPath);
// 删除临时文件
if (file_exists($realPath)) {
unlink($realPath);
}
return [
'success_count' => $import->getSuccessCount(),
'error_count' => $import->getErrorCount(),
'errors' => $import->getErrors(),
];
}
/**
* 获取导出文件路径
*/
public function getExportFilePath(string $filename): string
{
return storage_path('app/exports/' . $filename);
}
/**
* 删除导出文件
*/
public function deleteExportFile(string $filename): bool
{
$path = $this->getExportFilePath($filename);
if (file_exists($path)) {
return unlink($path);
}
return true;
}
}
@@ -0,0 +1,256 @@
<?php
namespace App\Services\Auth;
use App\Models\Auth\User;
use App\Models\Auth\Role;
use App\Models\Auth\Permission;
use Illuminate\Support\Facades\Cache;
class PermissionCacheService
{
protected $cachePrefix = 'permission:';
protected $cacheMinutes = 60; // 缓存60分钟
/**
* 获取用户的权限列表(带缓存)
*/
public function getUserPermissions(int $userId): array
{
$cacheKey = $this->getUserPermissionsCacheKey($userId);
return Cache::remember($cacheKey, now()->addMinutes($this->cacheMinutes), function() use ($userId) {
$user = User::find($userId);
if (!$user) {
return [];
}
$permissions = [];
foreach ($user->roles as $role) {
foreach ($role->permissions as $permission) {
$permissions[$permission->id] = [
'id' => $permission->id,
'name' => $permission->name,
'code' => $permission->code,
'type' => $permission->type,
'route' => $permission->route,
];
}
}
return array_values($permissions);
});
}
/**
* 获取用户的权限编码列表(带缓存)
*/
public function getUserPermissionCodes(int $userId): array
{
$cacheKey = $this->getUserPermissionCodesCacheKey($userId);
return Cache::remember($cacheKey, now()->addMinutes($this->cacheMinutes), function() use ($userId) {
$permissions = $this->getUserPermissions($userId);
return array_column($permissions, 'code');
});
}
/**
* 获取用户的菜单树(带缓存)
*/
public function getUserMenuTree(int $userId): array
{
$cacheKey = $this->getUserMenuTreeCacheKey($userId);
return Cache::remember($cacheKey, now()->addMinutes($this->cacheMinutes), function() use ($userId) {
$user = User::find($userId);
if (!$user) {
return [];
}
// 获取用户的所有权限ID
$permissionIds = [];
foreach ($user->roles as $role) {
foreach ($role->permissions as $permission) {
$permissionIds[] = $permission->id;
}
}
// 获取菜单类型的权限
$permissions = Permission::whereIn('id', $permissionIds)
->whereIn('type', ['menu', 'api'])
->where('status', 1)
->orderBy('sort', 'asc')
->get();
return $this->buildMenuTree($permissions->toArray());
});
}
/**
* 检查用户是否有某个权限(带缓存)
*/
public function userHasPermission(int $userId, string $permissionCode): bool
{
$codes = $this->getUserPermissionCodes($userId);
return in_array($permissionCode, $codes);
}
/**
* 获取角色的权限列表(带缓存)
*/
public function getRolePermissions(int $roleId): array
{
$cacheKey = $this->getRolePermissionsCacheKey($roleId);
return Cache::remember($cacheKey, now()->addMinutes($this->cacheMinutes), function() use ($roleId) {
$role = Role::find($roleId);
if (!$role) {
return [];
}
return $role->permissions->map(function($permission) {
return [
'id' => $permission->id,
'name' => $permission->name,
'code' => $permission->code,
'type' => $permission->type,
];
})->toArray();
});
}
/**
* 清除用户权限缓存
*/
public function clearUserPermissionCache(int $userId): void
{
Cache::forget($this->getUserPermissionsCacheKey($userId));
Cache::forget($this->getUserPermissionCodesCacheKey($userId));
Cache::forget($this->getUserMenuTreeCacheKey($userId));
}
/**
* 清除角色权限缓存
*/
public function clearRolePermissionCache(int $roleId): void
{
Cache::forget($this->getRolePermissionsCacheKey($roleId));
// 清除所有拥有该角色的用户权限缓存
$role = Role::find($roleId);
if ($role) {
foreach ($role->users as $user) {
$this->clearUserPermissionCache($user->id);
}
}
}
/**
* 清除所有权限缓存
*/
public function clearAllPermissionCache(): void
{
if (Cache::getStore() instanceof \Illuminate\Cache\RedisStore) {
$redis = Cache::getStore()->connection();
$keys = $redis->keys($this->cachePrefix . '*');
if (!empty($keys)) {
$redis->del($keys);
}
}
}
/**
* 清除指定用户的权限缓存(当用户角色变化时调用)
*/
public function onUserRolesChanged(int $userId): void
{
$this->clearUserPermissionCache($userId);
}
/**
* 清除角色的用户缓存(当角色权限变化时调用)
*/
public function onRolePermissionsChanged(int $roleId): void
{
$this->clearRolePermissionCache($roleId);
}
/**
* 清除权限的所有缓存(当权限本身变化时调用)
*/
public function onPermissionChanged(int $permissionId): void
{
// 清除所有缓存,因为权限变化可能影响所有用户
$this->clearAllPermissionCache();
}
/**
* 构建菜单树
*/
protected function buildMenuTree(array $permissions, int $parentId = 0): array
{
$tree = [];
foreach ($permissions as $permission) {
if ($permission['parent_id'] == $parentId) {
$node = [
'id' => $permission['id'],
'name' => $permission['name'],
'code' => $permission['code'],
'type' => $permission['type'],
'route' => $permission['route'],
'component' => $permission['component'],
'meta' => json_decode($permission['meta'] ?? '{}', true),
'sort' => $permission['sort'],
'children' => $this->buildMenuTree($permissions, $permission['id']),
];
// 如果没有子节点,移除children字段
if (empty($node['children'])) {
unset($node['children']);
}
$tree[] = $node;
}
}
// 按sort排序
usort($tree, function($a, $b) {
return $a['sort'] <=> $b['sort'];
});
return $tree;
}
/**
* 生成用户权限缓存键
*/
protected function getUserPermissionsCacheKey(int $userId): string
{
return $this->cachePrefix . 'user:' . $userId . ':permissions';
}
/**
* 生成用户权限编码缓存键
*/
protected function getUserPermissionCodesCacheKey(int $userId): string
{
return $this->cachePrefix . 'user:' . $userId . ':permission_codes';
}
/**
* 生成用户菜单树缓存键
*/
protected function getUserMenuTreeCacheKey(int $userId): string
{
return $this->cachePrefix . 'user:' . $userId . ':menu_tree';
}
/**
* 生成角色权限缓存键
*/
protected function getRolePermissionsCacheKey(int $roleId): string
{
return $this->cachePrefix . 'role:' . $roleId . ':permissions';
}
}
+323
View File
@@ -0,0 +1,323 @@
<?php
namespace App\Services\Auth;
use App\Models\Auth\Permission;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class PermissionService
{
/**
* 获取权限列表
*/
public function getList(array $params): array
{
$query = Permission::query();
// 搜索条件
if (!empty($params['keyword'])) {
$query->where(function ($q) use ($params) {
$q->where('name', 'like', '%' . $params['keyword'] . '%')
->orWhere('code', 'like', '%' . $params['keyword'] . '%');
});
}
if (!empty($params['type'])) {
$query->where('type', $params['type']);
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
// 排序
$orderBy = $params['order_by'] ?? 'sort';
$orderDirection = $params['order_direction'] ?? 'asc';
$query->orderBy($orderBy, $orderDirection);
// 分页
$page = $params['page'] ?? 1;
$pageSize = $params['page_size'] ?? 20;
$list = $query->paginate($pageSize, ['*'], 'page', $page);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $page,
'page_size' => $pageSize,
];
}
/**
* 获取权限树
*/
public function getTree(array $params = []): array
{
$query = Permission::query();
if (!empty($params['type'])) {
$query->where('type', $params['type']);
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
$permissions = $query->orderBy('sort', 'asc')->get();
return $this->buildTree($permissions);
}
/**
* 获取菜单树(前端使用)
*/
public function getMenuTree(int $userId = null): array
{
$query = Permission::whereIn('type', ['menu', 'api'])
->where('status', 1);
if ($userId) {
// 获取用户的权限
$user = \App\Models\Auth\User::find($userId);
if ($user) {
$permissionIds = [];
foreach ($user->roles as $role) {
foreach ($role->permissions as $permission) {
$permissionIds[] = $permission->id;
}
}
$query->whereIn('id', $permissionIds);
}
}
$permissions = $query->orderBy('sort', 'asc')->get();
return $this->buildTree($permissions);
}
/**
* 获取权限详情
*/
public function getById(int $id): array
{
$permission = Permission::with(['parent'])->find($id);
if (!$permission) {
throw ValidationException::withMessages([
'id' => ['权限不存在'],
]);
}
return [
'id' => $permission->id,
'name' => $permission->name,
'code' => $permission->code,
'type' => $permission->type,
'parent_id' => $permission->parent_id,
'parent' => $permission->parent ? [
'id' => $permission->parent->id,
'name' => $permission->parent->name,
] : null,
'route' => $permission->route,
'component' => $permission->component,
'meta' => $permission->meta,
'sort' => $permission->sort,
'status' => $permission->status,
'created_at' => $permission->created_at->toDateTimeString(),
'updated_at' => $permission->updated_at->toDateTimeString(),
];
}
/**
* 创建权限
*/
public function create(array $data): Permission
{
// 检查权限名称是否已存在
if (Permission::where('name', $data['name'])->exists()) {
throw ValidationException::withMessages([
'name' => ['权限名称已存在'],
]);
}
// 检查权限编码是否已存在
if (Permission::where('code', $data['code'])->exists()) {
throw ValidationException::withMessages([
'code' => ['权限编码已存在'],
]);
}
// 如果有父级ID,检查父级是否存在
if (!empty($data['parent_id'])) {
$parent = Permission::find($data['parent_id']);
if (!$parent) {
throw ValidationException::withMessages([
'parent_id' => ['父级权限不存在'],
]);
}
}
return Permission::create([
'name' => $data['name'],
'code' => $data['code'],
'type' => $data['type'] ?? 'api',
'parent_id' => $data['parent_id'] ?? 0,
'route' => $data['route'] ?? null,
'component' => $data['component'] ?? null,
'meta' => $data['meta'] ?? null,
'sort' => $data['sort'] ?? 0,
'status' => $data['status'] ?? 1,
]);
}
/**
* 更新权限
*/
public function update(int $id, array $data): Permission
{
$permission = Permission::find($id);
if (!$permission) {
throw ValidationException::withMessages([
'id' => ['权限不存在'],
]);
}
// 检查权限名称是否已被其他权限使用
if (isset($data['name']) && $data['name'] !== $permission->name) {
if (Permission::where('name', $data['name'])->exists()) {
throw ValidationException::withMessages([
'name' => ['权限名称已存在'],
]);
}
}
// 检查权限编码是否已被其他权限使用
if (isset($data['code']) && $data['code'] !== $permission->code) {
if (Permission::where('code', $data['code'])->exists()) {
throw ValidationException::withMessages([
'code' => ['权限编码已存在'],
]);
}
}
// 如果有父级ID,检查父级是否存在
if (isset($data['parent_id']) && !empty($data['parent_id'])) {
$parent = Permission::find($data['parent_id']);
if (!$parent) {
throw ValidationException::withMessages([
'parent_id' => ['父级权限不存在'],
]);
}
// 不能将权限设置为自己的子级
if ($data['parent_id'] == $id) {
throw ValidationException::withMessages([
'parent_id' => ['不能将权限设置为自己的子级'],
]);
}
}
$updateData = [
'name' => $data['name'] ?? $permission->name,
'code' => $data['code'] ?? $permission->code,
'type' => $data['type'] ?? $permission->type,
'parent_id' => $data['parent_id'] ?? $permission->parent_id,
'route' => $data['route'] ?? $permission->route,
'component' => $data['component'] ?? $permission->component,
'meta' => isset($data['meta']) ? $data['meta'] : $permission->meta,
'sort' => $data['sort'] ?? $permission->sort,
'status' => $data['status'] ?? $permission->status,
];
$permission->update($updateData);
return $permission;
}
/**
* 删除权限
*/
public function delete(int $id): void
{
$permission = Permission::find($id);
if (!$permission) {
throw ValidationException::withMessages([
'id' => ['权限不存在'],
]);
}
// 检查是否有子权限
if ($permission->children()->exists()) {
throw ValidationException::withMessages([
'id' => ['该权限下还有子权限,无法删除'],
]);
}
// 检查是否被角色使用
if ($permission->roles()->exists()) {
throw ValidationException::withMessages([
'id' => ['该权限已被角色使用,无法删除'],
]);
}
$permission->delete();
}
/**
* 批量删除权限
*/
public function batchDelete(array $ids): int
{
// 检查是否有子权限
$hasChildren = Permission::whereIn('id', $ids)->whereHas('children')->exists();
if ($hasChildren) {
throw ValidationException::withMessages([
'ids' => ['选中的权限中还有子权限,无法删除'],
]);
}
// 检查是否被角色使用
$hasRoles = Permission::whereIn('id', $ids)->whereHas('roles')->exists();
if ($hasRoles) {
throw ValidationException::withMessages([
'ids' => ['选中的权限中已被角色使用,无法删除'],
]);
}
return Permission::whereIn('id', $ids)->delete();
}
/**
* 批量更新权限状态
*/
public function batchUpdateStatus(array $ids, int $status): int
{
return Permission::whereIn('id', $ids)->update(['status' => $status]);
}
/**
* 构建权限树
*/
private function buildTree($permissions, $parentId = 0): array
{
$tree = [];
foreach ($permissions as $permission) {
if ($permission->parent_id == $parentId) {
$node = [
'id' => $permission->id,
'name' => $permission->name,
'code' => $permission->code,
'type' => $permission->type,
'route' => $permission->route,
'component' => $permission->component,
'meta' => $permission->meta,
'sort' => $permission->sort,
'status' => $permission->status,
'children' => $this->buildTree($permissions, $permission->id),
];
$tree[] = $node;
}
}
return $tree;
}
}
+430
View File
@@ -0,0 +1,430 @@
<?php
namespace App\Services\Auth;
use App\Models\Auth\Role;
use App\Models\Auth\Permission;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class RoleService
{
/**
* 获取角色列表
*/
public function getList(array $params): array
{
$query = Role::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']);
}
// 排序
$orderBy = $params['order_by'] ?? 'sort';
$orderDirection = $params['order_direction'] ?? 'asc';
$query->orderBy($orderBy, $orderDirection);
// 分页
$page = $params['page'] ?? 1;
$pageSize = $params['page_size'] ?? 20;
$list = $query->paginate($pageSize, ['*'], 'page', $page);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $page,
'page_size' => $pageSize,
];
}
/**
* 获取所有角色(不分页)
*/
public function getAll(): array
{
$roles = Role::where('status', 1)->orderBy('sort', 'asc')->get();
return $roles->map(function ($role) {
return [
'id' => $role->id,
'name' => $role->name,
'code' => $role->code,
];
})->toArray();
}
/**
* 获取角色详情
*/
public function getById(int $id): array
{
$role = Role::with(['permissions'])->find($id);
if (!$role) {
throw ValidationException::withMessages([
'id' => ['角色不存在'],
]);
}
return [
'id' => $role->id,
'name' => $role->name,
'code' => $role->code,
'description' => $role->description,
'sort' => $role->sort,
'status' => $role->status,
'permissions' => $role->permissions->pluck('id')->toArray(),
'created_at' => $role->created_at->toDateTimeString(),
'updated_at' => $role->updated_at->toDateTimeString(),
];
}
/**
* 创建角色
*/
public function create(array $data): Role
{
// 检查角色名称是否已存在
if (Role::where('name', $data['name'])->exists()) {
throw ValidationException::withMessages([
'name' => ['角色名称已存在'],
]);
}
// 检查角色编码是否已存在
if (Role::where('code', $data['code'])->exists()) {
throw ValidationException::withMessages([
'code' => ['角色编码已存在'],
]);
}
DB::beginTransaction();
try {
$role = Role::create([
'name' => $data['name'],
'code' => $data['code'],
'description' => $data['description'] ?? null,
'sort' => $data['sort'] ?? 0,
'status' => $data['status'] ?? 1,
]);
// 关联权限
if (!empty($data['permission_ids'])) {
$role->permissions()->attach($data['permission_ids']);
}
DB::commit();
return $role;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* 更新角色
*/
public function update(int $id, array $data): Role
{
$role = Role::find($id);
if (!$role) {
throw ValidationException::withMessages([
'id' => ['角色不存在'],
]);
}
// 检查角色名称是否已被其他角色使用
if (isset($data['name']) && $data['name'] !== $role->name) {
if (Role::where('name', $data['name'])->exists()) {
throw ValidationException::withMessages([
'name' => ['角色名称已存在'],
]);
}
}
// 检查角色编码是否已被其他角色使用
if (isset($data['code']) && $data['code'] !== $role->code) {
if (Role::where('code', $data['code'])->exists()) {
throw ValidationException::withMessages([
'code' => ['角色编码已存在'],
]);
}
}
DB::beginTransaction();
try {
$updateData = [
'name' => $data['name'] ?? $role->name,
'code' => $data['code'] ?? $role->code,
'description' => $data['description'] ?? $role->description,
'sort' => $data['sort'] ?? $role->sort,
'status' => $data['status'] ?? $role->status,
];
$role->update($updateData);
// 更新权限关联
if (isset($data['permission_ids'])) {
$role->permissions()->sync($data['permission_ids']);
}
DB::commit();
return $role;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* 删除角色
*/
public function delete(int $id): void
{
$role = Role::find($id);
if (!$role) {
throw ValidationException::withMessages([
'id' => ['角色不存在'],
]);
}
// 检查角色下是否有用户
if ($role->users()->exists()) {
throw ValidationException::withMessages([
'id' => ['该角色下还有用户,无法删除'],
]);
}
$role->delete();
}
/**
* 批量删除角色
*/
public function batchDelete(array $ids): int
{
// 检查角色下是否有用户
$hasUsers = Role::whereIn('id', $ids)->whereHas('users')->exists();
if ($hasUsers) {
throw ValidationException::withMessages([
'ids' => ['选中的角色中还有用户,无法删除'],
]);
}
return Role::whereIn('id', $ids)->delete();
}
/**
* 批量更新角色状态
*/
public function batchUpdateStatus(array $ids, int $status): int
{
return Role::whereIn('id', $ids)->update(['status' => $status]);
}
/**
* 分配权限
*/
public function assignPermissions(int $id, array $permissionIds): void
{
$role = Role::find($id);
if (!$role) {
throw ValidationException::withMessages([
'id' => ['角色不存在'],
]);
}
$role->permissions()->sync($permissionIds);
}
/**
* 获取角色的权限列表
*/
public function getPermissions(int $id): array
{
$role = Role::with(['permissions' => function ($query) {
$query->orderBy('sort', 'asc');
}])->find($id);
if (!$role) {
throw ValidationException::withMessages([
'id' => ['角色不存在'],
]);
}
return $this->buildPermissionTree($role->permissions);
}
/**
* 复制角色
*/
public function copy(int $id, array $data): Role
{
$sourceRole = Role::with(['permissions'])->find($id);
if (!$sourceRole) {
throw ValidationException::withMessages([
'id' => ['角色不存在'],
]);
}
// 检查新角色名称是否已存在
if (Role::where('name', $data['name'])->exists()) {
throw ValidationException::withMessages([
'name' => ['角色名称已存在'],
]);
}
// 检查新角色编码是否已存在
if (Role::where('code', $data['code'])->exists()) {
throw ValidationException::withMessages([
'code' => ['角色编码已存在'],
]);
}
DB::beginTransaction();
try {
// 创建新角色
$newRole = Role::create([
'name' => $data['name'],
'code' => $data['code'],
'description' => $data['description'] ?? $sourceRole->description,
'sort' => $data['sort'] ?? $sourceRole->sort,
'status' => $data['status'] ?? 1,
]);
// 复制权限
$permissionIds = $sourceRole->permissions->pluck('id')->toArray();
if (!empty($permissionIds)) {
$newRole->permissions()->attach($permissionIds);
}
DB::commit();
return $newRole;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* 批量复制角色
*/
public function batchCopy(array $roleIds, array $data): array
{
$successCount = 0;
$errorCount = 0;
$errors = [];
$newRoles = [];
DB::beginTransaction();
try {
foreach ($roleIds as $index => $roleId) {
try {
$sourceRole = Role::with(['permissions'])->find($roleId);
if (!$sourceRole) {
$errors[] = "角色ID {$roleId} 不存在";
$errorCount++;
continue;
}
// 生成新的名称和编码
$newName = $data['name'] ?? ($sourceRole->name . ' (副本)');
$newCode = $data['code'] ?? ($sourceRole->code . '_copy_' . time());
// 检查名称和编码是否已存在
$nameSuffix = '';
$codeSuffix = '';
$counter = 1;
while (Role::where('name', $newName . $nameSuffix)->exists()) {
$nameSuffix = ' (' . $counter . ')';
$counter++;
}
$counter = 1;
while (Role::where('code', $newCode . $codeSuffix)->exists()) {
$codeSuffix = '_' . $counter;
$counter++;
}
// 创建新角色
$newRole = Role::create([
'name' => $newName . $nameSuffix,
'code' => $newCode . $codeSuffix,
'description' => $data['description'] ?? $sourceRole->description,
'sort' => $data['sort'] ?? $sourceRole->sort,
'status' => $data['status'] ?? 1,
]);
// 复制权限
$permissionIds = $sourceRole->permissions->pluck('id')->toArray();
if (!empty($permissionIds)) {
$newRole->permissions()->attach($permissionIds);
}
$newRoles[] = [
'id' => $newRole->id,
'name' => $newRole->name,
'code' => $newRole->code,
];
$successCount++;
} catch (\Exception $e) {
$errors[] = "复制角色ID {$roleId} 失败:" . $e->getMessage();
$errorCount++;
}
}
DB::commit();
return [
'success_count' => $successCount,
'error_count' => $errorCount,
'errors' => $errors,
'new_roles' => $newRoles,
];
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* 构建权限树
*/
private function buildPermissionTree($permissions, $parentId = 0): array
{
$tree = [];
foreach ($permissions as $permission) {
if ($permission->parent_id == $parentId) {
$node = [
'id' => $permission->id,
'name' => $permission->name,
'code' => $permission->code,
'type' => $permission->type,
'route' => $permission->route,
'component' => $permission->component,
'meta' => $permission->meta,
'sort' => $permission->sort,
'status' => $permission->status,
'children' => $this->buildPermissionTree($permissions, $permission->id),
];
$tree[] = $node;
}
}
return $tree;
}
}
+208
View File
@@ -0,0 +1,208 @@
<?php
namespace App\Services\Auth;
use App\Models\Auth\User;
use Illuminate\Support\Facades\Cache;
class UserOnlineService
{
protected $cachePrefix = 'user_online:';
protected $expireMinutes = 5;
/**
* 设置用户在线
*/
public function setOnline(int $userId, string $token): void
{
$key = $this->getCacheKey($userId, $token);
Cache::put($key, [
'user_id' => $userId,
'token' => $token,
'last_active_at' => now()->toDateTimeString(),
'ip' => request()->ip(),
'user_agent' => request()->userAgent(),
], now()->addMinutes($this->expireMinutes));
// 更新用户的最后在线时间
User::where('id', $userId)->update([
'last_active_at' => now(),
]);
}
/**
* 更新用户在线状态
*/
public function updateOnline(int $userId, string $token): void
{
$key = $this->getCacheKey($userId, $token);
if (Cache::has($key)) {
Cache::put($key, [
'user_id' => $userId,
'token' => $token,
'last_active_at' => now()->toDateTimeString(),
'ip' => request()->ip(),
'user_agent' => request()->userAgent(),
], now()->addMinutes($this->expireMinutes));
}
}
/**
* 设置用户离线
*/
public function setOffline(int $userId, string $token): void
{
$key = $this->getCacheKey($userId, $token);
Cache::forget($key);
}
/**
* 设置用户所有设备离线
*/
public function setAllOffline(int $userId): void
{
$pattern = $this->cachePrefix . $userId . ':*';
$keys = Cache::store('redis')->getPrefix() . $pattern;
// Redis 模式删除
if (Cache::getStore() instanceof \Illuminate\Cache\RedisStore) {
$redis = Cache::getStore()->connection();
$keys = $redis->keys($this->cachePrefix . $userId . ':*');
if (!empty($keys)) {
$redis->del($keys);
}
}
}
/**
* 检查用户是否在线
*/
public function isOnline(int $userId): bool
{
$pattern = $this->cachePrefix . $userId . ':*';
if (Cache::getStore() instanceof \Illuminate\Cache\RedisStore) {
$redis = Cache::getStore()->connection();
$keys = $redis->keys($this->cachePrefix . $userId . ':*');
return !empty($keys);
}
return false;
}
/**
* 获取用户在线信息
*/
public function getOnlineInfo(int $userId, string $token): ?array
{
$key = $this->getCacheKey($userId, $token);
return Cache::get($key);
}
/**
* 获取用户所有在线会话
*/
public function getUserSessions(int $userId): array
{
$sessions = [];
if (Cache::getStore() instanceof \Illuminate\Cache\RedisStore) {
$redis = Cache::getStore()->connection();
$keys = $redis->keys($this->cachePrefix . $userId . ':*');
foreach ($keys as $key) {
$session = $redis->get($key);
if ($session) {
$sessions[] = json_decode($session, true);
}
}
}
return $sessions;
}
/**
* 获取所有在线用户数量
*/
public function getOnlineCount(): int
{
$count = 0;
if (Cache::getStore() instanceof \Illuminate\Cache\RedisStore) {
$redis = Cache::getStore()->connection();
$keys = $redis->keys($this->cachePrefix . '*');
// 去重用户ID
$userIds = [];
foreach ($keys as $key) {
preg_match('/user_online:(\d+):/', $key, $matches);
if (isset($matches[1])) {
$userIds[$matches[1]] = true;
}
}
$count = count($userIds);
}
return $count;
}
/**
* 获取在线用户列表
*/
public function getOnlineUsers(int $limit = 100): array
{
$onlineUsers = [];
if (Cache::getStore() instanceof \Illuminate\Cache\RedisStore) {
$redis = Cache::getStore()->connection();
$keys = $redis->keys($this->cachePrefix . '*');
$userIds = [];
$userSessions = [];
foreach ($keys as $key) {
$session = $redis->get($key);
if ($session) {
$session = json_decode($session, true);
$userId = $session['user_id'];
if (!isset($userIds[$userId])) {
$userIds[$userId] = $userId;
}
$userSessions[$userId] = $session;
}
}
$userIdList = array_values(array_slice($userIds, 0, $limit));
$users = User::whereIn('id', $userIdList)->get();
foreach ($users as $user) {
$onlineUsers[] = [
'id' => $user->id,
'username' => $user->username,
'real_name' => $user->real_name,
'avatar' => $user->avatar,
'last_active_at' => $userSessions[$user->id]['last_active_at'] ?? null,
'ip' => $userSessions[$user->id]['ip'] ?? null,
];
}
}
return $onlineUsers;
}
/**
* 清理过期会话
*/
public function cleanExpiredSessions(): int
{
// Redis 的 TTL 会自动清理过期键
return 0;
}
/**
* 生成缓存键
*/
protected function getCacheKey(int $userId, string $token): string
{
return $this->cachePrefix . $userId . ':' . md5($token);
}
}
+320
View File
@@ -0,0 +1,320 @@
<?php
namespace App\Services\Auth;
use App\Models\Auth\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
use Maatwebsite\Excel\Facades\Excel;
use App\Exports\Auth\UserExport;
use App\Imports\Auth\UserImport;
use App\Jobs\Auth\UserImportJob;
use App\Jobs\Auth\UserExportJob;
class UserService
{
/**
* 获取用户列表
*/
public function getList(array $params): array
{
$query = User::with(['department', 'roles']);
// 搜索条件
if (!empty($params['keyword'])) {
$query->where(function ($q) use ($params) {
$q->where('username', 'like', '%' . $params['keyword'] . '%')
->orWhere('real_name', 'like', '%' . $params['keyword'] . '%')
->orWhere('phone', 'like', '%' . $params['keyword'] . '%')
->orWhere('email', 'like', '%' . $params['keyword'] . '%');
});
}
if (!empty($params['department_id'])) {
$query->where('department_id', $params['department_id']);
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
if (!empty($params['role_id'])) {
$query->whereHas('roles', function ($q) use ($params) {
$q->where('role_id', $params['role_id']);
});
}
// 排序
$orderBy = $params['order_by'] ?? 'id';
$orderDirection = $params['order_direction'] ?? 'desc';
$query->orderBy($orderBy, $orderDirection);
// 分页
$page = $params['page'] ?? 1;
$pageSize = $params['page_size'] ?? 20;
$list = $query->paginate($pageSize, ['*'], 'page', $page);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $page,
'page_size' => $pageSize,
];
}
/**
* 获取用户详情
*/
public function getById(int $id): array
{
$user = User::with(['department', 'roles'])->find($id);
if (!$user) {
throw ValidationException::withMessages([
'id' => ['用户不存在'],
]);
}
return $this->formatUserInfo($user);
}
/**
* 创建用户
*/
public function create(array $data): User
{
// 检查用户名是否已存在
if (User::where('username', $data['username'])->exists()) {
throw ValidationException::withMessages([
'username' => ['用户名已存在'],
]);
}
// 检查手机号是否已存在
if (!empty($data['phone']) && User::where('phone', $data['phone'])->exists()) {
throw ValidationException::withMessages([
'phone' => ['手机号已被使用'],
]);
}
// 检查邮箱是否已存在
if (!empty($data['email']) && User::where('email', $data['email'])->exists()) {
throw ValidationException::withMessages([
'email' => ['邮箱已被使用'],
]);
}
DB::beginTransaction();
try {
$user = User::create([
'username' => $data['username'],
'password' => Hash::make($data['password']),
'real_name' => $data['real_name'],
'email' => $data['email'] ?? null,
'phone' => $data['phone'] ?? null,
'department_id' => $data['department_id'] ?? null,
'avatar' => $data['avatar'] ?? null,
'status' => $data['status'] ?? 1,
]);
// 关联角色
if (!empty($data['role_ids'])) {
$user->roles()->attach($data['role_ids']);
}
DB::commit();
return $user;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* 更新用户
*/
public function update(int $id, array $data): User
{
$user = User::find($id);
if (!$user) {
throw ValidationException::withMessages([
'id' => ['用户不存在'],
]);
}
// 检查用户名是否已被其他用户使用
if (isset($data['username']) && $data['username'] !== $user->username) {
if (User::where('username', $data['username'])->exists()) {
throw ValidationException::withMessages([
'username' => ['用户名已存在'],
]);
}
}
// 检查手机号是否已被其他用户使用
if (isset($data['phone']) && $data['phone'] !== $user->phone) {
if (User::where('phone', $data['phone'])->exists()) {
throw ValidationException::withMessages([
'phone' => ['手机号已被使用'],
]);
}
}
// 检查邮箱是否已被其他用户使用
if (isset($data['email']) && $data['email'] !== $user->email) {
if (User::where('email', $data['email'])->exists()) {
throw ValidationException::withMessages([
'email' => ['邮箱已被使用'],
]);
}
}
DB::beginTransaction();
try {
$updateData = [
'real_name' => $data['real_name'] ?? $user->real_name,
'email' => $data['email'] ?? $user->email,
'phone' => $data['phone'] ?? $user->phone,
'department_id' => $data['department_id'] ?? $user->department_id,
'avatar' => $data['avatar'] ?? $user->avatar,
'status' => $data['status'] ?? $user->status,
];
if (isset($data['username'])) {
$updateData['username'] = $data['username'];
}
if (isset($data['password'])) {
$updateData['password'] = Hash::make($data['password']);
}
$user->update($updateData);
// 更新角色关联
if (isset($data['role_ids'])) {
$user->roles()->sync($data['role_ids']);
}
DB::commit();
return $user;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* 删除用户
*/
public function delete(int $id): void
{
$user = User::find($id);
if (!$user) {
throw ValidationException::withMessages([
'id' => ['用户不存在'],
]);
}
$user->delete();
}
/**
* 批量删除用户
*/
public function batchDelete(array $ids): int
{
return User::whereIn('id', $ids)->delete();
}
/**
* 批量更新用户状态
*/
public function batchUpdateStatus(array $ids, int $status): int
{
return User::whereIn('id', $ids)->update(['status' => $status]);
}
/**
* 批量分配部门
*/
public function batchAssignDepartment(array $ids, int $departmentId): int
{
return User::whereIn('id', $ids)->update(['department_id' => $departmentId]);
}
/**
* 批量分配角色
*/
public function batchAssignRoles(array $ids, array $roleIds): void
{
foreach ($ids as $userId) {
$user = User::find($userId);
if ($user) {
$user->roles()->sync($roleIds);
}
}
}
/**
* 导出用户数据
*/
public function export(array $params): string
{
// 异步导出
$job = new UserExportJob($params);
dispatch($job);
return '导出任务已提交,请稍后在导出列表中查看';
}
/**
* 导入用户数据
*/
public function import(\Illuminate\Http\UploadedFile $file): array
{
$path = $file->store('imports');
// 异步导入
$job = new UserImportJob($path);
dispatch($job);
return [
'message' => '导入任务已提交,请稍后在导入列表中查看',
'file_path' => $path,
];
}
/**
* 格式化用户信息
*/
private function formatUserInfo(User $user): array
{
return [
'id' => $user->id,
'username' => $user->username,
'real_name' => $user->real_name,
'email' => $user->email,
'phone' => $user->phone,
'avatar' => $user->avatar,
'department' => $user->department ? [
'id' => $user->department->id,
'name' => $user->department->name,
] : null,
'roles' => $user->roles->map(function ($role) {
return [
'id' => $role->id,
'name' => $role->name,
'code' => $role->code,
];
})->toArray(),
'status' => $user->status,
'last_login_at' => $user->last_login_at ? $user->last_login_at->toDateTimeString() : null,
'last_login_ip' => $user->last_login_ip,
'created_at' => $user->created_at->toDateTimeString(),
'updated_at' => $user->updated_at->toDateTimeString(),
];
}
}
+198
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();
}
}
+158
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();
}
}
+210
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;
}
}
+125
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,
];
}
}
+167
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,
];
}
}
+160
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);
}
}
+451
View File
@@ -0,0 +1,451 @@
<?php
namespace App\Services\WebSocket;
use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
use Illuminate\Support\Facades\Log;
use App\Services\Auth\UserOnlineService;
/**
* WebSocket Handler
*
* Handles WebSocket connections, messages, and disconnections
*/
class WebSocketHandler implements WebSocketHandlerInterface
{
/**
* @var UserOnlineService
*/
protected $userOnlineService;
/**
* WebSocketHandler constructor
*/
public function __construct()
{
$this->userOnlineService = app(UserOnlineService::class);
}
/**
* Handle WebSocket connection open event
*
* @param Server $server
* @param Request $request
* @return void
*/
public function onOpen(Server $server, Request $request): void
{
try {
$fd = $request->fd;
$path = $request->server['path_info'] ?? $request->server['request_uri'] ?? '/';
Log::info('WebSocket connection opened', [
'fd' => $fd,
'path' => $path,
'ip' => $request->server['remote_addr'] ?? 'unknown'
]);
// Extract user ID from query parameters if provided
$userId = $request->get['user_id'] ?? null;
$token = $request->get['token'] ?? null;
if ($userId && $token) {
// Store user connection mapping
$server->wsTable->set('uid:' . $userId, [
'value' => $fd,
'expiry' => time() + 3600, // 1 hour expiry
]);
$server->wsTable->set('fd:' . $fd, [
'value' => $userId,
'expiry' => time() + 3600
]);
// Update user online status
$this->userOnlineService->updateUserOnlineStatus($userId, $fd, true);
Log::info('User connected to WebSocket', [
'user_id' => $userId,
'fd' => $fd
]);
// Send welcome message to client
$server->push($fd, json_encode([
'type' => 'welcome',
'data' => [
'message' => 'WebSocket connection established',
'user_id' => $userId,
'timestamp' => time()
]
]));
} else {
Log::warning('WebSocket connection without authentication', [
'fd' => $fd
]);
// Send error message
$server->push($fd, json_encode([
'type' => 'error',
'data' => [
'message' => 'Authentication required. Please provide user_id and token.',
'code' => 401
]
]));
}
} catch (\Exception $e) {
Log::error('WebSocket onOpen error', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
}
}
/**
* Handle WebSocket message event
*
* @param Server $server
* @param Frame $frame
* @return void
*/
public function onMessage(Server $server, Frame $frame): void
{
try {
$fd = $frame->fd;
$data = $frame->data;
Log::info('WebSocket message received', [
'fd' => $fd,
'data' => $data,
'opcode' => $frame->opcode
]);
// Parse incoming message
$message = json_decode($data, true);
if (!$message) {
$server->push($fd, json_encode([
'type' => 'error',
'data' => [
'message' => 'Invalid JSON format',
'code' => 400
]
]));
return;
}
// Handle different message types
$this->handleMessage($server, $fd, $message);
} catch (\Exception $e) {
Log::error('WebSocket onMessage error', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
}
}
/**
* Handle WebSocket message based on type
*
* @param Server $server
* @param int $fd
* @param array $message
* @return void
*/
protected function handleMessage(Server $server, int $fd, array $message): void
{
$type = $message['type'] ?? 'unknown';
$data = $message['data'] ?? [];
switch ($type) {
case 'ping':
// Respond to ping with pong
$server->push($fd, json_encode([
'type' => 'pong',
'data' => [
'timestamp' => time()
]
]));
break;
case 'heartbeat':
// Handle heartbeat
$server->push($fd, json_encode([
'type' => 'heartbeat_ack',
'data' => [
'timestamp' => time()
]
]));
break;
case 'chat':
// Handle chat message
$this->handleChatMessage($server, $fd, $data);
break;
case 'broadcast':
// Handle broadcast message (admin only)
$this->handleBroadcast($server, $fd, $data);
break;
case 'subscribe':
// Handle channel subscription
$this->handleSubscribe($server, $fd, $data);
break;
case 'unsubscribe':
// Handle channel unsubscription
$this->handleUnsubscribe($server, $fd, $data);
break;
default:
$server->push($fd, json_encode([
'type' => 'error',
'data' => [
'message' => 'Unknown message type: ' . $type,
'code' => 400
]
]));
break;
}
}
/**
* Handle chat message
*
* @param Server $server
* @param int $fd
* @param array $data
* @return void
*/
protected function handleChatMessage(Server $server, int $fd, array $data): void
{
$toUserId = $data['to_user_id'] ?? null;
$content = $data['content'] ?? '';
if (!$toUserId || !$content) {
$server->push($fd, json_encode([
'type' => 'error',
'data' => [
'message' => 'Missing required fields: to_user_id and content',
'code' => 400
]
]));
return;
}
// Get target user's connection
$targetFd = $server->wsTable->get('uid:' . $toUserId);
if ($targetFd && $targetFd['value']) {
$server->push((int)$targetFd['value'], json_encode([
'type' => 'chat',
'data' => [
'from_user_id' => $server->wsTable->get('fd:' . $fd)['value'] ?? null,
'content' => $content,
'timestamp' => time()
]
]));
// Send delivery receipt to sender
$server->push($fd, json_encode([
'type' => 'message_delivered',
'data' => [
'to_user_id' => $toUserId,
'content' => $content,
'timestamp' => time()
]
]));
} else {
$server->push($fd, json_encode([
'type' => 'error',
'data' => [
'message' => 'Target user is not online',
'code' => 404
]
]));
}
}
/**
* Handle broadcast message
*
* @param Server $server
* @param int $fd
* @param array $data
* @return void
*/
protected function handleBroadcast(Server $server, int $fd, array $data): void
{
$message = $data['message'] ?? '';
$userId = $server->wsTable->get('fd:' . $fd)['value'] ?? null;
// TODO: Check if user has admin permission to broadcast
// For now, allow any authenticated user
if (!$message) {
$server->push($fd, json_encode([
'type' => 'error',
'data' => [
'message' => 'Message content is required',
'code' => 400
]
]));
return;
}
// Broadcast to all connected clients except sender
$broadcastData = json_encode([
'type' => 'broadcast',
'data' => [
'from_user_id' => $userId,
'message' => $message,
'timestamp' => time()
]
]);
foreach ($server->connections as $connectionFd) {
if ($server->isEstablished($connectionFd) && $connectionFd !== $fd) {
$server->push($connectionFd, $broadcastData);
}
}
// Send confirmation to sender
$server->push($fd, json_encode([
'type' => 'broadcast_sent',
'data' => [
'message' => $message,
'timestamp' => time()
]
]));
}
/**
* Handle channel subscription
*
* @param Server $server
* @param int $fd
* @param array $data
* @return void
*/
protected function handleSubscribe(Server $server, int $fd, array $data): void
{
$channel = $data['channel'] ?? '';
if (!$channel) {
$server->push($fd, json_encode([
'type' => 'error',
'data' => [
'message' => 'Channel name is required',
'code' => 400
]
]));
return;
}
// Store subscription in wsTable
$server->wsTable->set('channel:' . $channel . ':fd:' . $fd, [
'value' => 1,
'expiry' => time() + 7200 // 2 hours
]);
$server->push($fd, json_encode([
'type' => 'subscribed',
'data' => [
'channel' => $channel,
'timestamp' => time()
]
]));
Log::info('User subscribed to channel', [
'fd' => $fd,
'channel' => $channel
]);
}
/**
* Handle channel unsubscription
*
* @param Server $server
* @param int $fd
* @param array $data
* @return void
*/
protected function handleUnsubscribe(Server $server, int $fd, array $data): void
{
$channel = $data['channel'] ?? '';
if (!$channel) {
$server->push($fd, json_encode([
'type' => 'error',
'data' => [
'message' => 'Channel name is required',
'code' => 400
]
]));
return;
}
// Remove subscription from wsTable
$server->wsTable->del('channel:' . $channel . ':fd:' . $fd);
$server->push($fd, json_encode([
'type' => 'unsubscribed',
'data' => [
'channel' => $channel,
'timestamp' => time()
]
]));
Log::info('User unsubscribed from channel', [
'fd' => $fd,
'channel' => $channel
]);
}
/**
* Handle WebSocket connection close event
*
* @param Server $server
* @param $fd
* @param $reactorId
* @return void
*/
public function onClose(Server $server, $fd, $reactorId): void
{
try {
Log::info('WebSocket connection closed', [
'fd' => $fd,
'reactor_id' => $reactorId
]);
// Get user ID from wsTable
$userId = $server->wsTable->get('fd:' . $fd)['value'] ?? null;
if ($userId) {
// Remove user connection mapping
$server->wsTable->del('uid:' . $userId);
$server->wsTable->del('fd:' . $fd);
// Update user online status
$this->userOnlineService->updateUserOnlineStatus($userId, $fd, false);
Log::info('User disconnected from WebSocket', [
'user_id' => $userId,
'fd' => $fd
]);
}
// Clean up channel subscriptions
// Note: In production, you might want to iterate through all channel keys
// and remove the ones associated with this fd
} catch (\Exception $e) {
Log::error('WebSocket onClose error', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
}
}
}
+402
View File
@@ -0,0 +1,402 @@
<?php
namespace App\Services\WebSocket;
use Illuminate\Support\Facades\Log;
use Swoole\WebSocket\Server;
/**
* WebSocket Service
*
* Provides helper functions for WebSocket operations
*/
class WebSocketService
{
/**
* Get Swoole WebSocket Server instance
*
* @return Server|null
*/
public function getServer(): ?Server
{
return app('swoole.server');
}
/**
* Send message to a specific user
*
* @param int $userId
* @param array $data
* @return bool
*/
public function sendToUser(int $userId, array $data): bool
{
$server = $this->getServer();
if (!$server || !isset($server->wsTable)) {
Log::warning('WebSocket server not available', ['user_id' => $userId]);
return false;
}
$fdInfo = $server->wsTable->get('uid:' . $userId);
if (!$fdInfo || !$fdInfo['value']) {
Log::info('User not connected to WebSocket', ['user_id' => $userId]);
return false;
}
$fd = (int)$fdInfo['value'];
if (!$server->isEstablished($fd)) {
Log::info('WebSocket connection not established', ['user_id' => $userId, 'fd' => $fd]);
// Clean up stale connection
$server->wsTable->del('uid:' . $userId);
$server->wsTable->del('fd:' . $fd);
return false;
}
$server->push($fd, json_encode($data));
Log::info('Message sent to user via WebSocket', [
'user_id' => $userId,
'fd' => $fd,
'data' => $data
]);
return true;
}
/**
* Send message to multiple users
*
* @param array $userIds
* @param array $data
* @return array Array of user IDs who received the message
*/
public function sendToUsers(array $userIds, array $data): array
{
$sentTo = [];
foreach ($userIds as $userId) {
if ($this->sendToUser($userId, $data)) {
$sentTo[] = $userId;
}
}
return $sentTo;
}
/**
* Broadcast message to all connected clients
*
* @param array $data
* @param int|null $excludeUserId User ID to exclude from broadcast
* @return int Number of clients the message was sent to
*/
public function broadcast(array $data, ?int $excludeUserId = null): int
{
$server = $this->getServer();
if (!$server) {
Log::warning('WebSocket server not available for broadcast');
return 0;
}
$message = json_encode($data);
$count = 0;
foreach ($server->connections as $fd) {
if (!$server->isEstablished($fd)) {
continue;
}
// Check if we should exclude this user
if ($excludeUserId) {
$fdInfo = $server->wsTable->get('fd:' . $fd);
if ($fdInfo && $fdInfo['value'] == $excludeUserId) {
continue;
}
}
$server->push($fd, $message);
$count++;
}
Log::info('Broadcast sent via WebSocket', [
'data' => $data,
'exclude_user_id' => $excludeUserId,
'count' => $count
]);
return $count;
}
/**
* Send message to all subscribers of a channel
*
* @param string $channel
* @param array $data
* @return int Number of subscribers who received the message
*/
public function sendToChannel(string $channel, array $data): int
{
$server = $this->getServer();
if (!$server || !isset($server->wsTable)) {
Log::warning('WebSocket server not available for channel broadcast', ['channel' => $channel]);
return 0;
}
$count = 0;
$message = json_encode($data);
// Iterate through all connections and check if they're subscribed to the channel
foreach ($server->connections as $fd) {
if (!$server->isEstablished($fd)) {
continue;
}
$subscription = $server->wsTable->get('channel:' . $channel . ':fd:' . $fd);
if ($subscription) {
$server->push($fd, $message);
$count++;
}
}
Log::info('Channel message sent via WebSocket', [
'channel' => $channel,
'data' => $data,
'count' => $count
]);
return $count;
}
/**
* Get online user count
*
* @return int
*/
public function getOnlineUserCount(): int
{
$server = $this->getServer();
if (!$server || !isset($server->wsTable)) {
return 0;
}
// Count established connections
$count = 0;
foreach ($server->connections as $fd) {
if ($server->isEstablished($fd)) {
$count++;
}
}
return $count;
}
/**
* Check if a user is online
*
* @param int $userId
* @return bool
*/
public function isUserOnline(int $userId): bool
{
$server = $this->getServer();
if (!$server || !isset($server->wsTable)) {
return false;
}
$fdInfo = $server->wsTable->get('uid:' . $userId);
if (!$fdInfo || !$fdInfo['value']) {
return false;
}
$fd = (int)$fdInfo['value'];
return $server->isEstablished($fd);
}
/**
* Disconnect a user from WebSocket
*
* @param int $userId
* @return bool
*/
public function disconnectUser(int $userId): bool
{
$server = $this->getServer();
if (!$server || !isset($server->wsTable)) {
return false;
}
$fdInfo = $server->wsTable->get('uid:' . $userId);
if (!$fdInfo || !$fdInfo['value']) {
return false;
}
$fd = (int)$fdInfo['value'];
if ($server->isEstablished($fd)) {
$server->push($fd, json_encode([
'type' => 'disconnect',
'data' => [
'message' => 'You have been disconnected',
'timestamp' => time()
]
]));
// Close the connection
$server->disconnect($fd);
// Clean up
$server->wsTable->del('uid:' . $userId);
$server->wsTable->del('fd:' . $fd);
Log::info('User disconnected from WebSocket by server', [
'user_id' => $userId,
'fd' => $fd
]);
return true;
}
return false;
}
/**
* Get all online user IDs
*
* @return array
*/
public function getOnlineUserIds(): array
{
$server = $this->getServer();
if (!$server || !isset($server->wsTable)) {
return [];
}
$userIds = [];
foreach ($server->connections as $fd) {
if (!$server->isEstablished($fd)) {
continue;
}
$fdInfo = $server->wsTable->get('fd:' . $fd);
if ($fdInfo && $fdInfo['value']) {
$userIds[] = (int)$fdInfo['value'];
}
}
return array_unique($userIds);
}
/**
* Send system notification to all online users
*
* @param string $title
* @param string $message
* @param string $type
* @param array $extraData
* @return int
*/
public function sendSystemNotification(string $title, string $message, string $type = 'info', array $extraData = []): int
{
$data = [
'type' => 'notification',
'data' => [
'title' => $title,
'message' => $message,
'type' => $type, // info, success, warning, error
'timestamp' => time(),
...$extraData
]
];
return $this->broadcast($data);
}
/**
* Send notification to specific users
*
* @param array $userIds
* @param string $title
* @param string $message
* @param string $type
* @param array $extraData
* @return array
*/
public function sendNotificationToUsers(array $userIds, string $title, string $message, string $type = 'info', array $extraData = []): array
{
$data = [
'type' => 'notification',
'data' => [
'title' => $title,
'message' => $message,
'type' => $type,
'timestamp' => time(),
...$extraData
]
];
return $this->sendToUsers($userIds, $data);
}
/**
* Push data update to specific users
*
* @param array $userIds
* @param string $resourceType
* @param string $action
* @param array $data
* @return array
*/
public function pushDataUpdate(array $userIds, string $resourceType, string $action, array $data): array
{
$message = [
'type' => 'data_update',
'data' => [
'resource_type' => $resourceType, // e.g., 'user', 'order', 'product'
'action' => $action, // create, update, delete
'data' => $data,
'timestamp' => time()
]
];
return $this->sendToUsers($userIds, $message);
}
/**
* Push data update to a channel
*
* @param string $channel
* @param string $resourceType
* @param string $action
* @param array $data
* @return int
*/
public function pushDataUpdateToChannel(string $channel, string $resourceType, string $action, array $data): int
{
$message = [
'type' => 'data_update',
'data' => [
'resource_type' => $resourceType,
'action' => $action,
'data' => $data,
'timestamp' => time()
]
];
return $this->sendToChannel($channel, $message);
}
}