diff --git a/1.sql b/1.sql deleted file mode 100644 index ea1e67c..0000000 --- a/1.sql +++ /dev/null @@ -1,3 +0,0 @@ -User ← user_roles → Role ← role_resources → Resource (type=menu/button) - ↘︎ - role_permissions → Permission \ No newline at end of file diff --git a/app/admin/controller/AuthController_bak.php b/app/admin/controller/AuthController_bak.php deleted file mode 100644 index f6feeb4..0000000 --- a/app/admin/controller/AuthController_bak.php +++ /dev/null @@ -1,83 +0,0 @@ -authServer = $auth; - $this->resourceServer = $res; - } - - // 浏览器授权页 - public function authorize(ServerRequestInterface $request) - { - // 如果未登录,跳转登录页(自行实现) - // 登录后,确认授权 -> issue auth code -> 重定向回 client - - // …未登录重定向到登录页,登录后调用: - // $authRequest = $this->authServer->validateAuthorizationRequest($request); - // …用户同意授权… - // return $this->authServer->completeAuthorizationRequest($authRequest, new Psr7Response()); - } - - // 授权码换 Token - public function token(ServerRequestInterface $request) - { - return $this->authServer->respondToAccessTokenRequest($request, new Psr7Response()); - } - - // 获取用户信息 - public function userinfo(ServerRequestInterface $request) - { - $psr = $this->resourceServer->validateAuthenticatedRequest($request); - $userId = $psr->getAttribute('oauth_user_id'); - // 根据 $userId 查询用户表返回 - // return json(['id' => $userId, 'username' => 'demo']); - - // 用 Webman 的 support\Response 返回 JSON 更方便 - return json([ - 'id' => $userId, - 'username' => User::find($userId)->username, - ]); - } - - - public function info(Request $request) - { - $userId = $request->session()->get('user_id'); - if (!$userId) { - return json(['code'=>401,'msg'=>'未登录']); - } - - $user = User::with('roles')->find($userId); - - return json([ - 'code' => 0, - 'data' => [ - 'user' => [ - 'id' => $user->id, - 'username' => $user->username, - 'roles' => $user->roles->pluck('name')->all(), - ], - 'menus' => AuthService::getMenuTree($userId), - 'permissions' => AuthService::getPermissionCodes($userId), - ], - 'msg' => 'ok', - ]); - } -} diff --git a/app/admin/controller/RoleController.php b/app/admin/controller/RoleController.php new file mode 100644 index 0000000..59ccb54 --- /dev/null +++ b/app/admin/controller/RoleController.php @@ -0,0 +1,98 @@ +all(); + $result = (new RoleService())->getRoleList($params); + // 定义后处理器 + $postProcessor = function($formattedData) { + if (isset($formattedData['created_at'])) + $formattedData['created_at'] = date('Y-m-d H:i:s', $formattedData['created_at']); + if (isset($formattedData['updated_at'])) + $formattedData['updated_at'] = date('Y-m-d H:i:s', $formattedData['updated_at']); + return $formattedData; + }; + return $this->success($this->transformData($result, $postProcessor), '角色列表获取成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 获取角色详情 + */ + public function detail(Request $request) + { + try { + $roleId = $request->get('role_id'); + if (!$roleId) { + return $this->error('缺少角色ID', 422); + } + $result = (new RoleService())->getRoleDetail($roleId); + return $this->success($result, '角色详情获取成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 增加角色 + */ + public function create(Request $request) + { + try { + $data = $this->validate(new RoleValidate(), $request->post(), 'role_create'); + $result = (new RoleService())->createRole($data); + return $this->success($result, '角色创建成功'); + } catch (\think\exception\ValidateException $e) { + return $this->error($e->getError(), 422); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 修改角色 + */ + public function update(Request $request) + { + try { + $data = $this->validate(new RoleValidate(), $request->post(), 'role_update'); + $result = (new RoleService())->updateRole($data); + return $this->success($result, '角色更新成功'); + } catch (\think\exception\ValidateException $e) { + return $this->error($e->getError(), 422); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 删除角色 + */ + public function delete(Request $request) + { + try { + $roleId = $request->get('role_id'); + if (!$roleId) { + return $this->error('缺少角色ID', 422); + } + (new RoleService())->deleteRole($roleId); + return $this->success([], '角色删除成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } +} diff --git a/app/common/controller/AuthController.php b/app/common/controller/AuthController.php index 4129ee0..02676e8 100644 --- a/app/common/controller/AuthController.php +++ b/app/common/controller/AuthController.php @@ -14,6 +14,8 @@ class AuthController extends BaseController */ protected $noNeedHeader = ['feishuAuth']; + protected $noNeedLogin = ['feishuScope', 'login', 'register', 'feishuAuth']; + // 调试 public function feishuAuth(Request $request) @@ -58,46 +60,56 @@ class AuthController extends BaseController } } - public function verify(Request $request) - { - try { - $token = $request->post('access_token') ?: $request->header('authorization'); - $systemCode = $request->post('system_code'); - if (!$token || !$systemCode) return $this->error('参数缺失', 422); - $payload = (new AuthService())->verifyToken($token, $systemCode); - return $this->success($payload, '验证成功'); - } catch (\Throwable $e) { - return $this->error($e->getMessage() ?: '无效或过期的token', 401); - } - } - public function refresh(Request $request) { try { - $refresh_token = $request->post('refresh_token'); - $systemCode = $request->post('system_code'); - if (!$refresh_token || !$systemCode) return $this->error('参数缺失', 422); - $result = (new AuthService())->refreshToken($refresh_token, $systemCode); + $data = $this->requestData($request); + if (!$data['refresh_token'] || !$data['system_code']) return $this->error('参数缺失', 422); + $result = (new AuthService())->refreshToken($data['refresh_token'], $data['system_code']); return $this->success($result, '刷新成功'); } catch (\Throwable $e) { return $this->error($e->getMessage(), 401); } } - public function logout(Request $request) - { - $token = $request->post('access_token') ?: $request->header('Authorization'); - $systemCode = $request->post('system_code') ?: $request->header('system_code'); - (new AuthService())->logout($token, $systemCode); - return $this->success([], '已登出'); - } - public function blockToken(Request $request) - { - $token = $request->post('token'); - $systemCode = $request->post('system_code'); - if (!$token || !$systemCode) return $this->error('参数缺失', 422); - (new AuthService())->blockToken($token, $systemCode); - return $this->success([], '已强制停用该token'); - } + // public function perm(Request $request) + // { + // try { + // $data = $this->requestData($request); + // if (!$data['access_token'] || !$data['system_code']) return $this->error('参数缺失', 422); + // $result = (new AuthService())->logout($data['access_token'], $data['system_code']); + // return $this->success($result, '已登出'); + // } catch (\Throwable $e) {} + // } + + // public function verify(Request $request) + // { + // try { + // $token = $request->post('access_token') ?: $request->header('authorization'); + // $systemCode = $request->post('system_code'); + // if (!$token || !$systemCode) return $this->error('参数缺失', 422); + // $payload = (new AuthService())->verifyToken($token, $systemCode); + // return $this->success($payload, '验证成功'); + // } catch (\Throwable $e) { + // return $this->error($e->getMessage() ?: '无效或过期的token', 401); + // } + // } + + // public function logout(Request $request) + // { + // $token = $request->post('access_token') ?: $request->header('Authorization'); + // $systemCode = $request->post('system_code') ?: $request->header('system_code'); + // (new AuthService())->logout($token, $systemCode); + // return $this->success([], '已登出'); + // } + + // public function blockToken(Request $request) + // { + // $token = $request->post('token'); + // $systemCode = $request->post('system_code'); + // if (!$token || !$systemCode) return $this->error('参数缺失', 422); + // (new AuthService())->blockToken($token, $systemCode); + // return $this->success([], '已强制停用该token'); + // } } \ No newline at end of file diff --git a/app/common/controller/BaseController.php b/app/common/controller/BaseController.php index 0fefb82..2dbf636 100644 --- a/app/common/controller/BaseController.php +++ b/app/common/controller/BaseController.php @@ -4,12 +4,13 @@ namespace app\common\controller; use support\exception\BusinessException; use app\common\traits\ResponseTrait; +use app\common\traits\DataTransformer; use think\Validate; use support\Request; -abstract class BaseController +class BaseController { - use ResponseTrait; + use ResponseTrait, DataTransformer; protected mixed $validator = null; diff --git a/app/common/controller/UserController.php b/app/common/controller/UserController.php new file mode 100644 index 0000000..efab4a1 --- /dev/null +++ b/app/common/controller/UserController.php @@ -0,0 +1,25 @@ +user->uid; + $result = (new UserService())->getUserInfo($uid); + return $this->success($result, '获取用户信息'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + +} \ No newline at end of file diff --git a/app/common/logic/AuthLogic.php b/app/common/logic/AuthLogic.php index 3c84694..ccdbf16 100644 --- a/app/common/logic/AuthLogic.php +++ b/app/common/logic/AuthLogic.php @@ -7,7 +7,6 @@ use app\common\model\CorpFeishuApp; use app\common\model\UserTokenLog; use app\common\model\Systems; use app\common\model\User; -use app\common\model\UserFeishuApp; use app\common\model\UserSn; use app\common\utils\Feishu; use app\common\utils\Jwt; @@ -46,87 +45,83 @@ class AuthLogic return $this->generateTokensAndReturn($user, $system, $data); } - /** - * 扫码登录 - */ - public function qrCodeLogin(array $data): array - { - $userId = $this->validateQrCodeToken($data['qr_code_token']); - $user = User::find($userId); - - if (!$user || $user->status != 1) { - throw new BusinessException('用户无效', 403); - } - - $system = $this->validateSystem($data['system_code']); - $this->validateSystemPermission($user, $system); - - return $this->generateTokensAndReturn($user, $system, $data); - } - /** * 注册 */ public function register(array $data) { - // 获取可用用户编号 - $sn = $this->getAvailableUserSn(); - $data['sn'] = $sn->user_sn; + try { + // 获取可用用户编号 + $sn = $this->getAvailableUserSn(); + $data['sn'] = $sn->user_sn; - // 对密码进行哈希处理 - $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + // 对密码进行哈希处理 + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); - // 获取企业ID - $corpInfo = $this->validateCorp($data['corp_abbr']); - $data['corp_id'] = $corpInfo->id; + // 获取企业ID + $corpInfo = $this->validateCorp($data['corp_abbr']); + $data['corp_id'] = $corpInfo->id; - // 处理账户 - $data = $data + normalizeAccount($data['account']); - - // 创建用户记录 - $user = $this->createUser($data); + // 处理账户 + $data = $data + normalizeAccount($data['account']); + + // 创建用户记录 + $user = $this->createUser($data); - // 更新用户编号的用户 ID - $this->updateUserId($user->id, $sn->user_sn); + // 更新用户编号的用户 ID + $this->updateUserId($user->id, $sn->user_sn); - $system = $this->validateSystem($data['system_code']); - return $this->generateTokensAndReturn($user, $system, $data); + $system = $this->validateSystem($data['system_code']); + return $this->generateTokensAndReturn($user, $system, $data); + } catch (\Throwable $e) { + throw $e; + } } - /** ----------- 内部方法 ------------- **/ - /** * 查找或创建飞书用户 */ private function getOrCreateUser(array $feishuUserInfo) { - $userFeishuApp = UserFeishuApp::firstOrCreate([ - 'name' => $feishuUserInfo['en_name'], - 'mobile' => $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''), - 'union_id' => $feishuUserInfo['union_id'], - 'user_id' => $feishuUserInfo['user_id'], - 'open_id' => $feishuUserInfo['open_id'], - 'tenant_key' => $feishuUserInfo['tenant_key'] - ]); - - if ($userFeishuApp->uid) { - $user = User::find($userFeishuApp->uid); - $user->isLogin = true; - return $user; - } else { - $user = User::where('phone', $userFeishuApp->mobile)->first(); - $user->isLogin = true; + try { + $phone = $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''); + $user = User::where('phone', $phone)->where('corp_id', $feishuUserInfo['corp_id'])->first(); + if (!$user) { - $user = $this->createUserByFeishu($feishuUserInfo, $userFeishuApp); + // 获取编号 + $userSn = $this->getAvailableUserSn(); + // 创建用户 + $user = User::create([ + 'name' => $feishuUserInfo['en_name'], + 'avatar' => $feishuUserInfo['avatar_middle'], + 'phone' => $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''), + 'union_id' => $feishuUserInfo['union_id'], + 'user_id' => $feishuUserInfo['user_id'], + 'open_id' => $feishuUserInfo['open_id'], + 'tenant_key' => $feishuUserInfo['tenant_key'], + 'corp_id' => $feishuUserInfo['corp_id'], + 'password' => password_hash($userSn->user_sn, PASSWORD_DEFAULT) + ]); + // 更新使用 + $this->updateUserId($user->id, $userSn->user_sn); + // 更新编号 + if (!$user->update(['sn'=>$userSn->user_sn])) { + throw new BusinessException('编号绑定失败'); + } + // 是否登陆 $user->isLogin = false; - } - $userFeishuApp->uid = $user->id; - $userFeishuApp->corp_id = $feishuUserInfo['corp_id']; - if (!$userFeishuApp->save()) { - throw new BusinessException('绑定用户飞书记录失败'); + } else { + $user->update([ + 'union_id' => $feishuUserInfo['union_id'], + 'user_id' => $feishuUserInfo['user_id'], + 'open_id' => $feishuUserInfo['open_id'], + 'tenant_key' => $feishuUserInfo['tenant_key'] + ]); } return $user; - } + } catch (\Throwable $e) { + throw $e; + } } /** @@ -137,38 +132,6 @@ class AuthLogic return isset($mobile) ? str_replace('+86', '', $mobile) : ''; } - /** - * 创建飞书用户 - */ - private function createUserByFeishu(array $feishuUserInfo, UserFeishuApp $userFeishuApp) - { - // 1. 获取适用的用户编号 - $userSn = UserSn::where('user_id', '=', 0)->inRandomOrder()->first(); - if (!$userSn) { - throw new BusinessException('未找到适用编号'); - } - - // 2. 创建用户 - $user = new User(); - $user->sn = $userSn->user_sn; - $user->name = $feishuUserInfo['en_name']; - $user->avatar = $feishuUserInfo['avatar_middle'] ?? ''; - $user->password = password_hash($userSn->user_sn, PASSWORD_DEFAULT); - $user->phone = $userFeishuApp->mobile; - $user->email = ''; - $user->system_code = $feishuUserInfo['system_code']; - $user->corp_id = $feishuUserInfo['corp_id']; - - if (!$user->save()) { - throw new BusinessException('用户创建失败'); - } - - // 更新使用状态 - $this->updateUserId($user->id, $userSn->user_sn); - - return $user; - } - /** * 生成并记录token,返回包含token的响应数据 */ @@ -295,7 +258,8 @@ class AuthLogic public function feishuAppInfo(string $corpAbbr, string $systemCode) { $corpInfo = $this->validateCorp($corpAbbr); - $fsAppInfo = $this->validateFeishuApp($corpInfo->id, $systemCode); + $systemInfo = $this->validateSystem($systemCode); + $fsAppInfo = $this->validateFeishuApp($corpInfo->id, $systemInfo->id); return [ 'app_id' => $fsAppInfo->app_id, 'app_secret' => $fsAppInfo->app_secret, @@ -303,7 +267,8 @@ class AuthLogic 'scope' => $fsAppInfo->scopes, 'state' => $fsAppInfo->state, 'abbr' => $corpAbbr, - 'corp_id' => $corpInfo->id + 'corp_id' => $corpInfo->id, + 'system_id' => $systemInfo->id ]; } @@ -349,9 +314,9 @@ class AuthLogic /** * 验证飞书应用信息 */ - private function validateFeishuApp(int $corpId, string $systemCode) + private function validateFeishuApp(int $corpId, int $systemId) { - $feishuAppInfo = CorpFeishuApp::where('corp_id', $corpId)->where('system_code', $systemCode)->first(); + $feishuAppInfo = CorpFeishuApp::where('corp_id', $corpId)->where('system_id', $systemId)->first(); if (!$feishuAppInfo) { throw new BusinessException('飞书应用不存在', 403); } @@ -371,18 +336,6 @@ class AuthLogic } } - /** - * 验证扫码令牌 - */ - private function validateQrCodeToken(string $qrCodeToken) - { - $userId = Redis::get("qr_code_token:{$qrCodeToken}"); - if (!$userId) { - throw new BusinessException('扫码令牌无效或已过期', 401); - } - return $userId; - } - /** * 获取可用的用户编号 * @@ -463,4 +416,88 @@ class AuthLogic Redis::set("refresh_token_map:{$refreshToken}:{$systemCode}", $userId, config('jwt.jwt_refresh_token_expiration')); return $refreshToken; } + + + /** + * 刷新token + * @param string $refreshToken + * @param string|null $systemCode 可选,指定系统 + * @return array + */ + public function refreshToken(string $refreshToken, ?string $systemCode = null): array + { + if (!$systemCode) throw new BusinessException('缺少system_code', 422); + + // 校验refresh_token + $userId = Redis::get("refresh_token_map:{$refreshToken}:{$systemCode}"); + if (!$userId) { + throw new BusinessException('refresh_token无效或已过期', 401); + } + + $validToken = Redis::get("refresh_token:{$userId}:{$systemCode}"); + if ($validToken !== $refreshToken) { + throw new BusinessException('refresh_token无效或已过期', 401); + } + + $user = User::find($userId); + if (!$user || $user->status != 1) { + throw new BusinessException('用户无效', 403); + } + + $system = $this->validateSystem($systemCode); + $this->validateSystemPermission($user, $system); + + $payload = $this->buildJwtPayload($user, $system); + $token = $this->generateAccessToken($payload); + $refreshToken = $this->generateRefreshToken($user->id, $systemCode); + $this->recordUserToken($user->id, $token, $systemCode); + + return [ + 'access_token' => $token, + 'refresh_token' => $refreshToken, + 'expires_in' => config('jwt.jwt_expiration'), + // 'payload' => $payload, + ]; + } + + /** + * 登出/失效token + */ + public function logout(?string $token, ?string $systemCode = null) + { + if ($token) { + Redis::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration')); + $payload = null; + try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {} + $userId = $payload['uid'] ?? null; + $aud = $payload['aud'] ?? null; + $sc = $systemCode ?? $aud; + if ($userId && $sc) { + Redis::sRem("user_tokens:{$userId}:{$sc}", $token); + } + Redis::del("user_token_meta:{$token}"); + UserTokenLog::where('token', $token)->update(['is_valid'=>0]); + } + } + + /** + * 强制拉黑token + */ + public function blockToken(string $token, ?string $systemCode = null) + { + if (!$token) { + throw new BusinessException('token不能为空', 422); + } + Redis::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration')); + $payload = null; + try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {} + $userId = $payload['uid'] ?? null; + $aud = $payload['aud'] ?? null; + $sc = $systemCode ?? $aud; + if ($userId && $sc) { + Redis::sRem("user_tokens:{$userId}:{$sc}", $token); + } + Redis::del("user_token_meta:{$token}"); + UserTokenLog::where('token', $token)->update(['is_valid'=>0]); + } } \ No newline at end of file diff --git a/app/common/logic/BaseLogic.php b/app/common/logic/BaseLogic.php new file mode 100644 index 0000000..94beff3 --- /dev/null +++ b/app/common/logic/BaseLogic.php @@ -0,0 +1,86 @@ +paginate($params['per_page'], ['*'], 'page', $params['page']); + } + + return $query->get(); + } + + /** + * 获取单个记录 + */ + public function getDetail(int $id, string $modelClass) + { + $record = $modelClass::find($id); + if (!$record) { + throw new BusinessException("记录不存在", 404); + } + + return $record; + } + + /** + * 创建记录 + */ + public function create(array $data, string $modelClass) + { + $model = new $modelClass; + $model->fill($data); // 根据字段填充数据 + try { + $model->save(); + } catch (\Throwable $e) { + throw new BusinessException("创建记录失败: " . $e->getMessage(), 500); + } + + return $model; + } + + /** + * 更新记录 + */ + public function update(array $data, string $modelClass) + { + $record = $modelClass::find($data['id']); + if (!$record) { + throw new BusinessException("记录不存在", 404); + } + + $record->update($data); + return $record; + } + + /** + * 删除记录 + */ + public function delete(int $id, string $modelClass) + { + $record = $modelClass::find($id); + if (!$record) { + throw new BusinessException("记录不存在", 404); + } + + try { + $record->delete(); + } catch (\Throwable $e) { + throw new BusinessException("删除记录失败: " . $e->getMessage(), 500); + } + + return true; + } +} diff --git a/app/common/logic/PermLogic.php b/app/common/logic/PermLogic.php new file mode 100644 index 0000000..b4fe819 --- /dev/null +++ b/app/common/logic/PermLogic.php @@ -0,0 +1,109 @@ +find($userId); + if (!$user) { + return collect(); + } + + $all = collect(); + foreach ($user->roles as $role) { + $all = $all->merge($role->resources); + } + $all = $all->merge($user->resources); + return $all->unique('id')->values(); + } + + /** + * 构建目录/菜单树,节点包含权限与打开方式,拼接完整路径,并对子级菜单component处理 + * @param \Illuminate\Support\Collection $resources + * @param int $parentId + * @param string $parentPath + * @return array + */ + public function buildTree($resources, int $parentId = 0, string $parentPath = ''): array + { + $tree = []; + foreach ($resources as $node) { + if ($node->parent_id !== $parentId) continue; + + // 拼接完整路径 + $currentPath = $parentPath; + if ($currentPath && substr($currentPath, -1) === '/') { + $currentPath = rtrim($currentPath, '/'); + } + if (!empty($node->url)) { + if (!$currentPath) { + $currentPath = $node->url; + } else { + $url = ltrim($node->url, '/'); + $currentPath = $currentPath . '/' . $url; + } + } + + // 子节点递归 + $children = []; + if ($node->type === 'directory') { + $children = $this->buildTree($resources, $node->id, $currentPath); + } + + // 权限 + $authList = $resources + ->filter(fn($r) => $r->type === 'permission' && $r->parent_id === $node->id) + ->map(fn($r) => [ + 'id' => $r->id, + 'title' => $r->name, + 'auth_mark' => $r->code, + ])->values()->all(); + + // 判断是否有子级菜单(非权限) + $hasChildrenMenu = collect($resources)->where('parent_id', $node->id) + ->whereIn('type', ['directory', 'menu'])->isNotEmpty(); + + // component 字段规则: + // 1. 存在子菜单,固定 '/index/index' + // 2. 否则为当前完整路径(或其它你想要的) + $componentValue = $hasChildrenMenu ? '/index/index' : $currentPath; + + $tree[] = [ + 'id' => $node->id, + 'name' => $node->name, + 'path' => $node->url, + 'component' => $componentValue, + 'meta' => [ + 'title' => $node->name, + 'icon' => $node->icon, + 'keepAlive' => true, + 'authList' => $authList, + 'openType' => $node->open_type, + ], + 'children' => $children, + ]; + } + return $tree; + } + + /** + * 获取用户所有权限code + */ + public function getPermissionCodes(int $userId): array + { + $resources = $this->getUserResources($userId); + return $resources + ->filter(fn($res) => $res->type === 'permission') + ->pluck('code') + ->unique() + ->values() + ->all(); + } +} \ No newline at end of file diff --git a/app/common/logic/RoleLogic.php b/app/common/logic/RoleLogic.php new file mode 100644 index 0000000..5cb3ae7 --- /dev/null +++ b/app/common/logic/RoleLogic.php @@ -0,0 +1,93 @@ +where('system_id', $params['system_id']); + } + if (isset($params['status'])) { + $query->where('status', $params['status']); + } + + // 添加分页 + $roles = $query->paginate($params['per_page'] ?? 15); + + return $roles; + } + + /** + * 获取角色详情 + */ + public function getRoleDetail(int $roleId) + { + $role = Role::find($roleId); + if (!$role) { + throw new BusinessException('角色不存在', 404); + } + + return $role; + } + + /** + * 创建角色 + */ + public function createRole(array $data) + { + $role = new Role(); + $role->fill($data); // 根据字段填充数据 + try { + $role->save(); + } catch (\Throwable $e) { + Log::error('创建角色失败:'.$e->getMessage()); + throw new BusinessException('创建角色失败', 500); + } + + return $role; + } + + /** + * 更新角色 + */ + public function updateRole(array $data) + { + $role = Role::find($data['id']); + if (!$role) { + throw new BusinessException('角色不存在', 404); + } + + $role->update($data); // 更新字段 + return $role; + } + + /** + * 删除角色 + */ + public function deleteRole(int $roleId) + { + $role = Role::find($roleId); + if (!$role) { + throw new BusinessException('角色不存在', 404); + } + + try { + $role->delete(); + } catch (\Throwable $e) { + Log::error('删除角色失败:'.$e->getMessage()); + throw new BusinessException('删除角色失败', 500); + } + } +} diff --git a/app/common/logic/UserLogic.php b/app/common/logic/UserLogic.php new file mode 100644 index 0000000..1d6dc93 --- /dev/null +++ b/app/common/logic/UserLogic.php @@ -0,0 +1,27 @@ +status != 1) { + throw new BusinessException('账号已禁用', 403); + } + + return $user; + } +} \ No newline at end of file diff --git a/app/common/model/Role.php b/app/common/model/Role.php index 3f51b37..ea84200 100644 --- a/app/common/model/Role.php +++ b/app/common/model/Role.php @@ -41,6 +41,8 @@ class Role extends Model * @var bool */ public $timestamps = true; + + protected $dateFormat = 'U'; // 角色下所有用户 public function users() diff --git a/app/common/model/User.php b/app/common/model/User.php index 23e8b1c..4709b44 100644 --- a/app/common/model/User.php +++ b/app/common/model/User.php @@ -58,8 +58,12 @@ class User extends Model 'phone', 'email', 'status', - 'system_code', - 'corp_id' + 'corp_id', + 'user_type', + 'union_id', + 'user_id', + 'open_id', + 'tenant_key' ]; // 用户所有角色 diff --git a/app/common/model/UserFeishuApp_bak.php b/app/common/model/UserFeishuApp_bak.php new file mode 100644 index 0000000..9aa06c0 --- /dev/null +++ b/app/common/model/UserFeishuApp_bak.php @@ -0,0 +1,69 @@ + $logic->feishuLogin($data), - 'qrcode' => $logic->qrCodeLogin($data), + // 'qrcode' => $logic->qrCodeLogin($data), default => $logic->uPwdLogin($data), }; } @@ -86,4 +86,13 @@ class AuthService { (new AuthLogic())->blockToken($token, $systemCode); } + + + // public function perms() + // { + // $permService = new PermService(); + // $menuTree = $permService->getMenuTree($uid); + // $permissionCodes = $permService->getPermissionCodes($uid); + // return (new PermService())->getMenuTree($this->userId) + // } } \ No newline at end of file diff --git a/app/common/service/PermService.php b/app/common/service/PermService.php index 076a4bd..8f8770c 100644 --- a/app/common/service/PermService.php +++ b/app/common/service/PermService.php @@ -2,11 +2,17 @@ namespace app\common\service; -use app\common\model\User; +use app\common\logic\PermLogic; use support\Cache; class PermService { + protected $logic; + + public function __construct() + { + $this->logic = (new PermLogic()); + } // 用户资源缓存key protected static function cacheKey(int $userId): string @@ -17,92 +23,42 @@ class PermService /** * 获取用户所有资源(合并角色+直接分配),结果缓存 */ - public static function getUserResources(int $userId) + public function getUserResources(int $userId) { $cacheKey = self::cacheKey($userId); $cached = Cache::get($cacheKey); if ($cached !== null) { return $cached; } - - $user = User::with(['roles.resources', 'resources'])->find($userId); - if (!$user) { - return collect(); - } - - $all = collect(); - foreach ($user->roles as $role) { - $all = $all->merge($role->resources); - } - $all = $all->merge($user->resources); - $unique = $all->unique('id')->values(); - - Cache::set($cacheKey, $unique, 3600); - return $unique; + // 调用 Logic 层获取资源 + $resources = $this->logic->getUserResources($userId); + // 缓存结果 + Cache::set($cacheKey, $resources, 3600); + return $resources; } /** * 构建目录/菜单树,节点包含权限与打开方式 */ - public static function getMenuTree(int $userId): array + public function getMenuTree(int $userId): array { - $resources = self::getUserResources($userId); - $menus = $resources - ->filter(fn($r) => in_array($r->type, ['directory','menu'])) - ->values(); - return self::buildTree($menus, $resources); - } - - protected static function buildTree($nodes, $allResources, int $parentId = 0): array - { - $tree = []; - foreach ($nodes as $node) { - if ($node->parent_id !== $parentId) continue; - - $children = self::buildTree($nodes, $allResources, $node->id); - - $authList = $allResources - ->filter(fn($r) => $r->type === 'permission' && $r->parent_id === $node->id) - ->map(fn($r) => [ - 'id' => $r->id, - 'title' => $r->name, - 'auth_mark' => $r->code, - ])->values()->all(); - - $tree[] = [ - 'id' => $node->id, - 'name' => $node->name, - 'path' => $node->url, - 'meta' => [ - 'title' => $node->name, - 'icon' => $node->icon, - 'keepAlive' => true, - 'authList' => $authList, - 'openType' => $node->open_type, // 'internal' | 'external' | 'iframe' - ], - 'children' => $children, - ]; - } - return $tree; + // 调用 Logic 层处理菜单构建 + $resources = $this->getUserResources($userId); + return $this->logic->buildTree($resources); } /** * 获取用户所有权限code */ - public static function getPermissionCodes(int $userId): array + public function getPermissionCodes(int $userId): array { - return self::getUserResources($userId) - ->filter(fn($res) => $res->type === 'permission') - ->pluck('code') - ->unique() - ->values() - ->all(); + return $this->logic->getPermissionCodes($userId); } /** * 清除资源相关缓存 */ - public static function clearCache(int $userId): void + public function clearCache(int $userId): void { Cache::delete(self::cacheKey($userId)); } diff --git a/app/common/service/PermService_bak.php b/app/common/service/PermService_bak.php new file mode 100644 index 0000000..a2dcc27 --- /dev/null +++ b/app/common/service/PermService_bak.php @@ -0,0 +1,109 @@ +find($userId); + if (!$user) { + return collect(); + } + + $all = collect(); + foreach ($user->roles as $role) { + $all = $all->merge($role->resources); + } + $all = $all->merge($user->resources); + $unique = $all->unique('id')->values(); + + Cache::set($cacheKey, $unique, 3600); + return $unique; + } + + /** + * 构建目录/菜单树,节点包含权限与打开方式 + */ + public static function getMenuTree(int $userId): array + { + $resources = self::getUserResources($userId); + $menus = $resources + ->filter(fn($r) => in_array($r->type, ['directory','menu'])) + ->values(); + return self::buildTree($menus, $resources); + } + + protected static function buildTree($nodes, $allResources, int $parentId = 0): array + { + $tree = []; + foreach ($nodes as $node) { + if ($node->parent_id !== $parentId) continue; + + $children = self::buildTree($nodes, $allResources, $node->id); + + $authList = $allResources + ->filter(fn($r) => $r->type === 'permission' && $r->parent_id === $node->id) + ->map(fn($r) => [ + 'id' => $r->id, + 'title' => $r->name, + 'auth_mark' => $r->code, + ])->values()->all(); + + $tree[] = [ + 'id' => $node->id, + 'name' => $node->name, + 'path' => $node->url, + 'meta' => [ + 'title' => $node->name, + 'icon' => $node->icon, + 'keepAlive' => true, + 'authList' => $authList, + 'openType' => $node->open_type, // 'internal' | 'external' | 'iframe' + ], + 'children' => $children, + ]; + } + return $tree; + } + + /** + * 获取用户所有权限code + */ + public static function getPermissionCodes(int $userId): array + { + return self::getUserResources($userId) + ->filter(fn($res) => $res->type === 'permission') + ->pluck('code') + ->unique() + ->values() + ->all(); + } + + /** + * 清除资源相关缓存 + */ + public static function clearCache(int $userId): void + { + Cache::delete(self::cacheKey($userId)); + } +} \ No newline at end of file diff --git a/app/common/service/RoleService.php b/app/common/service/RoleService.php new file mode 100644 index 0000000..24c14cf --- /dev/null +++ b/app/common/service/RoleService.php @@ -0,0 +1,55 @@ +roleLogic = (new RoleLogic()); + } + + /** + * 获取角色列表 + */ + public function getRoleList(array $params) + { + return $this->roleLogic->getRoleList($params); + } + + /** + * 获取角色详情 + */ + public function getRoleDetail(int $roleId) + { + return $this->roleLogic->getRoleDetail($roleId); + } + + /** + * 创建角色 + */ + public function createRole(array $data) + { + return $this->roleLogic->createRole($data); + } + + /** + * 更新角色 + */ + public function updateRole(array $data) + { + return $this->roleLogic->updateRole($data); + } + + /** + * 删除角色 + */ + public function deleteRole(int $roleId) + { + return $this->roleLogic->deleteRole($roleId); + } +} diff --git a/app/common/service/UserService.php b/app/common/service/UserService.php index 99b80d5..98375bc 100644 --- a/app/common/service/UserService.php +++ b/app/common/service/UserService.php @@ -2,19 +2,39 @@ namespace app\common\service; -use app\model\User; +use app\common\logic\UserLogic; class UserService { - public static function getInfo($userId) + + protected $logic; + + public function __construct() { - $user = User::with('roles')->find($userId); - if (!$user) return null; + $this->logic = (new UserLogic()); + } + + /** + * 获取用户信息(包括菜单和权限) + */ + public function getUserInfo(int $uid) + { + $user = $this->logic->userInfo($uid); + $permService = new PermService(); + $menuTree = $permService->getMenuTree($uid); + $permissionCodes = $permService->getPermissionCodes($uid); return [ - 'id' => $user->id, - 'username' => $user->username, - 'roles' => $user->roles->pluck('name')->toArray(), - // 可扩展其他字段 + 'user' => [ + 'id' => $user->id, + 'name' => $user->name, + 'avatar' => $user->avatar, + 'phone' => $user->phone, + 'email' => $user->email, + 'status' => $user->status, + 'roles' => $user->roles->pluck('name') + ], + 'menu' => $menuTree, + 'permissions' => $permissionCodes ]; } -} +} \ No newline at end of file diff --git a/app/common/traits/DataTransformer.php b/app/common/traits/DataTransformer.php new file mode 100644 index 0000000..42faf81 --- /dev/null +++ b/app/common/traits/DataTransformer.php @@ -0,0 +1,135 @@ + $field; + + if ($rawData instanceof LengthAwarePaginator) { + $formatted = $this->formatPaginated($rawData, $postProcessor, $fieldFormatter); + } elseif ($rawData instanceof Model) { + $formatted = $this->formatModel($rawData, $fieldFormatter); + } elseif ($rawData instanceof Collection) { + $formatted = $this->formatCollection($rawData, $fieldFormatter); + } elseif (is_array($rawData)) { + $formatted = $this->formatArray($rawData, $fieldFormatter); + } else { + $formatted = (array) $rawData; + } + + if (is_callable($postProcessor)) { + $formatted = $postProcessor($formatted); + } + + return $formatted; + } catch (\Throwable $e) { + return [ + 'error' => true, + 'message' => $e->getMessage(), + 'code' => $e->getCode(), + ]; + } + } + + /** + * 格式化分页数据:将每条记录也进行 transformData 递归处理 + * + * @param LengthAwarePaginator $paginator + * @param callable|null $postProcessor + * @param callable $fieldFormatter + * @return array + */ + protected function formatPaginated( + LengthAwarePaginator $paginator, + ?callable $postProcessor, + callable $fieldFormatter + ): array { + $list = $paginator->getCollection()->map(function ($item) use ($postProcessor, $fieldFormatter) { + return $this->transformData($item, $postProcessor, $fieldFormatter); + })->toArray(); + + return [ + 'records' => $list, + 'total' => $paginator->total(), + 'current' => $paginator->currentPage(), + 'size' => $paginator->perPage() + ]; + } + + /** + * 格式化 Eloquent 模型:递归嵌套关联 + * + * @param Model $model + * @param callable $fieldFormatter + * @return array + */ + protected function formatModel(Model $model, callable $fieldFormatter): array + { + $formatted = []; + + // 主表字段 + foreach ($model->getAttributes() as $key => $value) { + $formatted[$fieldFormatter($key)] = $value; + } + + // 关联数据,递归嵌套 + foreach ($model->getRelations() as $relation => $relationData) { + if ($relationData instanceof Model) { + // 单个关联模型 + $formatted[$relation] = $this->formatModel($relationData, $fieldFormatter); + } elseif ($relationData instanceof Collection) { + // 关联集合 + $formatted[$relation] = $relationData + ->map(fn($item) => $this->transformData($item, null, $fieldFormatter)) + ->toArray(); + } + } + + return $formatted; + } + + /** + * 格式化集合 + * + * @param Collection $collection + * @param callable $fieldFormatter + * @return array + */ + protected function formatCollection(Collection $collection, callable $fieldFormatter): array + { + return $collection->map(fn($item) => $this->transformData($item, null, $fieldFormatter)) + ->toArray(); + } + + /** + * 格式化普通数组 + * + * @param array $array + * @param callable $fieldFormatter + * @return array + */ + protected function formatArray(array $array, callable $fieldFormatter): array + { + return array_map(fn($item) => $this->transformData($item, null, $fieldFormatter), $array); + } +} \ No newline at end of file diff --git a/app/common/validate/RoleValidate.php b/app/common/validate/RoleValidate.php new file mode 100644 index 0000000..330fae6 --- /dev/null +++ b/app/common/validate/RoleValidate.php @@ -0,0 +1,32 @@ + 'require|integer', + 'system_id' => 'require|integer', + 'name' => 'require|max:64', + 'desc' => 'max:128', + 'sort' => 'integer', + 'status' => 'in:0,1', + ]; + + protected $message = [ + 'id.require' => '角色ID不能为空', + 'system_id.require' => '系统ID不能为空', + 'name.require' => '角色名称不能为空', + 'name.max' => '角色名称不能超过64个字符', + 'desc.max' => '描述不能超过128个字符', + 'sort.integer' => '排序为数字', + 'status.in' => '状态值必须为0或1', + ]; + + protected $scene = [ + 'role_create' => ['system_id', 'name', 'desc', 'sort'], + 'role_update' => ['id', 'system_id', 'name', 'desc', 'sort', 'status'], + ]; +} diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index 2ea3432..f0feb2c 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -6,6 +6,8 @@ use support\Request; class IndexController { + + protected $noNeedLogin = ['index', 'view']; public function index(Request $request) { diff --git a/app/functions.php b/app/functions.php index 14d5e4a..2a05e3f 100644 --- a/app/functions.php +++ b/app/functions.php @@ -2,8 +2,6 @@ /** * Here is your custom functions. */ - -use support\exception\BusinessException; /** * 标准化账号参数 @@ -11,54 +9,69 @@ use support\exception\BusinessException; * @param string $account 用户输入的账号(可能是手机号或邮箱) * @return array 返回 ['phone' => $account] 或 ['email' => $account] */ -function normalizeAccount(string $account): array -{ - // 使用FILTER_VALIDATE_EMAIL验证邮箱格式 - if (filter_var($account, FILTER_VALIDATE_EMAIL)) { - return ['email' => $account]; - } - - // 清理手机号中的非数字字符 - $cleanPhone = preg_replace('/[^0-9]/', '', $account); - - // 验证手机号格式(简单版:11位数字) - if (preg_match('/^1[3-9]\d{9}$/', $cleanPhone)) { +if (!function_exists('normalizeAccount')) { + function normalizeAccount(string $account): array + { + // 使用FILTER_VALIDATE_EMAIL验证邮箱格式 + if (filter_var($account, FILTER_VALIDATE_EMAIL)) { + return ['email' => $account]; + } + + // 清理手机号中的非数字字符 + $cleanPhone = preg_replace('/[^0-9]/', '', $account); + + // 验证手机号格式(简单版:11位数字) + if (preg_match('/^1[3-9]\d{9}$/', $cleanPhone)) { + return ['phone' => $cleanPhone]; + } + + // 如果既不是邮箱也不是手机号,根据业务需求处理 + // 这里默认按手机号处理(可根据实际需求调整) return ['phone' => $cleanPhone]; } - - // 如果既不是邮箱也不是手机号,根据业务需求处理 - // 这里默认按手机号处理(可根据实际需求调整) - return ['phone' => $cleanPhone]; } /** * 公共html页面 */ -function htmlpage() -{ - static $html; - if (!$html) { - $html = file_get_contents(public_path('mzl.html')); +if (!function_exists('htmlpage')) { + function htmlpage() + { + static $html; + if (!$html) { + $html = file_get_contents(public_path('mzl.html')); + } + return $html; } - return $html; } - /** * 获取配置 */ -function getConfig($filePath = null) -{ - // 构建文件路径 - $config_file = app_path($filePath); - // 检查文件是否存在 - if (!file_exists($config_file)) { - return []; +if (!function_exists('getConfig')) { + function getConfig($filePath = null) + { + // 构建文件路径 + $config_file = app_path($filePath); + // 检查文件是否存在 + if (!file_exists($config_file)) { + return []; + } + // 包含文件并获取返回的数组 + $config = include $config_file; + // 验证是否为数组 + if (!is_array($config)) { + return []; + } + return $config; } - // 包含文件并获取返回的数组 - $config = include $config_file; - // 验证是否为数组 - if (!is_array($config)) { - return []; +} + +/* + * 调试用 + */ +if (!function_exists('dpr')) { + function dpr(\Throwable $e) + { + var_dump("=== === 报错打印 === ===", "文件:".$e->getFile(), "行数:".$e->getLine(), "详情:".$e->getMessage(), "=== === 报错打印 === ==="); } - return $config; } \ No newline at end of file diff --git a/app/middleware/AuthCheck.php b/app/middleware/AuthCheck.php index 3e6eb8a..c085901 100644 --- a/app/middleware/AuthCheck.php +++ b/app/middleware/AuthCheck.php @@ -19,7 +19,7 @@ class AuthCheck implements MiddlewareInterface return $handler($request); } else { try { - $authHeader = $request->header('Authorization'); + $authHeader = $request->header('Fdesk-Auth-Token'); if (!$authHeader || !preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) { $body = [ 'status' => 'fail', diff --git a/app/view/index/qrcode.html b/app/view/index/qrcode.html index 26d1918..fb6f11c 100644 --- a/app/view/index/qrcode.html +++ b/app/view/index/qrcode.html @@ -16,7 +16,7 @@ // const REDIRECT_URI = encodeURIComponent('https://example.com/api/oauth/callback/feishu'); // const STATE = '随机串_防CSRF'; // // 用于跳转获取 code 的 URL - const GOTO = `https://passport.feishu.cn/suite/passport/oauth/authorize?client_id=cli_a73e1e6406fa501c&redirect_uri=https://mzl.dev.hnxinyouhe.com/common/auth/feishu/auth&response_type=code&state=MZL`; + const GOTO = `https://passport.feishu.cn/suite/passport/oauth/authorize?client_id=cli_a72f13d6c022500d&redirect_uri=https://mzl.dev.hnxinyouhe.com/common/auth/feishu/auth&response_type=code&state=MZL`; // const GOTO = `https://open.feishu.cn/open-apis/authen/v1/authorize?app_id=cli_a73e1e6406fa501c&redirect_uri=https://mzl.dev.hnxinyouhe.com/common/auth/feishu/auth&scope=contact:user.base:readonly contact:contact.base:readonly contact:user.employee_id:readonly im:message:send_as_bot&state=xinyouhe`; // 2. 创建二维码实例 const QRLoginObj = QRLogin({ diff --git a/composer.json b/composer.json index ccf259e..e4e63bb 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,7 @@ "topthink/think-validate": "^3.0", "shopwwi/webman-filesystem": "1.1", "shopwwi/filesystem-oss": "^1.0", - "webman/redis-queue": "^1.3", + "webman/redis-queue": "^2.1", "symfony/translation": "^7.3" }, "suggest": { diff --git a/composer.lock b/composer.lock index 01843a4..b9b58c7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "acb636fcb8774558a04bd674f9f95f55", + "content-hash": "30cc3cbb162022972a21efed9b69d1e1", "packages": [ { "name": "aliyuncs/oss-sdk-php", @@ -6155,26 +6155,23 @@ }, { "name": "webman/redis-queue", - "version": "v1.3.1", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/webman-php/redis-queue.git", - "reference": "6fd491b19acb006d60931724aa2577a046ccb612" + "reference": "12e1c9915ec64c91c0c9c255606bd0c7de27334c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webman-php/redis-queue/zipball/6fd491b19acb006d60931724aa2577a046ccb612", - "reference": "6fd491b19acb006d60931724aa2577a046ccb612", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/webman-php/redis-queue/zipball/12e1c9915ec64c91c0c9c255606bd0c7de27334c", + "reference": "12e1c9915ec64c91c0c9c255606bd0c7de27334c", + "shasum": "" }, "require": { - "workerman/redis-queue": "^1.0" + "ext-redis": "*", + "php": ">=8.1", + "workerman/redis-queue": "^1.2", + "workerman/webman-framework": "^2.1 || dev-master" }, "type": "library", "autoload": { @@ -6186,9 +6183,9 @@ "description": "Redis message queue plugin for webman.", "support": { "issues": "https://github.com/webman-php/redis-queue/issues", - "source": "https://github.com/webman-php/redis-queue/tree/v1.3.1" + "source": "https://github.com/webman-php/redis-queue/tree/v2.1.0" }, - "time": "2024-02-28T07:21:52+00:00" + "time": "2025-01-24T12:56:01+00:00" }, { "name": "workerman/coroutine", diff --git a/config/middleware.php b/config/middleware.php index cf0335a..ae0ad6e 100644 --- a/config/middleware.php +++ b/config/middleware.php @@ -16,6 +16,7 @@ return [ '' => [ \app\middleware\AccessControl::class, - \app\middleware\BasicData::class + \app\middleware\BasicData::class, + \app\middleware\AuthCheck::class ] ]; \ No newline at end of file diff --git a/config/route.php b/config/route.php index db2c831..41c3dff 100644 --- a/config/route.php +++ b/config/route.php @@ -31,12 +31,26 @@ Route::group('/common', function () { Route::any('/login', [app\common\controller\AuthController::class, 'login']); Route::any('/register', [app\common\controller\AuthController::class, 'register']); Route::any('/feishu/scope', [app\common\controller\AuthController::class, 'feishuScope']); + Route::any('/refresh/token', [app\common\controller\AuthController::class, 'refresh']); - - // 调试 + // // 调试 Route::any('/feishu/auth', [app\common\controller\AuthController::class, 'feishuAuth']); }); + + Route::group('/user', function () { + Route::any('/info', [app\common\controller\UserController::class, 'info']); + }); +}); + +Route::group('/admin', function () { + Route::group('/role', function () { + Route::any('/list', [app\admin\controller\RoleController::class, 'list']); + Route::any('/detail', [app\admin\controller\RoleController::class, 'detail']); + Route::any('/create', [app\admin\controller\RoleController::class, 'create']); + Route::any('/update', [app\admin\controller\RoleController::class, 'update']); + Route::any('/delete', [app\admin\controller\RoleController::class, 'delete']); + }); }); diff --git a/config/whitelist.php b/config/whitelist.php index 8071847..d1669b3 100644 --- a/config/whitelist.php +++ b/config/whitelist.php @@ -4,6 +4,6 @@ return [ 'basi_cdata' => [ '/', '/index/index', - // '/index/view' + '/index/view' ] ]; \ No newline at end of file