package service import ( "context" "sort" "github.com/gogf/gf/v2/errors/gerror" "golang.org/x/crypto/bcrypt" "service.xpcool.com/internal/consts" "service.xpcool.com/internal/dao" "service.xpcool.com/internal/library/response" "service.xpcool.com/internal/model/do" "service.xpcool.com/internal/model/dto" "service.xpcool.com/internal/model/entity" ) // IAdminManage manages administrators. type IAdminManage interface { List(context.Context, dto.PageQuery) ([]*dto.AdminItem, int, error) Create(context.Context, dto.AdminCreateInput) (uint64, error) Update(context.Context, dto.AdminUpdateInput) error ResetPassword(context.Context, uint64, string) error Delete(context.Context, uint64) error } // IRoleManage manages roles and their menu bindings. type IRoleManage interface { List(context.Context, dto.PageQuery) ([]*dto.RoleItem, int, error) Create(context.Context, dto.RoleCreateInput) (uint64, error) Update(context.Context, dto.RoleUpdateInput) error Delete(context.Context, uint64) error } // IMenuManage manages the menu tree (menus + button permissions). type IMenuManage interface { Tree(context.Context) ([]*dto.MenuNode, error) Create(context.Context, dto.MenuCreateInput) (uint64, error) Update(context.Context, dto.MenuUpdateInput) error Delete(context.Context, uint64) error } type adminManage struct{} type roleManage struct{} type menuManage struct{} func NewAdminManage() IAdminManage { return &adminManage{} } func NewRoleManage() IRoleManage { return &roleManage{} } func NewMenuManage() IMenuManage { return &menuManage{} } var ( localAdminManage IAdminManage localRoleManage IRoleManage localMenuManage IMenuManage ) func AdminManage() IAdminManage { if localAdminManage == nil { panic("AdminManage implementation not registered") } return localAdminManage } func RegisterAdminManage(i IAdminManage) { localAdminManage = i } func RoleManage() IRoleManage { if localRoleManage == nil { panic("RoleManage implementation not registered") } return localRoleManage } func RegisterRoleManage(i IRoleManage) { localRoleManage = i } func MenuManage() IMenuManage { if localMenuManage == nil { panic("MenuManage implementation not registered") } return localMenuManage } func RegisterMenuManage(i IMenuManage) { localMenuManage = i } // ---------------------- Admin manage ---------------------- func (s *adminManage) List(ctx context.Context, q dto.PageQuery) ([]*dto.AdminItem, int, error) { total, err := dao.AdminUser.Ctx(ctx).Count() if err != nil { return nil, 0, gerror.Wrap(err, "count admins") } if total == 0 { return nil, 0, nil } var list []entity.AdminUser if err = dao.AdminUser.Ctx(ctx).Page(q.Page, q.Size).OrderDesc("id").Scan(&list); err != nil { return nil, 0, gerror.Wrap(err, "query admins") } items := make([]*dto.AdminItem, 0, len(list)) for i := range list { a := &list[i] roleIds, roleNames, err := s.roles(ctx, a.Id) if err != nil { return nil, 0, err } items = append(items, &dto.AdminItem{ Id: a.Id, Username: a.Username, Nickname: a.Nickname, Status: a.Status, RoleIds: roleIds, RoleNames: roleNames, CreatedAt: a.CreatedAt.Layout("2006-01-02 15:04:05"), }) } return items, total, nil } func (s *adminManage) roles(ctx context.Context, adminID uint64) ([]uint64, []string, error) { var rels []entity.AdminUserRole if err := dao.AdminUserRole.Ctx(ctx).Where(do.AdminUserRole{AdminUserId: adminID}).Scan(&rels); err != nil { return nil, nil, gerror.Wrap(err, "query admin roles") } roleIds := make([]uint64, 0, len(rels)) for _, r := range rels { roleIds = append(roleIds, r.RoleId) } if len(roleIds) == 0 { return roleIds, nil, nil } var roles []entity.AdminRole if err := dao.AdminRole.Ctx(ctx).WhereIn("id", roleIds).Scan(&roles); err != nil { return nil, nil, gerror.Wrap(err, "query roles") } names := make([]string, 0, len(roles)) for _, r := range roles { names = append(names, r.Name) } return roleIds, names, nil } func (s *adminManage) Create(ctx context.Context, in dto.AdminCreateInput) (uint64, error) { count, err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Username: in.Username}).Count() if err != nil { return 0, gerror.Wrap(err, "check username") } if count > 0 { return 0, response.Error(consts.CodeInvalidParam, "username already exists") } hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost) if err != nil { return 0, gerror.Wrap(err, "hash password") } id, err := dao.AdminUser.Ctx(ctx).Data(do.AdminUser{ Username: in.Username, PasswordHash: string(hash), Nickname: in.Nickname, Status: 1, }).InsertAndGetId() if err != nil { return 0, gerror.Wrap(err, "insert admin") } if err = bindAdminRoles(ctx, uint64(id), in.RoleIds); err != nil { return 0, err } return uint64(id), nil } func (s *adminManage) Update(ctx context.Context, in dto.AdminUpdateInput) error { data := do.AdminUser{Nickname: in.Nickname, Status: in.Status} if _, err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: in.Id}).Data(data).Update(); err != nil { return gerror.Wrap(err, "update admin") } return bindAdminRoles(ctx, in.Id, in.RoleIds) } func (s *adminManage) ResetPassword(ctx context.Context, id uint64, password string) error { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return gerror.Wrap(err, "hash password") } if _, err = dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: id}).Data(do.AdminUser{PasswordHash: string(hash)}).Update(); err != nil { return gerror.Wrap(err, "reset password") } return nil } func (s *adminManage) Delete(ctx context.Context, id uint64) error { if _, err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: id}).Delete(); err != nil { return gerror.Wrap(err, "delete admin") } // 关联关系物理删除,避免唯一键残留。 if _, err := dao.AdminUserRole.Ctx(ctx).Unscoped().Where(do.AdminUserRole{AdminUserId: id}).Delete(); err != nil { return gerror.Wrap(err, "delete admin role bindings") } return nil } // bindAdminRoles 全量重绑管理员-角色关系。 func bindAdminRoles(ctx context.Context, adminID uint64, roleIds []uint64) error { if _, err := dao.AdminUserRole.Ctx(ctx).Unscoped().Where(do.AdminUserRole{AdminUserId: adminID}).Delete(); err != nil { return gerror.Wrap(err, "clear admin roles") } for _, rid := range roleIds { if _, err := dao.AdminUserRole.Ctx(ctx).Data(do.AdminUserRole{AdminUserId: adminID, RoleId: rid}).Insert(); err != nil { return gerror.Wrap(err, "bind admin role") } } return nil } // ---------------------- Role manage ---------------------- func (s *roleManage) List(ctx context.Context, q dto.PageQuery) ([]*dto.RoleItem, int, error) { total, err := dao.AdminRole.Ctx(ctx).Count() if err != nil { return nil, 0, gerror.Wrap(err, "count roles") } if total == 0 { return nil, 0, nil } var list []entity.AdminRole if err = dao.AdminRole.Ctx(ctx).Page(q.Page, q.Size).OrderAsc("id").Scan(&list); err != nil { return nil, 0, gerror.Wrap(err, "query roles") } items := make([]*dto.RoleItem, 0, len(list)) for i := range list { r := &list[i] menuIds, err := roleMenuIDs(ctx, r.Id) if err != nil { return nil, 0, err } items = append(items, &dto.RoleItem{ Id: r.Id, Code: r.Code, Name: r.Name, Status: r.Status, MenuIds: menuIds, CreatedAt: r.CreatedAt.Layout("2006-01-02 15:04:05"), }) } return items, total, nil } func (s *roleManage) Create(ctx context.Context, in dto.RoleCreateInput) (uint64, error) { count, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Code: in.Code}).Count() if err != nil { return 0, gerror.Wrap(err, "check role code") } if count > 0 { return 0, response.Error(consts.CodeInvalidParam, "role code already exists") } id, err := dao.AdminRole.Ctx(ctx).Data(do.AdminRole{Code: in.Code, Name: in.Name, Status: in.Status}).InsertAndGetId() if err != nil { return 0, gerror.Wrap(err, "insert role") } if err = bindRoleMenus(ctx, uint64(id), in.MenuIds); err != nil { return 0, err } return uint64(id), nil } func (s *roleManage) Update(ctx context.Context, in dto.RoleUpdateInput) error { if _, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Id: in.Id}).Data(do.AdminRole{Name: in.Name, Status: in.Status}).Update(); err != nil { return gerror.Wrap(err, "update role") } return bindRoleMenus(ctx, in.Id, in.MenuIds) } func (s *roleManage) Delete(ctx context.Context, id uint64) error { if _, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Id: id}).Delete(); err != nil { return gerror.Wrap(err, "delete role") } if _, err := dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{RoleId: id}).Delete(); err != nil { return gerror.Wrap(err, "delete role menu bindings") } if _, err := dao.AdminUserRole.Ctx(ctx).Unscoped().Where(do.AdminUserRole{RoleId: id}).Delete(); err != nil { return gerror.Wrap(err, "delete user role bindings") } return nil } func roleMenuIDs(ctx context.Context, roleID uint64) ([]uint64, error) { var rels []entity.AdminRoleMenu if err := dao.AdminRoleMenu.Ctx(ctx).Where(do.AdminRoleMenu{RoleId: roleID}).Scan(&rels); err != nil { return nil, gerror.Wrap(err, "query role menus") } ids := make([]uint64, 0, len(rels)) for _, r := range rels { ids = append(ids, r.MenuId) } return ids, nil } // bindRoleMenus 全量重绑角色-菜单关系。 func bindRoleMenus(ctx context.Context, roleID uint64, menuIds []uint64) error { if _, err := dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{RoleId: roleID}).Delete(); err != nil { return gerror.Wrap(err, "clear role menus") } for _, mid := range menuIds { if _, err := dao.AdminRoleMenu.Ctx(ctx).Data(do.AdminRoleMenu{RoleId: roleID, MenuId: mid}).Insert(); err != nil { return gerror.Wrap(err, "bind role menu") } } return nil } // ---------------------- Menu manage ---------------------- func (s *menuManage) Tree(ctx context.Context) ([]*dto.MenuNode, error) { var list []entity.AdminMenu if err := dao.AdminMenu.Ctx(ctx).OrderAsc("sort").Scan(&list); err != nil { return nil, gerror.Wrap(err, "query menus") } byID := make(map[uint64]*dto.MenuNode, len(list)) for i := range list { m := &list[i] byID[m.Id] = &dto.MenuNode{ Id: m.Id, ParentId: m.ParentId, Name: m.Name, Icon: m.Icon, Type: m.Type, Path: m.Path, Component: m.Component, Permission: m.Permission, Sort: m.Sort, Status: m.Status, Hidden: m.Hidden, } } var roots []*dto.MenuNode for i := range list { m := &list[i] node := byID[m.Id] if m.ParentId == 0 { roots = append(roots, node) } else if p, ok := byID[m.ParentId]; ok { p.Children = append(p.Children, node) } } sort.Slice(roots, func(i, j int) bool { return roots[i].Sort < roots[j].Sort }) for _, r := range roots { sortMenuChildren(r) } return roots, nil } func sortMenuChildren(n *dto.MenuNode) { sort.Slice(n.Children, func(i, j int) bool { return n.Children[i].Sort < n.Children[j].Sort }) for _, c := range n.Children { sortMenuChildren(c) } } func (s *menuManage) Create(ctx context.Context, in dto.MenuCreateInput) (uint64, error) { count, err := dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Permission: in.Permission}).Count() if err != nil { return 0, gerror.Wrap(err, "check menu permission") } if count > 0 { return 0, response.Error(consts.CodeInvalidParam, "permission already exists") } id, err := dao.AdminMenu.Ctx(ctx).Data(do.AdminMenu{ ParentId: in.ParentId, Name: in.Name, Icon: in.Icon, Type: in.Type, Path: in.Path, Component: in.Component, Permission: in.Permission, Sort: in.Sort, Status: in.Status, Hidden: in.Hidden, }).InsertAndGetId() if err != nil { return 0, gerror.Wrap(err, "insert menu") } return uint64(id), nil } func (s *menuManage) Update(ctx context.Context, in dto.MenuUpdateInput) error { count, err := dao.AdminMenu.Ctx(ctx).Where("permission = ? AND id != ?", in.Permission, in.Id).Count() if err != nil { return gerror.Wrap(err, "check menu permission") } if count > 0 { return response.Error(consts.CodeInvalidParam, "permission already exists") } _, err = dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Id: in.Id}).Data(do.AdminMenu{ ParentId: in.ParentId, Name: in.Name, Icon: in.Icon, Type: in.Type, Path: in.Path, Component: in.Component, Permission: in.Permission, Sort: in.Sort, Status: in.Status, Hidden: in.Hidden, }).Update() if err != nil { return gerror.Wrap(err, "update menu") } return nil } func (s *menuManage) Delete(ctx context.Context, id uint64) error { // 删除前检查是否存在子节点。 cnt, err := dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{ParentId: id}).Count() if err != nil { return gerror.Wrap(err, "check menu children") } if cnt > 0 { return response.Error(consts.CodeInvalidParam, "delete children first") } if _, err = dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Id: id}).Delete(); err != nil { return gerror.Wrap(err, "delete menu") } if _, err = dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{MenuId: id}).Delete(); err != nil { return gerror.Wrap(err, "delete role menu bindings") } return nil }