package admin import ( "context" "errors" "github.com/duke-git/lancet/v2/convertor" v1 "github.com/go-nunu/nunu-layout-advanced/api/v1" "github.com/go-nunu/nunu-layout-advanced/internal/model" "github.com/go-nunu/nunu-layout-advanced/internal/repository/admin" "github.com/go-nunu/nunu-layout-advanced/internal/service" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" "sort" "strings" "time" ) type AdminService interface { Login(ctx context.Context, req *v1.LoginRequest) (string, error) GetAdminUsers(ctx context.Context, req *v1.GetAdminUsersRequest) (*v1.GetAdminUsersResponseData, error) GetAdminUser(ctx context.Context, uid uint) (*v1.GetAdminUserResponseData, error) AdminUserUpdate(ctx context.Context, req *v1.AdminUserUpdateRequest) error AdminUserCreate(ctx context.Context, req *v1.AdminUserCreateRequest) error AdminUserDelete(ctx context.Context, id uint) error GetUserPermissions(ctx context.Context, uid uint) (*v1.GetUserPermissionsData, error) GetRolePermissions(ctx context.Context, role string) (*v1.GetRolePermissionsData, error) UpdateRolePermission(ctx context.Context, req *v1.UpdateRolePermissionRequest) error GetAdminMenus(ctx context.Context) (*v1.GetMenuResponseData, error) GetMenus(ctx context.Context, uid uint) (*v1.GetMenuResponseData, error) MenuUpdate(ctx context.Context, req *v1.MenuUpdateRequest) error MenuCreate(ctx context.Context, req *v1.MenuCreateRequest) error MenuDelete(ctx context.Context, id uint) error GetRoles(ctx context.Context, req *v1.GetRoleListRequest) (*v1.GetRolesResponseData, error) RoleUpdate(ctx context.Context, req *v1.RoleUpdateRequest) error RoleCreate(ctx context.Context, req *v1.RoleCreateRequest) error RoleDelete(ctx context.Context, id uint) error GetApis(ctx context.Context, req *v1.GetApisRequest) (*v1.GetApisResponseData, error) ApiUpdate(ctx context.Context, req *v1.ApiUpdateRequest) error ApiCreate(ctx context.Context, req *v1.ApiCreateRequest) error ApiDelete(ctx context.Context, id uint) error } func NewAdminService( service *service.Service, adminRepository admin.AdminRepository, ) AdminService { return &adminService{ Service: service, adminRepository: adminRepository, } } type adminService struct { *service.Service adminRepository admin.AdminRepository } func (s *adminService) GetAdminUser(ctx context.Context, uid uint) (*v1.GetAdminUserResponseData, error) { user, err := s.adminRepository.GetAdminUser(ctx, uid) if err != nil { return nil, err } roles, _ := s.adminRepository.GetUserRoles(ctx, uid) return &v1.GetAdminUserResponseData{ Email: user.Email, ID: user.ID, Username: user.Username, Nickname: user.Nickname, Phone: user.Phone, Roles: roles, CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"), UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"), }, nil } func (s *adminService) Login(ctx context.Context, req *v1.LoginRequest) (string, error) { user, err := s.adminRepository.GetAdminUserByUsername(ctx, req.Username) if err != nil { if err == gorm.ErrRecordNotFound { return "", v1.ErrUnauthorized } return "", v1.ErrInternalServerError } err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)) if err != nil { return "", err } token, err := s.Jwt.GenToken(user.ID, time.Now().Add(time.Hour*24*1)) if err != nil { return "", err } return token, nil } func (s *adminService) GetAdminUsers(ctx context.Context, req *v1.GetAdminUsersRequest) (*v1.GetAdminUsersResponseData, error) { list, total, err := s.adminRepository.GetAdminUsers(ctx, req) if err != nil { return nil, err } data := &v1.GetAdminUsersResponseData{ List: make([]v1.AdminUserDataItem, 0), Total: total, } for _, user := range list { roles, err := s.adminRepository.GetUserRoles(ctx, user.ID) if err != nil { s.Logger.Error("GetUserRoles error", zap.Error(err)) continue } data.List = append(data.List, v1.AdminUserDataItem{ Email: user.Email, ID: user.ID, Nickname: user.Nickname, Username: user.Username, Phone: user.Phone, Roles: roles, CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"), UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"), }) } return data, nil } func (s *adminService) AdminUserUpdate(ctx context.Context, req *v1.AdminUserUpdateRequest) error { old, _ := s.adminRepository.GetAdminUser(ctx, req.ID) if req.Password != "" { hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { return err } req.Password = string(hash) } else { req.Password = old.Password } err := s.adminRepository.UpdateUserRoles(ctx, req.ID, req.Roles) if err != nil { return err } return s.adminRepository.AdminUserUpdate(ctx, &model.AdminUser{ Model: gorm.Model{ ID: req.ID, }, Email: req.Email, Password: req.Password, Nickname: req.Nickname, Phone: req.Phone, Username: req.Username, }) } func (s *adminService) AdminUserCreate(ctx context.Context, req *v1.AdminUserCreateRequest) error { hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { return err } req.Password = string(hash) err = s.adminRepository.AdminUserCreate(ctx, &model.AdminUser{ Email: req.Email, Nickname: req.Nickname, Phone: req.Phone, Username: req.Username, Password: req.Password, }) if err != nil { return err } user, err := s.adminRepository.GetAdminUserByUsername(ctx, req.Username) if err != nil { return err } err = s.adminRepository.UpdateUserRoles(ctx, user.ID, req.Roles) if err != nil { return err } return err } func (s *adminService) AdminUserDelete(ctx context.Context, id uint) error { // 删除用户角色 err := s.adminRepository.DeleteUserRoles(ctx, id) if err != nil { return err } return s.adminRepository.AdminUserDelete(ctx, id) } func (s *adminService) UpdateRolePermission(ctx context.Context, req *v1.UpdateRolePermissionRequest) error { permissions := map[string]struct{}{} for _, v := range req.List { perm := strings.Split(v, model.PermSep) if len(perm) == 2 { permissions[v] = struct{}{} } } return s.adminRepository.UpdateRolePermission(ctx, req.Role, permissions) } func (s *adminService) GetApis(ctx context.Context, req *v1.GetApisRequest) (*v1.GetApisResponseData, error) { list, total, err := s.adminRepository.GetApis(ctx, req) if err != nil { return nil, err } groups, err := s.adminRepository.GetApiGroups(ctx) if err != nil { return nil, err } data := &v1.GetApisResponseData{ List: make([]v1.ApiDataItem, 0), Total: total, Groups: groups, } for _, api := range list { data.List = append(data.List, v1.ApiDataItem{ CreatedAt: api.CreatedAt.Format("2006-01-02 15:04:05"), Group: api.Group, ID: api.ID, Method: api.Method, Name: api.Name, Path: api.Path, UpdatedAt: api.UpdatedAt.Format("2006-01-02 15:04:05"), }) } return data, nil } func (s *adminService) ApiUpdate(ctx context.Context, req *v1.ApiUpdateRequest) error { return s.adminRepository.ApiUpdate(ctx, &model.Api{ Group: req.Group, Method: req.Method, Name: req.Name, Path: req.Path, Model: gorm.Model{ ID: req.ID, }, }) } func (s *adminService) ApiCreate(ctx context.Context, req *v1.ApiCreateRequest) error { return s.adminRepository.ApiCreate(ctx, &model.Api{ Group: req.Group, Method: req.Method, Name: req.Name, Path: req.Path, }) } func (s *adminService) ApiDelete(ctx context.Context, id uint) error { return s.adminRepository.ApiDelete(ctx, id) } func (s *adminService) GetUserPermissions(ctx context.Context, uid uint) (*v1.GetUserPermissionsData, error) { data := &v1.GetUserPermissionsData{ List: []string{}, } list, err := s.adminRepository.GetUserPermissions(ctx, uid) if err != nil { return nil, err } for _, v := range list { if len(v) == 3 { data.List = append(data.List, strings.Join([]string{v[1], v[2]}, model.PermSep)) } } return data, nil } func (s *adminService) GetRolePermissions(ctx context.Context, role string) (*v1.GetRolePermissionsData, error) { data := &v1.GetRolePermissionsData{ List: []string{}, } list, err := s.adminRepository.GetRolePermissions(ctx, role) if err != nil { return nil, err } for _, v := range list { if len(v) == 3 { data.List = append(data.List, strings.Join([]string{v[1], v[2]}, model.PermSep)) } } return data, nil } func (s *adminService) MenuUpdate(ctx context.Context, req *v1.MenuUpdateRequest) error { return s.adminRepository.MenuUpdate(ctx, &model.Menu{ Component: req.Component, Icon: req.Icon, KeepAlive: req.KeepAlive, HideInMenu: req.HideInMenu, Locale: req.Locale, Weight: req.Weight, Name: req.Name, ParentID: req.ParentID, Path: req.Path, Redirect: req.Redirect, Title: req.Title, URL: req.URL, Model: gorm.Model{ ID: req.ID, }, }) } func (s *adminService) MenuCreate(ctx context.Context, req *v1.MenuCreateRequest) error { return s.adminRepository.MenuCreate(ctx, &model.Menu{ Component: req.Component, Icon: req.Icon, KeepAlive: req.KeepAlive, HideInMenu: req.HideInMenu, Locale: req.Locale, Weight: req.Weight, Name: req.Name, ParentID: req.ParentID, Path: req.Path, Redirect: req.Redirect, Title: req.Title, URL: req.URL, }) } func (s *adminService) MenuDelete(ctx context.Context, id uint) error { return s.adminRepository.MenuDelete(ctx, id) } func (s *adminService) GetMenus(ctx context.Context, uid uint) (*v1.GetMenuResponseData, error) { // 步骤 1: 从数据库中一次性获取所有菜单项。 // 建议在这里就进行初步排序,确保基础顺序 menuList, err := s.adminRepository.GetMenuList(ctx) // 最好让此方法返回按 Weight, ID 排序后的结果 if err != nil { s.Logger.WithContext(ctx).Error("GetMenuList error", zap.Error(err)) return nil, err } // 步骤 2: 获取当前用户的完整权限列表。 permissions, err := s.adminRepository.GetUserPermissions(ctx, uid) if err != nil { return nil, err } // 步骤 3: 创建一个以菜单ID为键、菜单对象指针为值的映射(Map),用于快速查找。 allMenusMap := make(map[uint]*model.Menu) for i := range menuList { allMenusMap[menuList[i].ID] = &menuList[i] } // 步骤 4: 创建一个集合(Set),存放用户被明确授权可以访问的菜单路径。 permittedMenuPaths := make(map[string]struct{}) isSuperAdmin := convertor.ToString(uid) == model.AdminUserID // 提前判断是否为超级管理员 for _, permission := range permissions { // 超级管理员拥有所有菜单权限,或普通用户拥有特定菜单权限 if isSuperAdmin || (len(permission) == 3 && strings.HasPrefix(permission[1], model.MenuResourcePrefix)) { // 移除菜单资源特有的前缀,得到干净的菜单路径。 permittedMenuPaths[strings.TrimPrefix(permission[1], model.MenuResourcePrefix)] = struct{}{} } } // 步骤 5: 构建最终的菜单列表,核心逻辑是确保所有有权限访问的子菜单,其父菜单也一定会被包含进来。 finalMenusMap := make(map[uint]v1.MenuDataItem) for _, menu := range menuList { if _, ok := permittedMenuPaths[menu.Path]; ok { curr := &menu for curr != nil { if _, exists := finalMenusMap[curr.ID]; exists { break } finalMenusMap[curr.ID] = v1.MenuDataItem{ ID: curr.ID, Name: curr.Name, Title: curr.Title, Path: curr.Path, Component: curr.Component, Redirect: curr.Redirect, KeepAlive: curr.KeepAlive, HideInMenu: curr.HideInMenu, Locale: curr.Locale, Weight: curr.Weight, Icon: curr.Icon, ParentID: curr.ParentID, UpdatedAt: curr.UpdatedAt.Format("2006-01-02 15:04:05"), URL: curr.URL, } if curr.ParentID > 0 { curr = allMenusMap[curr.ParentID] } else { curr = nil } } } } // 步骤 6: 将Map形式的最终结果转换为列表(Slice)。 list := make([]v1.MenuDataItem, 0, len(finalMenusMap)) for _, menuItem := range finalMenusMap { list = append(list, menuItem) } // ================= FIX STARTS HERE ================= // 步骤 7: 对菜单列表进行排序,确保每次返回的顺序一致。 // 排序规则: // 1. 主要按权重 (Weight) 升序排列。 // 2. 如果权重相同,则按 ID 升序排列,作为稳定的次要排序,确保顺序唯一。 sort.Slice(list, func(i, j int) bool { // 如果权重不同,按权重比较 if list[i].Weight != list[j].Weight { return list[i].Weight > list[j].Weight } // 如果权重相同,按ID比较,保证稳定排序 return list[i].ID < list[j].ID }) // ================= FIX ENDS HERE =================== data := &v1.GetMenuResponseData{ List: list, } return data, nil } func (s *adminService) GetAdminMenus(ctx context.Context) (*v1.GetMenuResponseData, error) { menuList, err := s.adminRepository.GetMenuList(ctx) if err != nil { s.Logger.WithContext(ctx).Error("GetMenuList error", zap.Error(err)) return nil, err } data := &v1.GetMenuResponseData{ List: make([]v1.MenuDataItem, 0), } for _, menu := range menuList { data.List = append(data.List, v1.MenuDataItem{ ID: menu.ID, Name: menu.Name, Title: menu.Title, Path: menu.Path, Component: menu.Component, Redirect: menu.Redirect, KeepAlive: menu.KeepAlive, HideInMenu: menu.HideInMenu, Locale: menu.Locale, Weight: menu.Weight, Icon: menu.Icon, ParentID: menu.ParentID, UpdatedAt: menu.UpdatedAt.Format("2006-01-02 15:04:05"), URL: menu.URL, }) } return data, nil } func (s *adminService) RoleUpdate(ctx context.Context, req *v1.RoleUpdateRequest) error { return s.adminRepository.RoleUpdate(ctx, &model.Role{ Name: req.Name, Sid: req.Sid, Model: gorm.Model{ ID: req.ID, }, }) } func (s *adminService) RoleCreate(ctx context.Context, req *v1.RoleCreateRequest) error { _, err := s.adminRepository.GetRoleBySid(ctx, req.Sid) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return s.adminRepository.RoleCreate(ctx, &model.Role{ Name: req.Name, Sid: req.Sid, }) } else { return err } } return nil } func (s *adminService) RoleDelete(ctx context.Context, id uint) error { old, err := s.adminRepository.GetRole(ctx, id) if err != nil { return err } if err := s.adminRepository.CasbinRoleDelete(ctx, old.Sid); err != nil { return err } return s.adminRepository.RoleDelete(ctx, id) } func (s *adminService) GetRoles(ctx context.Context, req *v1.GetRoleListRequest) (*v1.GetRolesResponseData, error) { list, total, err := s.adminRepository.GetRoles(ctx, req) if err != nil { return nil, err } data := &v1.GetRolesResponseData{ List: make([]v1.RoleDataItem, 0), Total: total, } for _, role := range list { data.List = append(data.List, v1.RoleDataItem{ ID: role.ID, Name: role.Name, Sid: role.Sid, UpdatedAt: role.UpdatedAt.Format("2006-01-02 15:04:05"), CreatedAt: role.CreatedAt.Format("2006-01-02 15:04:05"), }) } return data, nil }