package service import ( "cold-delivery/app/admin/model" "cold-delivery/app/admin/service/dto" "cold-delivery/common/actions" cDto "cold-delivery/common/dto" "cold-delivery/common/global" "errors" "fmt" "strings" "github.com/casbin/casbin/v2" "gogs.baozhida.cn/zoie/OAuth-core/sdk" "gogs.baozhida.cn/zoie/OAuth-core/sdk/config" "gogs.baozhida.cn/zoie/OAuth-core/service" "gorm.io/gorm" ) type SysPost struct { service.Service } // GetPage 分页查询岗位列表 func (e *SysPost) GetPage(c *dto.SysPostGetPageReq, list *[]model.SysPost, count *int64, p *actions.DataPermission) error { var err error var data model.SysPost db := e.Orm.Model(&data). Scopes( cDto.MakeCondition(c.GetNeedSearch()), cDto.Paginate(c.GetPageSize(), c.GetPageIndex()), ) // 超级管理员(deptId=0)可以看到所有部门的岗位 if p.DeptId != 0 { db = db.Scopes(actions.Permission(data.TableName(), p)) } err = db.Find(list).Limit(-1).Offset(-1). Count(count).Error if err != nil { e.Log.Errorf("db error: %s", err) return global.GetFailedErr } return nil } // Get 获取单个岗位详情(含权限菜单ID列表) func (e *SysPost) Get(d *dto.SysPostGetReq, postModel *model.SysPost, p *actions.DataPermission) error { db := e.Orm // 超级管理员可以看到所有部门的岗位 if p.DeptId != 0 { db = db.Scopes(actions.Permission(postModel.TableName(), p)) } err := db.First(postModel, d.GetId()).Error if err != nil { e.Log.Errorf("db error: %s", err) if errors.Is(err, gorm.ErrRecordNotFound) { return global.GetNotFoundOrNoPermissionErr } return global.GetFailedErr } postModel.MenuIds, err = e.GetPostMenuIds(postModel.Id) if err != nil { e.Log.Errorf("get menuIds error, %s", err) return global.GetFailedErr } return nil } // Insert 创建岗位(含菜单权限分配) func (e *SysPost) Insert(c *dto.SysPostInsertReq) error { var err error var data model.SysPost tx := e.Orm.Begin() defer func() { if err != nil { tx.Rollback() } else { tx.Commit() } }() // 检查岗位编码在同一个部门下是否已存在 var count int64 err = tx.Model(&data).Where("post_code = ? AND dept_id = ?", c.PostCode, c.DeptId).Count(&count).Error if err != nil { e.Log.Errorf("db error: %s", err) return global.CreateFailedErr } if count > 0 { return errors.New("该部门下岗位编码已存在") } // 创建岗位 c.Generate(&data) err = tx.Create(&data).Error if err != nil { e.Log.Errorf("db error: %s", err) return global.CreateFailedErr } c.Id = data.Id // 添加岗位菜单关联 if len(c.MenuIds) > 0 { var menuList []model.SysMenu err = tx.Where("id in (?)", c.MenuIds).Find(&menuList).Error if err != nil { return global.GetFailedErr } if len(menuList) == 0 { return errors.New("菜单不存在!") } var postMenuList []model.SysPostMenu for _, v := range menuList { postMenuObj := model.SysPostMenu{ PostId: data.Id, MenuId: v.Id, } postMenuObj.SetCreateBy(c.CreateBy) postMenuList = append(postMenuList, postMenuObj) } err = tx.Save(&postMenuList).Error if err != nil { e.Log.Errorf("save menuIds error, %s", err) return global.UpdateFailedErr } // 同步写入 Casbin 规则 err = e.syncCasbinRules(tx, data, menuList) if err == nil { cb := sdk.Runtime.GetCasbinKey(config.ApplicationConfig.Host) if cb != nil { cb.LoadPolicy() } } if err != nil { e.Log.Errorf("sync casbin rules error, %s", err) return global.UpdateFailedErr } } return nil } // Update 修改岗位(含菜单权限重新分配) func (e *SysPost) Update(c *dto.SysPostUpdateReq, p *actions.DataPermission, cb *casbin.SyncedEnforcer) error { var err error tx := e.Orm.Begin() defer func() { if err != nil { tx.Rollback() } else { tx.Commit() } }() var postModel = model.SysPost{} // 查询岗位是否存在 db := e.Orm if p.DeptId != 0 { db = db.Scopes(actions.Permission(postModel.TableName(), p)) } err = db.First(&postModel, c.GetId()).Error if err != nil { e.Log.Errorf("db error: %s", err) if errors.Is(err, gorm.ErrRecordNotFound) { return global.UpdateNotFoundOrNoPermissionErr } return global.UpdateFailedErr } // 更新岗位基本信息 c.Generate(&postModel) // deptId 由 Generate 方法从更新请求中设置,允许修改部门 err = tx.Save(&postModel).Error if err != nil { if errors.Is(err, model.ErrForbidUpdateSysPost) { return err } e.Log.Errorf("db error: %s", err) return global.UpdateFailedErr } // 清除旧的 Casbin 规则(用旧的 postCode) _, _ = cb.RemoveFilteredPolicy(0, "role:"+postModel.PostCode) _, _ = cb.RemoveFilteredPolicy(0, postModel.PostCode) // 清除旧格式规则(无 role: 前缀) // 删除旧的菜单关联 err = tx.Where("post_id = ?", postModel.Id).Delete(&model.SysPostMenu{}).Error if err != nil { e.Log.Errorf("db error: %s", err) return global.UpdateFailedErr } // 重新添加菜单关联 if len(c.MenuIds) > 0 { var menuList []model.SysMenu err = e.Orm.Where("id in (?)", c.MenuIds).Find(&menuList).Error if err != nil { return global.GetFailedErr } if len(menuList) == 0 { return errors.New("菜单不存在!") } var postMenuList []model.SysPostMenu for _, v := range menuList { postMenuObj := model.SysPostMenu{ PostId: postModel.Id, MenuId: v.Id, } postMenuObj.SetCreateBy(c.UpdateBy) postMenuList = append(postMenuList, postMenuObj) } err = tx.Save(&postMenuList).Error if err != nil { e.Log.Errorf("save menuIds error, %s", err) return global.UpdateFailedErr } // 同步写入 Casbin 规则 err = e.syncCasbinRules(tx, postModel, menuList) if err == nil { cb.LoadPolicy() } if err != nil { e.Log.Errorf("sync casbin rules error, %s", err) return global.UpdateFailedErr } } // 清除缓存 _ = model.DeletePostMenuCache(postModel.Id) c.Id = postModel.Id return nil } // Remove 删除岗位(逻辑删除) func (e *SysPost) Remove(c *dto.SysPostDeleteReq, p *actions.DataPermission, cb *casbin.SyncedEnforcer) error { var err error tx := e.Orm.Begin() defer func() { if err != nil { tx.Rollback() } else { tx.Commit() } }() // 检查是否有用户关联该岗位 var userCount int64 err = tx.Table("sys_user").Where("post_id = ? AND deleted_at IS NULL", c.Id).Count(&userCount).Error if err != nil { e.Log.Errorf("db error: %s", err) return global.DeleteFailedErr } if userCount > 0 { return errors.New(fmt.Sprintf("有 %d 个用户关联了该岗位,禁止删除!", userCount)) } var postModel model.SysPost rdb := e.Orm if p.DeptId != 0 { rdb = rdb.Scopes(actions.Permission(postModel.TableName(), p)) } err = rdb.First(&postModel, c.GetId()).Error if err != nil { e.Log.Errorf("db error: %s", err) if errors.Is(err, gorm.ErrRecordNotFound) { return global.DeleteNotFoundOrNoPermissionErr } return global.DeleteFailedErr } // GORM 逻辑删除(自动设置 deleted_at) db := tx.Delete(&postModel) if err = db.Error; err != nil { if errors.Is(err, model.ErrForbidDeleteSysPost) { return err } e.Log.Errorf("db error: %s", err) return global.DeleteFailedErr } if db.RowsAffected == 0 { return global.DeleteNotFoundOrNoPermissionErr } // 删除岗位关联的菜单 err = tx.Where("post_id = ?", postModel.Id).Delete(&model.SysPostMenu{}).Error if err != nil { return global.UpdateFailedErr } // 清除 Casbin 规则 _, _ = cb.RemoveFilteredPolicy(0, "role:"+postModel.PostCode) _, _ = cb.RemoveFilteredPolicy(0, postModel.PostCode) // 清除旧格式规则(无 role: 前缀) // 清除缓存 _ = model.DeletePostMenuCache(postModel.Id) return nil } // GetAll 获取所有岗位(下拉列表用) func (e *SysPost) GetAll(deptId int, list *[]model.SysPost, p *actions.DataPermission) error { var err error var data model.SysPost db := e.Orm.Model(&data) // 超级管理员可以看到所有部门的岗位 if p.DeptId != 0 { db = db.Scopes(actions.Permission(data.TableName(), p)) } if deptId > 0 { db = db.Where("dept_id = ?", deptId) } err = db.Order("sort asc").Find(list).Error if err != nil { e.Log.Errorf("db error: %s", err) return global.GetFailedErr } return nil } // GetPostMenuIds 获取岗位已分配的菜单ID列表 func (e *SysPost) GetPostMenuIds(postId int) ([]int, error) { var menuIds []int err := e.Orm.Model(&model.SysPostMenu{}).Where("post_id = ?", postId).Pluck("menu_id", &menuIds).Error if err != nil { return []int{}, global.GetFailedErr } return menuIds, nil } // GetMenuTree 获取完整菜单树(用于权限分配界面) func (e *SysPost) GetMenuTree() ([]dto.MenuLabel, error) { // 直接查询所有菜单,不依赖 SysMenu.GetList 的复杂搜索逻辑 var menuList []model.SysMenu err := e.Orm.Order("sort asc").Find(&menuList).Error if err != nil { e.Log.Errorf("db error: %s", err) return nil, global.GetFailedErr } // 构建菜单树 tree := make([]dto.MenuLabel, 0) for i := 0; i < len(menuList); i++ { if menuList[i].ParentId != 0 { continue } label := dto.MenuLabel{} label.Id = menuList[i].Id label.Label = menuList[i].Title buildMenuLabelTree(&menuList, &label) tree = append(tree, label) } return tree, nil } // buildMenuLabelTree 递归构建菜单标签树 func buildMenuLabelTree(menuList *[]model.SysMenu, parent *dto.MenuLabel) { list := *menuList children := make([]dto.MenuLabel, 0) for j := 0; j < len(list); j++ { if parent.Id != list[j].ParentId { continue } child := dto.MenuLabel{} child.Id = list[j].Id child.Label = list[j].Title child.Children = []dto.MenuLabel{} if list[j].MenuType != "F" { buildMenuLabelTree(menuList, &child) } children = append(children, child) } if len(children) > 0 { parent.Children = children } else { parent.Children = nil } } // FilterMenuTree 根据允许的菜单ID过滤菜单树 func (e *SysPost) FilterMenuTree(fullTree []dto.MenuLabel, allowedIds []int) []dto.MenuLabel { if len(allowedIds) == 0 { return nil } allowedMap := make(map[int]bool) for _, id := range allowedIds { allowedMap[id] = true } var filter func(nodes []dto.MenuLabel) []dto.MenuLabel filter = func(nodes []dto.MenuLabel) []dto.MenuLabel { result := make([]dto.MenuLabel, 0) for _, node := range nodes { // Only include if allowed or has allowed children if allowedMap[node.Id] || hasAllowedChild(node.Children, allowedMap) { child := dto.MenuLabel{ Id: node.Id, Label: node.Label, Children: filter(node.Children), } result = append(result, child) } } return result } return filter(fullTree) } func hasAllowedChild(children []dto.MenuLabel, allowedMap map[int]bool) bool { for _, c := range children { if allowedMap[c.Id] || hasAllowedChild(c.Children, allowedMap) { return true } } return false } // syncCasbinRules 同步岗位权限到 Casbin 规则表 func (e *SysPost) syncCasbinRules(tx *gorm.DB, post model.SysPost, menuList []model.SysMenu) error { // 获取完整的菜单信息(包括父菜单的路由权限) var allMenus []model.SysMenu menuIds := make([]int, 0) for _, m := range menuList { menuIds = append(menuIds, m.Id) } // 递归获取所有父级菜单的权限路径 err := e.resolveMenuPaths(menuIds, &allMenus) if err != nil { return err } // 为每个菜单路径写入 Casbin 规则 // Casbin 规则格式: p = postCode, domain, path, method // 在 Casbin 中: sub = postCode, dom = service::api, obj = path, act = method for _, menu := range allMenus { if menu.Path == "" { continue } var methods []string if menu.MenuType == "C" { methods = []string{"GET"} } else if menu.MenuType == "M" { continue } else if menu.MenuType == "F" { methods = mapButtonToMethods(menu.Title) if len(methods) == 0 { continue } } for _, method := range methods { // 使用原始 SQL 插入避免与 Casbin adapter 冲突 // 检查是否已存在 var count int64 sub := "role:" + post.PostCode tx.Table("sys_casbin_rule"). Where("p_type = 'p' AND v0 = ? AND v1 = ? AND v2 = ? AND v3 = ?", sub, "service::api", menu.Path, method). Count(&count) if count == 0 { err = tx.Exec( "INSERT INTO sys_casbin_rule (p_type, v0, v1, v2, v3, v4, v5) VALUES ('p', ?, ?, ?, ?, '', '')", sub, "service::api", menu.Path, method, ).Error if err != nil { return err } } } } // 写入 g 规则(Casbin RBAC 角色继承) sub := "role:" + post.PostCode var gCount int64 tx.Table("sys_casbin_rule"). Where("p_type = 'g' AND v0 = ? AND v1 = ? AND v2 = ?", sub, sub, "service::api"). Count(&gCount) if gCount == 0 { err = tx.Exec( "INSERT INTO sys_casbin_rule (p_type, v0, v1, v2, v3, v4, v5) VALUES ('g', ?, ?, ?, '', '', '')", sub, sub, "service::api", ).Error if err != nil { return err } } return nil } // mapButtonToMethods 将按钮名称映射为 HTTP 方法 func mapButtonToMethods(title string) []string { title = strings.TrimSpace(title) switch { case strings.Contains(title, "查询"): return []string{"GET"} case strings.Contains(title, "新增") || strings.Contains(title, "导入"): return []string{"POST"} case strings.Contains(title, "编辑") || strings.Contains(title, "修改"): return []string{"PUT"} case strings.Contains(title, "删除"): return []string{"DELETE"} case strings.Contains(title, "重置") || strings.Contains(title, "审核"): return []string{"PUT"} case strings.Contains(title, "分配"): return []string{"POST"} default: return []string{"GET"} } } // resolveMenuPaths 递归获取菜单及其所有父菜单 func (e *SysPost) resolveMenuPaths(menuIds []int, result *[]model.SysMenu) error { if len(menuIds) == 0 { return nil } var menus []model.SysMenu err := e.Orm.Where("id in (?)", menuIds).Find(&menus).Error if err != nil { return err } parentIds := make([]int, 0) seen := make(map[int]bool) for _, m := range *result { seen[m.Id] = true } for _, m := range menus { if !seen[m.Id] { *result = append(*result, m) seen[m.Id] = true } if m.ParentId != 0 && !seen[m.ParentId] { parentIds = append(parentIds, m.ParentId) } } if len(parentIds) > 0 { return e.resolveMenuPaths(parentIds, result) } return nil } // GetWithName 根据岗位名称查询 func (e *SysPost) GetWithName(name string, deptId int, postModel *model.SysPost) *SysPost { db := e.Orm.Where("post_name = ?", name) if deptId > 0 { db = db.Where("dept_id = ?", deptId) } err := db.First(postModel).Error if err != nil { e.Log.Errorf("db error: %s", err) if errors.Is(err, gorm.ErrRecordNotFound) { err = global.GetNotFoundOrNoPermissionErr } else { err = global.GetFailedErr } _ = e.AddError(err) return e } return e } // GetPostNamesByIds 根据岗位ID列表获取岗位名称映射 func (e *SysPost) GetPostNamesByIds(ids []int) (map[int]string, error) { if len(ids) == 0 { return map[int]string{}, nil } var posts []model.SysPost err := e.Orm.Where("id in (?)", ids).Find(&posts).Error if err != nil { e.Log.Errorf("db error: %s", err) return nil, global.GetFailedErr } result := make(map[int]string) for _, p := range posts { result[p.Id] = p.PostName } return result, nil } // GetPostById 通过岗位ID获取菜单权限标识列表 func (e *SysPost) GetPostById(postId int) ([]string, error) { permissions := make([]string, 0) postModel := model.SysPost{} postModel.Id = postId err := e.Orm.Model(&postModel).First(&postModel).Error if err != nil { e.Log.Errorf("db error: %s", err) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, global.GetNotFoundOrNoPermissionErr } return nil, global.GetFailedErr } // 查询岗位关联的菜单权限标识 var menuPerms []string err = e.Orm.Model(&model.SysMenu{}). Joins("JOIN sys_post_menu ON sys_post_menu.menu_id = sys_menu.id"). Where("sys_post_menu.post_id = ? AND sys_menu.permission != ''", postId). Pluck("sys_menu.permission", &menuPerms).Error if err != nil { return nil, global.GetFailedErr } permissions = append(permissions, menuPerms...) return permissions, nil }