From 7df0450b1e5a4a43d99d67d87b0496be18e3219d Mon Sep 17 00:00:00 2001 From: Yvan <8574526@qq,com> Date: Wed, 7 Jan 2026 12:26:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E4=B8=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/biz/system/api.go | 310 ++++++++++++++++++++ internal/biz/system/menu.go | 293 +++++++++++++++++++ internal/data/system/api.go | 268 ++++++++++++++++++ internal/data/system/menu.go | 485 ++++++++++++++++++++++++++++++++ internal/service/system/api.go | 398 ++++++++++++++++++++++++++ internal/service/system/menu.go | 440 +++++++++++++++++++++++++++++ 6 files changed, 2194 insertions(+) create mode 100644 internal/biz/system/api.go create mode 100644 internal/biz/system/menu.go create mode 100644 internal/data/system/api.go create mode 100644 internal/data/system/menu.go create mode 100644 internal/service/system/api.go create mode 100644 internal/service/system/menu.go diff --git a/internal/biz/system/api.go b/internal/biz/system/api.go new file mode 100644 index 0000000..9f9db61 --- /dev/null +++ b/internal/biz/system/api.go @@ -0,0 +1,310 @@ +package system + +import ( + "context" + "errors" + "strings" +) + +// Api API实体 +type Api struct { + ID uint + Path string + Description string + ApiGroup string + Method string +} + +// IgnoreApi 忽略的API +type IgnoreApi struct { + Path string + Method string +} + +// CasbinRule Casbin规则 +type CasbinRule struct { + Path string + Method string +} + +// ApiRepo API仓储接口 +type ApiRepo interface { + Create(ctx context.Context, api *Api) error + Update(ctx context.Context, api *Api) error + Delete(ctx context.Context, id uint) error + DeleteByIds(ctx context.Context, ids []uint) error + FindByID(ctx context.Context, id uint) (*Api, error) + FindByPathMethod(ctx context.Context, path, method string) (*Api, error) + List(ctx context.Context, page, pageSize int, filters map[string]string, orderKey string, desc bool) ([]*Api, int64, error) + FindAll(ctx context.Context) ([]*Api, error) + FindByIds(ctx context.Context, ids []uint) ([]*Api, error) + GetApiGroups(ctx context.Context) ([]string, map[string]string, error) + CreateIgnoreApi(ctx context.Context, path, method string) error + DeleteIgnoreApi(ctx context.Context, path, method string) error + FindAllIgnoreApis(ctx context.Context) ([]*IgnoreApi, error) +} + +// CasbinRepo Casbin仓储接口 +type CasbinRepo interface { + ClearCasbin(v int, p ...string) bool + UpdateCasbinApi(oldPath, newPath, oldMethod, newMethod string) error + GetPolicyPathByAuthorityId(authorityId uint) []CasbinRule + FreshCasbin() error +} + +// ApiUsecase API用例 +type ApiUsecase struct { + repo ApiRepo + casbinRepo CasbinRepo + authRepo AuthorityRepo + useStrictAuth bool + routers []RouterInfo // 内存中的路由信息 +} + +// RouterInfo 路由信息 +type RouterInfo struct { + Path string + Method string +} + +// NewApiUsecase 创建API用例 +func NewApiUsecase(repo ApiRepo, casbinRepo CasbinRepo, authRepo AuthorityRepo, useStrictAuth bool) *ApiUsecase { + return &ApiUsecase{ + repo: repo, + casbinRepo: casbinRepo, + authRepo: authRepo, + useStrictAuth: useStrictAuth, + routers: make([]RouterInfo, 0), + } +} + +// SetRouters 设置内存中的路由信息 +func (uc *ApiUsecase) SetRouters(routers []RouterInfo) { + uc.routers = routers +} + +// CreateApi 创建API +func (uc *ApiUsecase) CreateApi(ctx context.Context, api *Api) error { + return uc.repo.Create(ctx, api) +} + +// UpdateApi 更新API +func (uc *ApiUsecase) UpdateApi(ctx context.Context, api *Api) error { + // 获取旧记录 + old, err := uc.repo.FindByID(ctx, api.ID) + if err != nil { + return err + } + + // 如果路径或方法变更,更新Casbin + if old.Path != api.Path || old.Method != api.Method { + if err := uc.casbinRepo.UpdateCasbinApi(old.Path, api.Path, old.Method, api.Method); err != nil { + return err + } + } + + return uc.repo.Update(ctx, api) +} + +// DeleteApi 删除API +func (uc *ApiUsecase) DeleteApi(ctx context.Context, id uint) error { + api, err := uc.repo.FindByID(ctx, id) + if err != nil { + return err + } + + if err := uc.repo.Delete(ctx, id); err != nil { + return err + } + + // 清除Casbin规则 + uc.casbinRepo.ClearCasbin(1, api.Path, api.Method) + return nil +} + +// DeleteApisByIds 批量删除API +func (uc *ApiUsecase) DeleteApisByIds(ctx context.Context, ids []uint) error { + apis, err := uc.repo.FindByIds(ctx, ids) + if err != nil { + return err + } + + if err := uc.repo.DeleteByIds(ctx, ids); err != nil { + return err + } + + // 清除Casbin规则 + for _, api := range apis { + uc.casbinRepo.ClearCasbin(1, api.Path, api.Method) + } + return nil +} + +// GetApiById 根据ID获取API +func (uc *ApiUsecase) GetApiById(ctx context.Context, id uint) (*Api, error) { + return uc.repo.FindByID(ctx, id) +} + +// GetAPIInfoList 分页获取API列表 +func (uc *ApiUsecase) GetAPIInfoList(ctx context.Context, page, pageSize int, filters map[string]string, orderKey string, desc bool) ([]*Api, int64, error) { + // 验证排序字段 + if orderKey != "" { + orderMap := map[string]bool{"id": true, "path": true, "api_group": true, "description": true, "method": true} + if !orderMap[orderKey] { + return nil, 0, errors.New("非法的排序字段: " + orderKey) + } + } + return uc.repo.List(ctx, page, pageSize, filters, orderKey, desc) +} + +// GetAllApis 获取所有API(根据权限过滤) +func (uc *ApiUsecase) GetAllApis(ctx context.Context, authorityID uint) ([]*Api, error) { + parentAuthorityID, err := uc.authRepo.GetParentAuthorityID(ctx, authorityID) + if err != nil { + return nil, err + } + + apis, err := uc.repo.FindAll(ctx) + if err != nil { + return nil, err + } + + // 如果是顶级角色或不使用严格权限,返回所有API + if parentAuthorityID == 0 || !uc.useStrictAuth { + return apis, nil + } + + // 根据Casbin规则过滤 + paths := uc.casbinRepo.GetPolicyPathByAuthorityId(authorityID) + var authApis []*Api + for _, api := range apis { + for _, rule := range paths { + if rule.Path == api.Path && rule.Method == api.Method { + authApis = append(authApis, api) + break + } + } + } + return authApis, nil +} + +// GetApiGroups 获取API分组 +func (uc *ApiUsecase) GetApiGroups(ctx context.Context) ([]string, map[string]string, error) { + return uc.repo.GetApiGroups(ctx) +} + +// SyncApi 同步API +func (uc *ApiUsecase) SyncApi(ctx context.Context) (newApis, deleteApis, ignoreApis []*Api, err error) { + newApis = make([]*Api, 0) + deleteApis = make([]*Api, 0) + ignoreApis = make([]*Api, 0) + + // 获取数据库中的API + apis, err := uc.repo.FindAll(ctx) + if err != nil { + return nil, nil, nil, err + } + + // 获取忽略的API + ignores, err := uc.repo.FindAllIgnoreApis(ctx) + if err != nil { + return nil, nil, nil, err + } + + for _, ig := range ignores { + ignoreApis = append(ignoreApis, &Api{ + Path: ig.Path, + Method: ig.Method, + }) + } + + // 过滤掉忽略的路由 + var cacheApis []*Api + for _, r := range uc.routers { + ignored := false + for _, ig := range ignores { + if ig.Path == r.Path && ig.Method == r.Method { + ignored = true + break + } + } + if !ignored { + cacheApis = append(cacheApis, &Api{ + Path: r.Path, + Method: r.Method, + }) + } + } + + // 对比:内存中有但数据库没有的 -> 新增 + for _, cache := range cacheApis { + found := false + for _, api := range apis { + if cache.Path == api.Path && cache.Method == api.Method { + found = true + break + } + } + if !found { + newApis = append(newApis, &Api{ + Path: cache.Path, + Method: cache.Method, + }) + } + } + + // 对比:数据库有但内存没有的 -> 删除 + for _, api := range apis { + found := false + for _, cache := range cacheApis { + if cache.Path == api.Path && cache.Method == api.Method { + found = true + break + } + } + if !found { + deleteApis = append(deleteApis, api) + } + } + + return newApis, deleteApis, ignoreApis, nil +} + +// IgnoreApi 忽略/取消忽略API +func (uc *ApiUsecase) IgnoreApi(ctx context.Context, path, method string, flag bool) error { + if flag { + return uc.repo.CreateIgnoreApi(ctx, path, method) + } + return uc.repo.DeleteIgnoreApi(ctx, path, method) +} + +// EnterSyncApi 确认同步API +func (uc *ApiUsecase) EnterSyncApi(ctx context.Context, newApis, deleteApis []*Api) error { + // 创建新API + for _, api := range newApis { + if err := uc.repo.Create(ctx, api); err != nil { + // 忽略重复错误 + if !strings.Contains(err.Error(), "存在相同api") { + return err + } + } + } + + // 删除旧API + for _, api := range deleteApis { + uc.casbinRepo.ClearCasbin(1, api.Path, api.Method) + existing, err := uc.repo.FindByPathMethod(ctx, api.Path, api.Method) + if err == nil && existing != nil { + if err := uc.repo.Delete(ctx, existing.ID); err != nil { + return err + } + } + } + + return nil +} + +// FreshCasbin 刷新Casbin缓存 +func (uc *ApiUsecase) FreshCasbin() error { + return uc.casbinRepo.FreshCasbin() +} diff --git a/internal/biz/system/menu.go b/internal/biz/system/menu.go new file mode 100644 index 0000000..f7c33f3 --- /dev/null +++ b/internal/biz/system/menu.go @@ -0,0 +1,293 @@ +package system + +import ( + "context" + "errors" + "strconv" +) + +// BaseMenu 基础菜单实体 +type BaseMenu struct { + ID uint + ParentId uint + Path string + Name string + Hidden bool + Component string + Sort int + ActiveName string + KeepAlive bool + DefaultMenu bool + Title string + Icon string + CloseTab bool + TransitionType string + Parameters []*MenuParameter + MenuBtn []*MenuBtn + Children []*BaseMenu +} + +// MenuParameter 菜单参数 +type MenuParameter struct { + ID uint + SysBaseMenuID uint + Type string + Key string + Value string +} + +// MenuBtn 菜单按钮 +type MenuBtn struct { + ID uint + SysBaseMenuID uint + Name string + Desc string +} + +// Menu 菜单(带权限) +type Menu struct { + BaseMenu + AuthorityId uint + MenuId uint + Btns map[string]uint + Children []*Menu +} + +// AuthorityBtn 角色按钮权限 +type AuthorityBtn struct { + AuthorityId uint + SysMenuID uint + SysBaseMenuBtnID uint + BtnName string +} + +// MenuRepo 菜单仓储接口 +type MenuRepo interface { + CreateBaseMenu(ctx context.Context, menu *BaseMenu) error + UpdateBaseMenu(ctx context.Context, menu *BaseMenu) error + DeleteBaseMenu(ctx context.Context, id uint) error + FindBaseMenuByID(ctx context.Context, id uint) (*BaseMenu, error) + FindAllBaseMenus(ctx context.Context) ([]*BaseMenu, error) + FindBaseMenusByIds(ctx context.Context, ids []string) ([]*BaseMenu, error) + FindAuthorityMenuIds(ctx context.Context, authorityId uint) ([]string, error) + FindAuthorityBtns(ctx context.Context, authorityId uint) ([]*AuthorityBtn, error) + SetMenuAuthority(ctx context.Context, authorityId uint, menuIds []uint) error +} + +// MenuUsecase 菜单用例 +type MenuUsecase struct { + repo MenuRepo + authRepo AuthorityRepo + useStrictAuth bool +} + +// NewMenuUsecase 创建菜单用例 +func NewMenuUsecase(repo MenuRepo, authRepo AuthorityRepo, useStrictAuth bool) *MenuUsecase { + return &MenuUsecase{ + repo: repo, + authRepo: authRepo, + useStrictAuth: useStrictAuth, + } +} + +// AddBaseMenu 添加基础菜单 +func (uc *MenuUsecase) AddBaseMenu(ctx context.Context, menu *BaseMenu) error { + return uc.repo.CreateBaseMenu(ctx, menu) +} + +// UpdateBaseMenu 更新基础菜单 +func (uc *MenuUsecase) UpdateBaseMenu(ctx context.Context, menu *BaseMenu) error { + return uc.repo.UpdateBaseMenu(ctx, menu) +} + +// DeleteBaseMenu 删除基础菜单 +func (uc *MenuUsecase) DeleteBaseMenu(ctx context.Context, id uint) error { + return uc.repo.DeleteBaseMenu(ctx, id) +} + +// GetBaseMenuById 根据ID获取基础菜单 +func (uc *MenuUsecase) GetBaseMenuById(ctx context.Context, id uint) (*BaseMenu, error) { + return uc.repo.FindBaseMenuByID(ctx, id) +} + +// GetMenuTree 获取动态菜单树 +func (uc *MenuUsecase) GetMenuTree(ctx context.Context, authorityId uint) ([]*Menu, error) { + treeMap, err := uc.getMenuTreeMap(ctx, authorityId) + if err != nil { + return nil, err + } + menus := treeMap[0] + for i := range menus { + uc.getChildrenList(menus[i], treeMap) + } + return menus, nil +} + +func (uc *MenuUsecase) getMenuTreeMap(ctx context.Context, authorityId uint) (map[uint][]*Menu, error) { + treeMap := make(map[uint][]*Menu) + + // 获取角色的菜单ID + menuIds, err := uc.repo.FindAuthorityMenuIds(ctx, authorityId) + if err != nil { + return nil, err + } + + // 获取基础菜单 + baseMenus, err := uc.repo.FindBaseMenusByIds(ctx, menuIds) + if err != nil { + return nil, err + } + + // 获取按钮权限 + btns, err := uc.repo.FindAuthorityBtns(ctx, authorityId) + if err != nil { + return nil, err + } + + // 构建按钮map + btnMap := make(map[uint]map[string]uint) + for _, b := range btns { + if btnMap[b.SysMenuID] == nil { + btnMap[b.SysMenuID] = make(map[string]uint) + } + btnMap[b.SysMenuID][b.BtnName] = authorityId + } + + // 构建菜单树 + for _, base := range baseMenus { + menu := &Menu{ + BaseMenu: *base, + AuthorityId: authorityId, + MenuId: base.ID, + Btns: btnMap[base.ID], + } + treeMap[base.ParentId] = append(treeMap[base.ParentId], menu) + } + + return treeMap, nil +} + +func (uc *MenuUsecase) getChildrenList(menu *Menu, treeMap map[uint][]*Menu) { + menu.Children = treeMap[menu.MenuId] + for i := range menu.Children { + uc.getChildrenList(menu.Children[i], treeMap) + } +} + +// GetBaseMenuTree 获取基础菜单树 +func (uc *MenuUsecase) GetBaseMenuTree(ctx context.Context, authorityID uint) ([]*BaseMenu, error) { + treeMap, err := uc.getBaseMenuTreeMap(ctx, authorityID) + if err != nil { + return nil, err + } + menus := treeMap[0] + for i := range menus { + uc.getBaseChildrenList(menus[i], treeMap) + } + return menus, nil +} + +func (uc *MenuUsecase) getBaseMenuTreeMap(ctx context.Context, authorityID uint) (map[uint][]*BaseMenu, error) { + parentAuthorityID, err := uc.authRepo.GetParentAuthorityID(ctx, authorityID) + if err != nil { + return nil, err + } + + var allMenus []*BaseMenu + treeMap := make(map[uint][]*BaseMenu) + + // 当开启了严格的树角色并且父角色不为0时需要进行菜单筛选 + if uc.useStrictAuth && parentAuthorityID != 0 { + menuIds, err := uc.repo.FindAuthorityMenuIds(ctx, authorityID) + if err != nil { + return nil, err + } + allMenus, err = uc.repo.FindBaseMenusByIds(ctx, menuIds) + if err != nil { + return nil, err + } + } else { + allMenus, err = uc.repo.FindAllBaseMenus(ctx) + if err != nil { + return nil, err + } + } + + for _, m := range allMenus { + treeMap[m.ParentId] = append(treeMap[m.ParentId], m) + } + return treeMap, nil +} + +func (uc *MenuUsecase) getBaseChildrenList(menu *BaseMenu, treeMap map[uint][]*BaseMenu) { + menu.Children = treeMap[menu.ID] + for i := range menu.Children { + uc.getBaseChildrenList(menu.Children[i], treeMap) + } +} + +// GetInfoList 获取菜单列表(树形) +func (uc *MenuUsecase) GetInfoList(ctx context.Context, authorityID uint) ([]*BaseMenu, error) { + return uc.GetBaseMenuTree(ctx, authorityID) +} + +// AddMenuAuthority 为角色增加菜单权限 +func (uc *MenuUsecase) AddMenuAuthority(ctx context.Context, menus []*BaseMenu, adminAuthorityID, authorityId uint) error { + // 检查权限 + parentAuthorityID, err := uc.authRepo.GetParentAuthorityID(ctx, adminAuthorityID) + if err != nil { + return err + } + + // 当开启了严格的树角色并且父角色不为0时需要进行菜单筛选 + if uc.useStrictAuth && parentAuthorityID != 0 { + menuIds, err := uc.repo.FindAuthorityMenuIds(ctx, adminAuthorityID) + if err != nil { + return err + } + + for _, menu := range menus { + hasMenu := false + idStr := strconv.Itoa(int(menu.ID)) + for _, mid := range menuIds { + if idStr == mid { + hasMenu = true + break + } + } + if !hasMenu { + return errors.New("添加失败,请勿跨级操作") + } + } + } + + // 设置菜单权限 + menuIds := make([]uint, len(menus)) + for i, m := range menus { + menuIds[i] = m.ID + } + return uc.repo.SetMenuAuthority(ctx, authorityId, menuIds) +} + +// GetMenuAuthority 获取角色的菜单权限 +func (uc *MenuUsecase) GetMenuAuthority(ctx context.Context, authorityId uint) ([]*Menu, error) { + menuIds, err := uc.repo.FindAuthorityMenuIds(ctx, authorityId) + if err != nil { + return nil, err + } + + baseMenus, err := uc.repo.FindBaseMenusByIds(ctx, menuIds) + if err != nil { + return nil, err + } + + menus := make([]*Menu, len(baseMenus)) + for i, base := range baseMenus { + menus[i] = &Menu{ + BaseMenu: *base, + AuthorityId: authorityId, + MenuId: base.ID, + } + } + return menus, nil +} diff --git a/internal/data/system/api.go b/internal/data/system/api.go new file mode 100644 index 0000000..769e2a5 --- /dev/null +++ b/internal/data/system/api.go @@ -0,0 +1,268 @@ +package system + +import ( + "context" + "errors" + "strings" + + "kra/internal/biz/system" + "kra/internal/data/model" + "kra/internal/data/query" + + "gorm.io/gorm" +) + +type apiRepo struct { + db *gorm.DB +} + +// NewApiRepo 创建API仓储 +func NewApiRepo(db *gorm.DB) system.ApiRepo { + return &apiRepo{db: db} +} + +func (r *apiRepo) Create(ctx context.Context, api *system.Api) error { + _, err := query.SysAPI.WithContext(ctx).Where( + query.SysAPI.Path.Eq(api.Path), + query.SysAPI.Method.Eq(api.Method), + ).First() + if err == nil { + return errors.New("存在相同api") + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + m := toModelAPI(api) + return query.SysAPI.WithContext(ctx).Create(m) +} + +func (r *apiRepo) Update(ctx context.Context, api *system.Api) error { + old, err := query.SysAPI.WithContext(ctx).Where(query.SysAPI.ID.Eq(int64(api.ID))).First() + if err != nil { + return err + } + if safeString(old.Path) != api.Path || safeString(old.Method) != api.Method { + dup, ferr := query.SysAPI.WithContext(ctx).Where( + query.SysAPI.Path.Eq(api.Path), + query.SysAPI.Method.Eq(api.Method), + ).First() + if ferr == nil && dup.ID != int64(api.ID) { + return errors.New("存在相同api路径") + } + if ferr != nil && !errors.Is(ferr, gorm.ErrRecordNotFound) { + return ferr + } + } + m := toModelAPI(api) + _, err = query.SysAPI.WithContext(ctx).Where(query.SysAPI.ID.Eq(int64(api.ID))).Updates(m) + return err +} + +func (r *apiRepo) Delete(ctx context.Context, id uint) error { + _, err := query.SysAPI.WithContext(ctx).Where(query.SysAPI.ID.Eq(int64(id))).Delete() + return err +} + +func (r *apiRepo) DeleteByIds(ctx context.Context, ids []uint) error { + int64Ids := make([]int64, len(ids)) + for i, id := range ids { + int64Ids[i] = int64(id) + } + _, err := query.SysAPI.WithContext(ctx).Where(query.SysAPI.ID.In(int64Ids...)).Delete() + return err +} + +func (r *apiRepo) FindByID(ctx context.Context, id uint) (*system.Api, error) { + m, err := query.SysAPI.WithContext(ctx).Where(query.SysAPI.ID.Eq(int64(id))).First() + if err != nil { + return nil, err + } + return toBizAPI(m), nil +} + +func (r *apiRepo) FindByPathMethod(ctx context.Context, path, method string) (*system.Api, error) { + m, err := query.SysAPI.WithContext(ctx).Where( + query.SysAPI.Path.Eq(path), + query.SysAPI.Method.Eq(method), + ).First() + if err != nil { + return nil, err + } + return toBizAPI(m), nil +} + +func (r *apiRepo) List(ctx context.Context, page, pageSize int, filters map[string]string, orderKey string, desc bool) ([]*system.Api, int64, error) { + q := query.SysAPI.WithContext(ctx) + if v, ok := filters["path"]; ok && v != "" { + q = q.Where(query.SysAPI.Path.Like("%" + v + "%")) + } + if v, ok := filters["description"]; ok && v != "" { + q = q.Where(query.SysAPI.Description.Like("%" + v + "%")) + } + if v, ok := filters["method"]; ok && v != "" { + q = q.Where(query.SysAPI.Method.Eq(v)) + } + if v, ok := filters["api_group"]; ok && v != "" { + q = q.Where(query.SysAPI.APIGroup.Eq(v)) + } + + total, err := q.Count() + if err != nil { + return nil, 0, err + } + + orderMap := map[string]bool{"id": true, "path": true, "api_group": true, "description": true, "method": true} + if orderKey != "" && orderMap[orderKey] { + switch orderKey { + case "id": + if desc { + q = q.Order(query.SysAPI.ID.Desc()) + } else { + q = q.Order(query.SysAPI.ID) + } + case "path": + if desc { + q = q.Order(query.SysAPI.Path.Desc()) + } else { + q = q.Order(query.SysAPI.Path) + } + case "api_group": + if desc { + q = q.Order(query.SysAPI.APIGroup.Desc()) + } else { + q = q.Order(query.SysAPI.APIGroup) + } + case "description": + if desc { + q = q.Order(query.SysAPI.Description.Desc()) + } else { + q = q.Order(query.SysAPI.Description) + } + case "method": + if desc { + q = q.Order(query.SysAPI.Method.Desc()) + } else { + q = q.Order(query.SysAPI.Method) + } + } + } else { + q = q.Order(query.SysAPI.ID.Desc()) + } + + offset := (page - 1) * pageSize + list, err := q.Offset(offset).Limit(pageSize).Find() + if err != nil { + return nil, 0, err + } + + apis := make([]*system.Api, len(list)) + for i, m := range list { + apis[i] = toBizAPI(m) + } + return apis, total, nil +} + +func (r *apiRepo) FindAll(ctx context.Context) ([]*system.Api, error) { + list, err := query.SysAPI.WithContext(ctx).Order(query.SysAPI.ID.Desc()).Find() + if err != nil { + return nil, err + } + apis := make([]*system.Api, len(list)) + for i, m := range list { + apis[i] = toBizAPI(m) + } + return apis, nil +} + +func (r *apiRepo) FindByIds(ctx context.Context, ids []uint) ([]*system.Api, error) { + int64Ids := make([]int64, len(ids)) + for i, id := range ids { + int64Ids[i] = int64(id) + } + list, err := query.SysAPI.WithContext(ctx).Where(query.SysAPI.ID.In(int64Ids...)).Find() + if err != nil { + return nil, err + } + apis := make([]*system.Api, len(list)) + for i, m := range list { + apis[i] = toBizAPI(m) + } + return apis, nil +} + +func (r *apiRepo) GetApiGroups(ctx context.Context) ([]string, map[string]string, error) { + list, err := query.SysAPI.WithContext(ctx).Find() + if err != nil { + return nil, nil, err + } + + groupMap := make(map[string]bool) + apiGroupMap := make(map[string]string) + var groups []string + + for _, m := range list { + path := safeString(m.Path) + apiGroup := safeString(m.APIGroup) + + if !groupMap[apiGroup] { + groupMap[apiGroup] = true + groups = append(groups, apiGroup) + } + + if len(path) > 1 { + parts := strings.Split(path, "/") + if len(parts) > 1 { + apiGroupMap[parts[1]] = apiGroup + } + } + } + return groups, apiGroupMap, nil +} + +func (r *apiRepo) CreateIgnoreApi(ctx context.Context, path, method string) error { + m := &model.SysIgnoreAPI{ + Path: &path, + Method: &method, + } + return r.db.WithContext(ctx).Create(m).Error +} + +func (r *apiRepo) DeleteIgnoreApi(ctx context.Context, path, method string) error { + return r.db.WithContext(ctx).Unscoped().Where("path = ? AND method = ?", path, method).Delete(&model.SysIgnoreAPI{}).Error +} + +func (r *apiRepo) FindAllIgnoreApis(ctx context.Context) ([]*system.IgnoreApi, error) { + var list []model.SysIgnoreAPI + if err := r.db.WithContext(ctx).Find(&list).Error; err != nil { + return nil, err + } + result := make([]*system.IgnoreApi, len(list)) + for i, m := range list { + result[i] = &system.IgnoreApi{ + Path: safeString(m.Path), + Method: safeString(m.Method), + } + } + return result, nil +} + +// 转换函数 +func toModelAPI(a *system.Api) *model.SysAPI { + return &model.SysAPI{ + ID: int64(a.ID), + Path: &a.Path, + Description: &a.Description, + APIGroup: &a.ApiGroup, + Method: &a.Method, + } +} + +func toBizAPI(m *model.SysAPI) *system.Api { + return &system.Api{ + ID: uint(m.ID), + Path: safeString(m.Path), + Description: safeString(m.Description), + ApiGroup: safeString(m.APIGroup), + Method: safeString(m.Method), + } +} diff --git a/internal/data/system/menu.go b/internal/data/system/menu.go new file mode 100644 index 0000000..4c4096a --- /dev/null +++ b/internal/data/system/menu.go @@ -0,0 +1,485 @@ +package system + +import ( + "context" + "errors" + "strconv" + + "kra/internal/biz/system" + "kra/internal/data/model" + + "gorm.io/gorm" +) + +type menuRepo struct { + db *gorm.DB +} + +// NewMenuRepo 创建菜单仓储 +func NewMenuRepo(db *gorm.DB) system.MenuRepo { + return &menuRepo{db: db} +} + +// CreateBaseMenu 创建基础菜单 +func (r *menuRepo) CreateBaseMenu(ctx context.Context, menu *system.BaseMenu) error { + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + // 检查name是否重复 + var count int64 + if err := tx.Model(&model.SysBaseMenu{}).Where("name = ?", menu.Name).Count(&count).Error; err != nil { + return err + } + if count > 0 { + return errors.New("存在重复name,请修改name") + } + + if menu.ParentId != 0 { + // 检查父菜单是否存在 + var parentMenu model.SysBaseMenu + if err := tx.First(&parentMenu, menu.ParentId).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("父菜单不存在") + } + return err + } + + // 检查父菜单下现有子菜单数量 + var existingChildrenCount int64 + if err := tx.Model(&model.SysBaseMenu{}).Where("parent_id = ?", menu.ParentId).Count(&existingChildrenCount).Error; err != nil { + return err + } + + // 如果父菜单原本是叶子菜单,需要清空其权限分配 + if existingChildrenCount == 0 { + // 检查父菜单是否被其他角色设置为首页 + var defaultRouterCount int64 + if err := tx.Model(&model.SysAuthority{}).Where("default_router = ?", parentMenu.Name).Count(&defaultRouterCount).Error; err != nil { + return err + } + if defaultRouterCount > 0 { + return errors.New("父菜单已被其他角色的首页占用,请先释放父菜单的首页权限") + } + + // 清空父菜单的所有权限分配 + if err := tx.Where("sys_base_menu_id = ?", menu.ParentId).Delete(&model.SysAuthorityMenu{}).Error; err != nil { + return err + } + } + } + + // 创建菜单 + m := toModelBaseMenu(menu) + if err := tx.Create(m).Error; err != nil { + return err + } + + // 创建参数 + if len(menu.Parameters) > 0 { + for _, p := range menu.Parameters { + param := &model.SysBaseMenuParameter{ + SysBaseMenuID: ptrInt64(int64(m.ID)), + Type: &p.Type, + Key: &p.Key, + Value: &p.Value, + } + if err := tx.Create(param).Error; err != nil { + return err + } + } + } + + // 创建按钮 + if len(menu.MenuBtn) > 0 { + for _, b := range menu.MenuBtn { + btn := &model.SysBaseMenuBtn{ + SysBaseMenuID: ptrInt64(int64(m.ID)), + Name: &b.Name, + Desc: &b.Desc, + } + if err := tx.Create(btn).Error; err != nil { + return err + } + } + } + + menu.ID = uint(m.ID) + return nil + }) +} + +// UpdateBaseMenu 更新基础菜单 +func (r *menuRepo) UpdateBaseMenu(ctx context.Context, menu *system.BaseMenu) error { + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + var oldMenu model.SysBaseMenu + if err := tx.Where("id = ?", menu.ID).First(&oldMenu).Error; err != nil { + return err + } + + // 检查name是否重复 + if safeString(oldMenu.Name) != menu.Name { + var count int64 + if err := tx.Model(&model.SysBaseMenu{}).Where("id <> ? AND name = ?", menu.ID, menu.Name).Count(&count).Error; err != nil { + return err + } + if count > 0 { + return errors.New("存在相同name修改失败") + } + } + + // 删除旧参数和按钮 + if err := tx.Unscoped().Where("sys_base_menu_id = ?", menu.ID).Delete(&model.SysBaseMenuParameter{}).Error; err != nil { + return err + } + if err := tx.Unscoped().Where("sys_base_menu_id = ?", menu.ID).Delete(&model.SysBaseMenuBtn{}).Error; err != nil { + return err + } + + // 创建新参数 + if len(menu.Parameters) > 0 { + for _, p := range menu.Parameters { + param := &model.SysBaseMenuParameter{ + SysBaseMenuID: ptrInt64(int64(menu.ID)), + Type: &p.Type, + Key: &p.Key, + Value: &p.Value, + } + if err := tx.Create(param).Error; err != nil { + return err + } + } + } + + // 创建新按钮 + if len(menu.MenuBtn) > 0 { + for _, b := range menu.MenuBtn { + btn := &model.SysBaseMenuBtn{ + SysBaseMenuID: ptrInt64(int64(menu.ID)), + Name: &b.Name, + Desc: &b.Desc, + } + if err := tx.Create(btn).Error; err != nil { + return err + } + } + } + + // 更新菜单 + updates := map[string]any{ + "keep_alive": menu.KeepAlive, + "close_tab": menu.CloseTab, + "default_menu": menu.DefaultMenu, + "parent_id": menu.ParentId, + "path": menu.Path, + "name": menu.Name, + "hidden": menu.Hidden, + "component": menu.Component, + "title": menu.Title, + "active_name": menu.ActiveName, + "icon": menu.Icon, + "sort": menu.Sort, + "transition_type": menu.TransitionType, + } + return tx.Model(&oldMenu).Updates(updates).Error + }) +} + +// DeleteBaseMenu 删除基础菜单 +func (r *menuRepo) DeleteBaseMenu(ctx context.Context, id uint) error { + // 检查是否有子菜单 + var childCount int64 + if err := r.db.WithContext(ctx).Model(&model.SysBaseMenu{}).Where("parent_id = ?", id).Count(&childCount).Error; err != nil { + return err + } + if childCount > 0 { + return errors.New("此菜单存在子菜单不可删除") + } + + // 获取菜单信息 + var menu model.SysBaseMenu + if err := r.db.WithContext(ctx).First(&menu, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("记录不存在") + } + return err + } + + // 检查是否有角色将此菜单作为首页 + var defaultRouterCount int64 + if err := r.db.WithContext(ctx).Model(&model.SysAuthority{}).Where("default_router = ?", menu.Name).Count(&defaultRouterCount).Error; err != nil { + return err + } + if defaultRouterCount > 0 { + return errors.New("此菜单有角色正在作为首页,不可删除") + } + + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := tx.Delete(&model.SysBaseMenu{}, "id = ?", id).Error; err != nil { + return err + } + if err := tx.Delete(&model.SysBaseMenuParameter{}, "sys_base_menu_id = ?", id).Error; err != nil { + return err + } + if err := tx.Delete(&model.SysBaseMenuBtn{}, "sys_base_menu_id = ?", id).Error; err != nil { + return err + } + if err := tx.Delete(&model.SysAuthorityBtn{}, "sys_menu_id = ?", id).Error; err != nil { + return err + } + if err := tx.Delete(&model.SysAuthorityMenu{}, "sys_base_menu_id = ?", id).Error; err != nil { + return err + } + return nil + }) +} + +// FindBaseMenuByID 根据ID获取基础菜单 +func (r *menuRepo) FindBaseMenuByID(ctx context.Context, id uint) (*system.BaseMenu, error) { + var menu model.SysBaseMenu + if err := r.db.WithContext(ctx).Where("id = ?", id).First(&menu).Error; err != nil { + return nil, err + } + + // 获取参数 + var params []model.SysBaseMenuParameter + r.db.WithContext(ctx).Where("sys_base_menu_id = ?", id).Find(¶ms) + + // 获取按钮 + var btns []model.SysBaseMenuBtn + r.db.WithContext(ctx).Where("sys_base_menu_id = ?", id).Find(&btns) + + return toBizBaseMenu(&menu, params, btns), nil +} + +// FindAllBaseMenus 获取所有基础菜单 +func (r *menuRepo) FindAllBaseMenus(ctx context.Context) ([]*system.BaseMenu, error) { + var menus []model.SysBaseMenu + if err := r.db.WithContext(ctx).Order("sort").Find(&menus).Error; err != nil { + return nil, err + } + + // 获取所有参数 + var allParams []model.SysBaseMenuParameter + r.db.WithContext(ctx).Find(&allParams) + paramMap := make(map[int64][]model.SysBaseMenuParameter) + for _, p := range allParams { + if p.SysBaseMenuID != nil { + paramMap[*p.SysBaseMenuID] = append(paramMap[*p.SysBaseMenuID], p) + } + } + + // 获取所有按钮 + var allBtns []model.SysBaseMenuBtn + r.db.WithContext(ctx).Find(&allBtns) + btnMap := make(map[int64][]model.SysBaseMenuBtn) + for _, b := range allBtns { + if b.SysBaseMenuID != nil { + btnMap[*b.SysBaseMenuID] = append(btnMap[*b.SysBaseMenuID], b) + } + } + + result := make([]*system.BaseMenu, len(menus)) + for i, m := range menus { + result[i] = toBizBaseMenu(&m, paramMap[m.ID], btnMap[m.ID]) + } + return result, nil +} + +// FindBaseMenusByIds 根据ID列表获取基础菜单 +func (r *menuRepo) FindBaseMenusByIds(ctx context.Context, ids []string) ([]*system.BaseMenu, error) { + var menus []model.SysBaseMenu + if err := r.db.WithContext(ctx).Where("id in (?)", ids).Order("sort").Find(&menus).Error; err != nil { + return nil, err + } + + // 获取参数 + var allParams []model.SysBaseMenuParameter + r.db.WithContext(ctx).Where("sys_base_menu_id in (?)", ids).Find(&allParams) + paramMap := make(map[int64][]model.SysBaseMenuParameter) + for _, p := range allParams { + if p.SysBaseMenuID != nil { + paramMap[*p.SysBaseMenuID] = append(paramMap[*p.SysBaseMenuID], p) + } + } + + // 获取按钮 + var allBtns []model.SysBaseMenuBtn + r.db.WithContext(ctx).Where("sys_base_menu_id in (?)", ids).Find(&allBtns) + btnMap := make(map[int64][]model.SysBaseMenuBtn) + for _, b := range allBtns { + if b.SysBaseMenuID != nil { + btnMap[*b.SysBaseMenuID] = append(btnMap[*b.SysBaseMenuID], b) + } + } + + result := make([]*system.BaseMenu, len(menus)) + for i, m := range menus { + result[i] = toBizBaseMenu(&m, paramMap[m.ID], btnMap[m.ID]) + } + return result, nil +} + +// FindAuthorityMenuIds 获取角色的菜单ID列表 +func (r *menuRepo) FindAuthorityMenuIds(ctx context.Context, authorityId uint) ([]string, error) { + var authorityMenus []model.SysAuthorityMenu + if err := r.db.WithContext(ctx).Where("sys_authority_authority_id = ?", authorityId).Find(&authorityMenus).Error; err != nil { + return nil, err + } + menuIds := make([]string, len(authorityMenus)) + for i, am := range authorityMenus { + menuIds[i] = strconv.FormatInt(am.SysBaseMenuID, 10) + } + return menuIds, nil +} + +// FindAuthorityBtns 获取角色的按钮权限 +func (r *menuRepo) FindAuthorityBtns(ctx context.Context, authorityId uint) ([]*system.AuthorityBtn, error) { + var btns []model.SysAuthorityBtn + if err := r.db.WithContext(ctx).Where("authority_id = ?", authorityId).Find(&btns).Error; err != nil { + return nil, err + } + + // 获取按钮名称 + var btnIds []int64 + for _, b := range btns { + if b.SysBaseMenuBtnID != nil { + btnIds = append(btnIds, *b.SysBaseMenuBtnID) + } + } + + btnNameMap := make(map[int64]string) + if len(btnIds) > 0 { + var menuBtns []model.SysBaseMenuBtn + r.db.WithContext(ctx).Where("id in (?)", btnIds).Find(&menuBtns) + for _, mb := range menuBtns { + btnNameMap[mb.ID] = safeString(mb.Name) + } + } + + result := make([]*system.AuthorityBtn, len(btns)) + for i, b := range btns { + result[i] = &system.AuthorityBtn{} + if b.AuthorityID != nil { + result[i].AuthorityId = uint(*b.AuthorityID) + } + if b.SysMenuID != nil { + result[i].SysMenuID = uint(*b.SysMenuID) + } + if b.SysBaseMenuBtnID != nil { + result[i].SysBaseMenuBtnID = uint(*b.SysBaseMenuBtnID) + result[i].BtnName = btnNameMap[*b.SysBaseMenuBtnID] + } + } + return result, nil +} + +// SetMenuAuthority 设置角色菜单权限 +func (r *menuRepo) SetMenuAuthority(ctx context.Context, authorityId uint, menuIds []uint) error { + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + // 删除旧权限 + if err := tx.Where("sys_authority_authority_id = ?", authorityId).Delete(&model.SysAuthorityMenu{}).Error; err != nil { + return err + } + + // 创建新权限 + for _, menuId := range menuIds { + am := &model.SysAuthorityMenu{ + SysAuthorityAuthorityID: int64(authorityId), + SysBaseMenuID: int64(menuId), + } + if err := tx.Create(am).Error; err != nil { + return err + } + } + return nil + }) +} + +// 转换函数 +func toModelBaseMenu(m *system.BaseMenu) *model.SysBaseMenu { + parentId := int64(m.ParentId) + sort := int64(m.Sort) + + return &model.SysBaseMenu{ + ID: int64(m.ID), + ParentID: &parentId, + Path: &m.Path, + Name: &m.Name, + Hidden: &m.Hidden, + Component: &m.Component, + Sort: &sort, + ActiveName: &m.ActiveName, + KeepAlive: &m.KeepAlive, + DefaultMenu: &m.DefaultMenu, + Title: &m.Title, + Icon: &m.Icon, + CloseTab: &m.CloseTab, + TransitionType: &m.TransitionType, + } +} + +func toBizBaseMenu(m *model.SysBaseMenu, params []model.SysBaseMenuParameter, btns []model.SysBaseMenuBtn) *system.BaseMenu { + menu := &system.BaseMenu{ + ID: uint(m.ID), + Path: safeString(m.Path), + Name: safeString(m.Name), + Component: safeString(m.Component), + ActiveName: safeString(m.ActiveName), + Title: safeString(m.Title), + Icon: safeString(m.Icon), + TransitionType: safeString(m.TransitionType), + } + if m.ParentID != nil { + menu.ParentId = uint(*m.ParentID) + } + if m.Sort != nil { + menu.Sort = int(*m.Sort) + } + if m.Hidden != nil { + menu.Hidden = *m.Hidden + } + if m.KeepAlive != nil { + menu.KeepAlive = *m.KeepAlive + } + if m.DefaultMenu != nil { + menu.DefaultMenu = *m.DefaultMenu + } + if m.CloseTab != nil { + menu.CloseTab = *m.CloseTab + } + + // 转换参数 + if len(params) > 0 { + menu.Parameters = make([]*system.MenuParameter, len(params)) + for i, p := range params { + menu.Parameters[i] = &system.MenuParameter{ + ID: uint(p.ID), + Type: safeString(p.Type), + Key: safeString(p.Key), + Value: safeString(p.Value), + } + if p.SysBaseMenuID != nil { + menu.Parameters[i].SysBaseMenuID = uint(*p.SysBaseMenuID) + } + } + } + + // 转换按钮 + if len(btns) > 0 { + menu.MenuBtn = make([]*system.MenuBtn, len(btns)) + for i, b := range btns { + menu.MenuBtn[i] = &system.MenuBtn{ + ID: uint(b.ID), + Name: safeString(b.Name), + Desc: safeString(b.Desc), + } + if b.SysBaseMenuID != nil { + menu.MenuBtn[i].SysBaseMenuID = uint(*b.SysBaseMenuID) + } + } + } + + return menu +} + +func ptrInt64(v int64) *int64 { + return &v +} diff --git a/internal/service/system/api.go b/internal/service/system/api.go new file mode 100644 index 0000000..f3f3cdf --- /dev/null +++ b/internal/service/system/api.go @@ -0,0 +1,398 @@ +package system + +import ( + "context" + + "kra/internal/biz/system" + "kra/internal/server/middleware" + + "github.com/go-kratos/kratos/v2/errors" + "github.com/go-kratos/kratos/v2/transport/http" +) + +// ApiService API服务 +type ApiService struct { + uc *system.ApiUsecase +} + +// NewApiService 创建API服务 +func NewApiService(uc *system.ApiUsecase) *ApiService { + return &ApiService{uc: uc} +} + +// ApiInfo API信息 +type ApiInfo struct { + ID uint `json:"ID"` + Path string `json:"path"` + Description string `json:"description"` + ApiGroup string `json:"apiGroup"` + Method string `json:"method"` +} + +// CreateApiRequest 创建API请求 +type CreateApiRequest struct { + Path string `json:"path"` + Description string `json:"description"` + ApiGroup string `json:"apiGroup"` + Method string `json:"method"` +} + +// CreateApi 创建API +func (s *ApiService) CreateApi(ctx context.Context, req *CreateApiRequest) error { + api := &system.Api{ + Path: req.Path, + Description: req.Description, + ApiGroup: req.ApiGroup, + Method: req.Method, + } + return s.uc.CreateApi(ctx, api) +} + +// UpdateApiRequest 更新API请求 +type UpdateApiRequest struct { + ID uint `json:"ID"` + Path string `json:"path"` + Description string `json:"description"` + ApiGroup string `json:"apiGroup"` + Method string `json:"method"` +} + +// UpdateApi 更新API +func (s *ApiService) UpdateApi(ctx context.Context, req *UpdateApiRequest) error { + api := &system.Api{ + ID: req.ID, + Path: req.Path, + Description: req.Description, + ApiGroup: req.ApiGroup, + Method: req.Method, + } + return s.uc.UpdateApi(ctx, api) +} + +// DeleteApi 删除API +func (s *ApiService) DeleteApi(ctx context.Context, id uint) error { + return s.uc.DeleteApi(ctx, id) +} + +// DeleteApisByIdsRequest 批量删除API请求 +type DeleteApisByIdsRequest struct { + Ids []uint `json:"ids"` +} + +// DeleteApisByIds 批量删除API +func (s *ApiService) DeleteApisByIds(ctx context.Context, req *DeleteApisByIdsRequest) error { + return s.uc.DeleteApisByIds(ctx, req.Ids) +} + +// GetApiById 根据ID获取API +func (s *ApiService) GetApiById(ctx context.Context, id uint) (*ApiInfo, error) { + api, err := s.uc.GetApiById(ctx, id) + if err != nil { + return nil, err + } + return toApiInfo(api), nil +} + +// GetApiListRequest 获取API列表请求 +type GetApiListRequest struct { + Page int `json:"page"` + PageSize int `json:"pageSize"` + Path string `json:"path"` + Description string `json:"description"` + Method string `json:"method"` + ApiGroup string `json:"apiGroup"` + OrderKey string `json:"orderKey"` + Desc bool `json:"desc"` +} + +// GetApiListResponse 获取API列表响应 +type GetApiListResponse struct { + List []*ApiInfo `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} + +// GetApiList 获取API列表 +func (s *ApiService) GetApiList(ctx context.Context, req *GetApiListRequest) (*GetApiListResponse, error) { + filters := make(map[string]string) + if req.Path != "" { + filters["path"] = req.Path + } + if req.Description != "" { + filters["description"] = req.Description + } + if req.Method != "" { + filters["method"] = req.Method + } + if req.ApiGroup != "" { + filters["api_group"] = req.ApiGroup + } + + apis, total, err := s.uc.GetAPIInfoList(ctx, req.Page, req.PageSize, filters, req.OrderKey, req.Desc) + if err != nil { + return nil, err + } + + list := make([]*ApiInfo, len(apis)) + for i, api := range apis { + list[i] = toApiInfo(api) + } + + return &GetApiListResponse{ + List: list, + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, nil +} + +// GetAllApis 获取所有API +func (s *ApiService) GetAllApis(ctx context.Context, authorityID uint) ([]*ApiInfo, error) { + apis, err := s.uc.GetAllApis(ctx, authorityID) + if err != nil { + return nil, err + } + list := make([]*ApiInfo, len(apis)) + for i, api := range apis { + list[i] = toApiInfo(api) + } + return list, nil +} + +// GetApiGroups 获取API分组 +func (s *ApiService) GetApiGroups(ctx context.Context) ([]string, map[string]string, error) { + return s.uc.GetApiGroups(ctx) +} + +// SyncApiResponse 同步API响应 +type SyncApiResponse struct { + NewApis []*ApiInfo `json:"newApis"` + DeleteApis []*ApiInfo `json:"deleteApis"` + IgnoreApis []*ApiInfo `json:"ignoreApis"` +} + +// SyncApi 同步API +func (s *ApiService) SyncApi(ctx context.Context) (*SyncApiResponse, error) { + newApis, deleteApis, ignoreApis, err := s.uc.SyncApi(ctx) + if err != nil { + return nil, err + } + + resp := &SyncApiResponse{ + NewApis: make([]*ApiInfo, len(newApis)), + DeleteApis: make([]*ApiInfo, len(deleteApis)), + IgnoreApis: make([]*ApiInfo, len(ignoreApis)), + } + for i, api := range newApis { + resp.NewApis[i] = toApiInfo(api) + } + for i, api := range deleteApis { + resp.DeleteApis[i] = toApiInfo(api) + } + for i, api := range ignoreApis { + resp.IgnoreApis[i] = toApiInfo(api) + } + return resp, nil +} + +// IgnoreApiRequest 忽略API请求 +type IgnoreApiRequest struct { + Path string `json:"path"` + Method string `json:"method"` + Flag bool `json:"flag"` +} + +// IgnoreApi 忽略API +func (s *ApiService) IgnoreApi(ctx context.Context, req *IgnoreApiRequest) error { + return s.uc.IgnoreApi(ctx, req.Path, req.Method, req.Flag) +} + +// EnterSyncApiRequest 确认同步API请求 +type EnterSyncApiRequest struct { + NewApis []*ApiInfo `json:"newApis"` + DeleteApis []*ApiInfo `json:"deleteApis"` +} + +// EnterSyncApi 确认同步API +func (s *ApiService) EnterSyncApi(ctx context.Context, req *EnterSyncApiRequest) error { + newApis := make([]*system.Api, len(req.NewApis)) + for i, api := range req.NewApis { + newApis[i] = &system.Api{ + Path: api.Path, + Description: api.Description, + ApiGroup: api.ApiGroup, + Method: api.Method, + } + } + deleteApis := make([]*system.Api, len(req.DeleteApis)) + for i, api := range req.DeleteApis { + deleteApis[i] = &system.Api{ + ID: api.ID, + Path: api.Path, + Method: api.Method, + } + } + return s.uc.EnterSyncApi(ctx, newApis, deleteApis) +} + +// FreshCasbin 刷新Casbin缓存 +func (s *ApiService) FreshCasbin() error { + return s.uc.FreshCasbin() +} + +// 转换函数 +func toApiInfo(api *system.Api) *ApiInfo { + return &ApiInfo{ + ID: api.ID, + Path: api.Path, + Description: api.Description, + ApiGroup: api.ApiGroup, + Method: api.Method, + } +} + +// RegisterRoutes 注册路由 +func (s *ApiService) RegisterRoutes(srv *http.Server) { + r := srv.Route("/") + + r.POST("/api/createApi", s.handleCreateApi) + r.POST("/api/updateApi", s.handleUpdateApi) + r.POST("/api/deleteApi", s.handleDeleteApi) + r.DELETE("/api/deleteApisByIds", s.handleDeleteApisByIds) + r.POST("/api/getApiList", s.handleGetApiList) + r.POST("/api/getApiById", s.handleGetApiById) + r.POST("/api/getAllApis", s.handleGetAllApis) + r.GET("/api/getApiGroups", s.handleGetApiGroups) + r.GET("/api/syncApi", s.handleSyncApi) + r.POST("/api/ignoreApi", s.handleIgnoreApi) + r.POST("/api/enterSyncApi", s.handleEnterSyncApi) + r.GET("/api/freshCasbin", s.handleFreshCasbin) +} + +// HTTP Handlers +func (s *ApiService) handleCreateApi(ctx http.Context) error { + var req CreateApiRequest + if err := ctx.Bind(&req); err != nil { + return err + } + if err := s.CreateApi(ctx, &req); err != nil { + return errors.BadRequest("CREATE_FAILED", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "创建成功"}) +} + +func (s *ApiService) handleUpdateApi(ctx http.Context) error { + var req UpdateApiRequest + if err := ctx.Bind(&req); err != nil { + return err + } + if err := s.UpdateApi(ctx, &req); err != nil { + return errors.BadRequest("UPDATE_FAILED", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "修改成功"}) +} + +func (s *ApiService) handleDeleteApi(ctx http.Context) error { + var req struct { + ID uint `json:"ID"` + } + if err := ctx.Bind(&req); err != nil { + return err + } + if err := s.DeleteApi(ctx, req.ID); err != nil { + return errors.BadRequest("DELETE_FAILED", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "删除成功"}) +} + +func (s *ApiService) handleDeleteApisByIds(ctx http.Context) error { + var req DeleteApisByIdsRequest + if err := ctx.Bind(&req); err != nil { + return err + } + if err := s.DeleteApisByIds(ctx, &req); err != nil { + return errors.BadRequest("DELETE_FAILED", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "删除成功"}) +} + +func (s *ApiService) handleGetApiList(ctx http.Context) error { + var req GetApiListRequest + if err := ctx.Bind(&req); err != nil { + return err + } + resp, err := s.GetApiList(ctx, &req) + if err != nil { + return errors.InternalServer("LIST_ERROR", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "获取成功", "data": resp}) +} + +func (s *ApiService) handleGetApiById(ctx http.Context) error { + var req struct { + ID uint `json:"ID"` + } + if err := ctx.Bind(&req); err != nil { + return err + } + resp, err := s.GetApiById(ctx, req.ID) + if err != nil { + return errors.NotFound("NOT_FOUND", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "获取成功", "data": map[string]any{"api": resp}}) +} + +func (s *ApiService) handleGetAllApis(ctx http.Context) error { + authorityID := middleware.GetAuthorityID(ctx) + resp, err := s.GetAllApis(ctx, authorityID) + if err != nil { + return errors.InternalServer("LIST_ERROR", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "获取成功", "data": map[string]any{"apis": resp}}) +} + +func (s *ApiService) handleGetApiGroups(ctx http.Context) error { + groups, apiGroupMap, err := s.GetApiGroups(ctx) + if err != nil { + return errors.InternalServer("LIST_ERROR", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "获取成功", "data": map[string]any{"groups": groups, "apiGroupMap": apiGroupMap}}) +} + +func (s *ApiService) handleSyncApi(ctx http.Context) error { + resp, err := s.SyncApi(ctx) + if err != nil { + return errors.InternalServer("SYNC_ERROR", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "data": resp}) +} + +func (s *ApiService) handleIgnoreApi(ctx http.Context) error { + var req IgnoreApiRequest + if err := ctx.Bind(&req); err != nil { + return err + } + if err := s.IgnoreApi(ctx, &req); err != nil { + return errors.BadRequest("IGNORE_FAILED", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "操作成功"}) +} + +func (s *ApiService) handleEnterSyncApi(ctx http.Context) error { + var req EnterSyncApiRequest + if err := ctx.Bind(&req); err != nil { + return err + } + if err := s.EnterSyncApi(ctx, &req); err != nil { + return errors.BadRequest("SYNC_FAILED", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "同步成功"}) +} + +func (s *ApiService) handleFreshCasbin(ctx http.Context) error { + if err := s.FreshCasbin(); err != nil { + return errors.InternalServer("FRESH_FAILED", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "刷新成功"}) +} diff --git a/internal/service/system/menu.go b/internal/service/system/menu.go new file mode 100644 index 0000000..dc24567 --- /dev/null +++ b/internal/service/system/menu.go @@ -0,0 +1,440 @@ +package system + +import ( + "context" + + "kra/internal/biz/system" + "kra/internal/server/middleware" + + "github.com/go-kratos/kratos/v2/errors" + "github.com/go-kratos/kratos/v2/transport/http" +) + +// MenuService 菜单服务 +type MenuService struct { + uc *system.MenuUsecase +} + +// NewMenuService 创建菜单服务 +func NewMenuService(uc *system.MenuUsecase) *MenuService { + return &MenuService{uc: uc} +} + +// MenuInfo 菜单信息 +type MenuInfo struct { + ID uint `json:"ID"` + ParentId uint `json:"parentId"` + Path string `json:"path"` + Name string `json:"name"` + Hidden bool `json:"hidden"` + Component string `json:"component"` + Sort int `json:"sort"` + Meta MenuMeta `json:"meta"` + AuthorityId uint `json:"authorityId,omitempty"` + MenuId uint `json:"menuId,omitempty"` + Parameters []*MenuParamInfo `json:"parameters,omitempty"` + MenuBtn []*MenuBtnInfo `json:"menuBtn,omitempty"` + Btns map[string]uint `json:"btns,omitempty"` + Children []*MenuInfo `json:"children,omitempty"` +} + +// MenuMeta 菜单元信息 +type MenuMeta struct { + ActiveName string `json:"activeName"` + KeepAlive bool `json:"keepAlive"` + DefaultMenu bool `json:"defaultMenu"` + Title string `json:"title"` + Icon string `json:"icon"` + CloseTab bool `json:"closeTab"` + TransitionType string `json:"transitionType"` +} + +// MenuParamInfo 菜单参数信息 +type MenuParamInfo struct { + ID uint `json:"ID"` + SysBaseMenuID uint `json:"sysBaseMenuId"` + Type string `json:"type"` + Key string `json:"key"` + Value string `json:"value"` +} + +// MenuBtnInfo 菜单按钮信息 +type MenuBtnInfo struct { + ID uint `json:"ID"` + SysBaseMenuID uint `json:"sysBaseMenuId"` + Name string `json:"name"` + Desc string `json:"desc"` +} + +// BaseMenuRequest 基础菜单请求 +type BaseMenuRequest struct { + ID uint `json:"ID"` + ParentId uint `json:"parentId"` + Path string `json:"path"` + Name string `json:"name"` + Hidden bool `json:"hidden"` + Component string `json:"component"` + Sort int `json:"sort"` + Meta MenuMeta `json:"meta"` + Parameters []*MenuParamInfo `json:"parameters"` + MenuBtn []*MenuBtnInfo `json:"menuBtn"` +} + +// GetMenu 获取用户动态路由 +func (s *MenuService) GetMenu(ctx context.Context, authorityId uint) ([]*MenuInfo, error) { + menus, err := s.uc.GetMenuTree(ctx, authorityId) + if err != nil { + return nil, err + } + return toMenuInfoList(menus), nil +} + +// GetBaseMenuTree 获取基础菜单树 +func (s *MenuService) GetBaseMenuTree(ctx context.Context, authorityId uint) ([]*MenuInfo, error) { + menus, err := s.uc.GetBaseMenuTree(ctx, authorityId) + if err != nil { + return nil, err + } + return toBaseMenuInfoList(menus), nil +} + +// GetMenuList 获取菜单列表 +func (s *MenuService) GetMenuList(ctx context.Context, authorityId uint) ([]*MenuInfo, error) { + menus, err := s.uc.GetInfoList(ctx, authorityId) + if err != nil { + return nil, err + } + return toBaseMenuInfoList(menus), nil +} + +// AddBaseMenu 添加基础菜单 +func (s *MenuService) AddBaseMenu(ctx context.Context, req *BaseMenuRequest) error { + menu := toBaseMenu(req) + return s.uc.AddBaseMenu(ctx, menu) +} + +// UpdateBaseMenu 更新基础菜单 +func (s *MenuService) UpdateBaseMenu(ctx context.Context, req *BaseMenuRequest) error { + menu := toBaseMenu(req) + return s.uc.UpdateBaseMenu(ctx, menu) +} + +// DeleteBaseMenu 删除基础菜单 +func (s *MenuService) DeleteBaseMenu(ctx context.Context, id uint) error { + return s.uc.DeleteBaseMenu(ctx, id) +} + +// GetBaseMenuById 根据ID获取基础菜单 +func (s *MenuService) GetBaseMenuById(ctx context.Context, id uint) (*MenuInfo, error) { + menu, err := s.uc.GetBaseMenuById(ctx, id) + if err != nil { + return nil, err + } + return toBaseMenuInfo(menu), nil +} + +// AddMenuAuthorityRequest 添加菜单权限请求 +type AddMenuAuthorityRequest struct { + AuthorityId uint `json:"authorityId"` + Menus []*BaseMenuRequest `json:"menus"` +} + +// AddMenuAuthority 添加菜单权限 +func (s *MenuService) AddMenuAuthority(ctx context.Context, adminAuthorityID uint, req *AddMenuAuthorityRequest) error { + menus := make([]*system.BaseMenu, len(req.Menus)) + for i, m := range req.Menus { + menus[i] = toBaseMenu(m) + } + return s.uc.AddMenuAuthority(ctx, menus, adminAuthorityID, req.AuthorityId) +} + +// GetMenuAuthority 获取角色菜单权限 +func (s *MenuService) GetMenuAuthority(ctx context.Context, authorityId uint) ([]*MenuInfo, error) { + menus, err := s.uc.GetMenuAuthority(ctx, authorityId) + if err != nil { + return nil, err + } + return toMenuInfoList(menus), nil +} + +// 转换函数 +func toBaseMenu(req *BaseMenuRequest) *system.BaseMenu { + menu := &system.BaseMenu{ + ID: req.ID, + ParentId: req.ParentId, + Path: req.Path, + Name: req.Name, + Hidden: req.Hidden, + Component: req.Component, + Sort: req.Sort, + ActiveName: req.Meta.ActiveName, + KeepAlive: req.Meta.KeepAlive, + DefaultMenu: req.Meta.DefaultMenu, + Title: req.Meta.Title, + Icon: req.Meta.Icon, + CloseTab: req.Meta.CloseTab, + TransitionType: req.Meta.TransitionType, + } + + if len(req.Parameters) > 0 { + menu.Parameters = make([]*system.MenuParameter, len(req.Parameters)) + for i, p := range req.Parameters { + menu.Parameters[i] = &system.MenuParameter{ + ID: p.ID, + SysBaseMenuID: p.SysBaseMenuID, + Type: p.Type, + Key: p.Key, + Value: p.Value, + } + } + } + + if len(req.MenuBtn) > 0 { + menu.MenuBtn = make([]*system.MenuBtn, len(req.MenuBtn)) + for i, b := range req.MenuBtn { + menu.MenuBtn[i] = &system.MenuBtn{ + ID: b.ID, + SysBaseMenuID: b.SysBaseMenuID, + Name: b.Name, + Desc: b.Desc, + } + } + } + + return menu +} + +func toMenuInfoList(menus []*system.Menu) []*MenuInfo { + if menus == nil { + return []*MenuInfo{} + } + result := make([]*MenuInfo, len(menus)) + for i, m := range menus { + result[i] = toMenuInfo(m) + } + return result +} + +func toMenuInfo(m *system.Menu) *MenuInfo { + info := &MenuInfo{ + ID: m.ID, + ParentId: m.ParentId, + Path: m.Path, + Name: m.Name, + Hidden: m.Hidden, + Component: m.Component, + Sort: m.Sort, + AuthorityId: m.AuthorityId, + MenuId: m.MenuId, + Btns: m.Btns, + Meta: MenuMeta{ + ActiveName: m.ActiveName, + KeepAlive: m.KeepAlive, + DefaultMenu: m.DefaultMenu, + Title: m.Title, + Icon: m.Icon, + CloseTab: m.CloseTab, + TransitionType: m.TransitionType, + }, + } + + if len(m.Parameters) > 0 { + info.Parameters = make([]*MenuParamInfo, len(m.Parameters)) + for i, p := range m.Parameters { + info.Parameters[i] = &MenuParamInfo{ + ID: p.ID, + SysBaseMenuID: p.SysBaseMenuID, + Type: p.Type, + Key: p.Key, + Value: p.Value, + } + } + } + + if len(m.Children) > 0 { + info.Children = toMenuInfoList(m.Children) + } + + return info +} + +func toBaseMenuInfoList(menus []*system.BaseMenu) []*MenuInfo { + if menus == nil { + return []*MenuInfo{} + } + result := make([]*MenuInfo, len(menus)) + for i, m := range menus { + result[i] = toBaseMenuInfo(m) + } + return result +} + +func toBaseMenuInfo(m *system.BaseMenu) *MenuInfo { + info := &MenuInfo{ + ID: m.ID, + ParentId: m.ParentId, + Path: m.Path, + Name: m.Name, + Hidden: m.Hidden, + Component: m.Component, + Sort: m.Sort, + Meta: MenuMeta{ + ActiveName: m.ActiveName, + KeepAlive: m.KeepAlive, + DefaultMenu: m.DefaultMenu, + Title: m.Title, + Icon: m.Icon, + CloseTab: m.CloseTab, + TransitionType: m.TransitionType, + }, + } + + if len(m.Parameters) > 0 { + info.Parameters = make([]*MenuParamInfo, len(m.Parameters)) + for i, p := range m.Parameters { + info.Parameters[i] = &MenuParamInfo{ + ID: p.ID, + SysBaseMenuID: p.SysBaseMenuID, + Type: p.Type, + Key: p.Key, + Value: p.Value, + } + } + } + + if len(m.MenuBtn) > 0 { + info.MenuBtn = make([]*MenuBtnInfo, len(m.MenuBtn)) + for i, b := range m.MenuBtn { + info.MenuBtn[i] = &MenuBtnInfo{ + ID: b.ID, + SysBaseMenuID: b.SysBaseMenuID, + Name: b.Name, + Desc: b.Desc, + } + } + } + + if len(m.Children) > 0 { + info.Children = toBaseMenuInfoList(m.Children) + } + + return info +} + +// RegisterRoutes 注册路由 +func (s *MenuService) RegisterRoutes(srv *http.Server) { + r := srv.Route("/") + + r.POST("/menu/getMenu", s.handleGetMenu) + r.POST("/menu/getBaseMenuTree", s.handleGetBaseMenuTree) + r.POST("/menu/getMenuList", s.handleGetMenuList) + r.POST("/menu/addBaseMenu", s.handleAddBaseMenu) + r.POST("/menu/updateBaseMenu", s.handleUpdateBaseMenu) + r.POST("/menu/deleteBaseMenu", s.handleDeleteBaseMenu) + r.POST("/menu/getBaseMenuById", s.handleGetBaseMenuById) + r.POST("/menu/addMenuAuthority", s.handleAddMenuAuthority) + r.POST("/menu/getMenuAuthority", s.handleGetMenuAuthority) +} + +// HTTP Handlers +func (s *MenuService) handleGetMenu(ctx http.Context) error { + authorityId := middleware.GetAuthorityID(ctx) + menus, err := s.GetMenu(ctx, authorityId) + if err != nil { + return errors.InternalServer("GET_MENU_FAILED", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "获取成功", "data": map[string]any{"menus": menus}}) +} + +func (s *MenuService) handleGetBaseMenuTree(ctx http.Context) error { + authorityId := middleware.GetAuthorityID(ctx) + menus, err := s.GetBaseMenuTree(ctx, authorityId) + if err != nil { + return errors.InternalServer("GET_MENU_FAILED", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "获取成功", "data": map[string]any{"menus": menus}}) +} + +func (s *MenuService) handleGetMenuList(ctx http.Context) error { + authorityId := middleware.GetAuthorityID(ctx) + menus, err := s.GetMenuList(ctx, authorityId) + if err != nil { + return errors.InternalServer("GET_MENU_FAILED", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "获取成功", "data": menus}) +} + +func (s *MenuService) handleAddBaseMenu(ctx http.Context) error { + var req BaseMenuRequest + if err := ctx.Bind(&req); err != nil { + return err + } + if err := s.AddBaseMenu(ctx, &req); err != nil { + return errors.BadRequest("ADD_MENU_FAILED", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "添加成功"}) +} + +func (s *MenuService) handleUpdateBaseMenu(ctx http.Context) error { + var req BaseMenuRequest + if err := ctx.Bind(&req); err != nil { + return err + } + if err := s.UpdateBaseMenu(ctx, &req); err != nil { + return errors.BadRequest("UPDATE_MENU_FAILED", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "更新成功"}) +} + +func (s *MenuService) handleDeleteBaseMenu(ctx http.Context) error { + var req struct { + ID uint `json:"ID"` + } + if err := ctx.Bind(&req); err != nil { + return err + } + if err := s.DeleteBaseMenu(ctx, req.ID); err != nil { + return errors.BadRequest("DELETE_MENU_FAILED", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "删除成功"}) +} + +func (s *MenuService) handleGetBaseMenuById(ctx http.Context) error { + var req struct { + ID uint `json:"ID"` + } + if err := ctx.Bind(&req); err != nil { + return err + } + menu, err := s.GetBaseMenuById(ctx, req.ID) + if err != nil { + return errors.NotFound("MENU_NOT_FOUND", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "获取成功", "data": map[string]any{"menu": menu}}) +} + +func (s *MenuService) handleAddMenuAuthority(ctx http.Context) error { + var req AddMenuAuthorityRequest + if err := ctx.Bind(&req); err != nil { + return err + } + adminAuthorityID := middleware.GetAuthorityID(ctx) + if err := s.AddMenuAuthority(ctx, adminAuthorityID, &req); err != nil { + return errors.BadRequest("ADD_AUTHORITY_FAILED", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "添加成功"}) +} + +func (s *MenuService) handleGetMenuAuthority(ctx http.Context) error { + var req struct { + AuthorityId uint `json:"authorityId"` + } + if err := ctx.Bind(&req); err != nil { + return err + } + menus, err := s.GetMenuAuthority(ctx, req.AuthorityId) + if err != nil { + return errors.InternalServer("GET_AUTHORITY_FAILED", err.Error()) + } + return ctx.Result(200, map[string]any{"code": 0, "msg": "获取成功", "data": map[string]any{"menus": menus}}) +}