:菜单规则管理实现)
这是一个系列 Blog作者将以一个 PHP 全栈工程师的身份利用 AI 工具claude code、codex、deepseek、豆包等从零开始学习 golang 语言并最终完成 ai-go-admingithub | gitee开源项目的制作全程记录分享。在上一期我们进行了 “列表通用排序接口实现增量重排法”本期将完成菜单规则管理实现菜单规则管理实现我们之前已经写好了AdminRule模型这里提供向 AI 详细的提供模型位置和新文件位置并提供参考代码基于internal/model/admin.go的AdminRule模型在internal/handler/admin/auth/建立AuthAdminRuleHandler控制器并建立对应的仓储、服务等可参考已有的AuthAdminHandler及其仓储、服务。已完成 AdminRule 的 CRUD 骨架搭建新增4个文件并通过 go build、go vet、gotest校验。 新增文件 internal/repository/admin/rule.go — AdminRuleRepository internal/service/admin/auth/rule.go — AuthAdminRuleService internal/handler/admin/auth/rule.go — AuthAdminRuleHandler internal/router/admin/auth/rule.go — 路由注册# ......先简单检查了一下还行好家伙考虑未来不做可视化CRUD了。数据效验目前 AI 写的效验全部是在服务层新增了一个validateRule函数里边写了大量if用来检查name不能为空、pid不能为自身等这当然不太合理gin的ShouldBindJSON方法本身就支持数据绑定同时完成效验而且我们在基控制器已经调用过该方法了所以必填、格式正确性等验证直接在模型上加bindingtag 即可。也可以写validatetag它是专门做验证的包https://github.com/go-playground/validator的语法该依赖已随gin框架内置可直接使用。还有一些涉及数据库、父子关系、唯一性、业务联动的效验必须抽到服务层这里有两个选择使用validatetag然后在服务层完成效验或直接代码里边if这里选择后者因为这次验证不算多且对验证消息的要求比较高如下// 主要就是因为这个验证选择统一使用 if这里的验证消息定制化较高ifentity.Typemenu(entity.Path||entity.Component){returnerrors.New(规则类型为菜单时菜单路由路径和菜单组件路径不能为空)}// 名称唯一校验排除自身existing,err:s.repo.FindByName(c,entity.Name)iferr!nil!errors.Is(err,gorm.ErrRecordNotFound){returnerr}ifexisting!nilstrconv.FormatUint(uint64(existing.ID),10)!pk{returnerrors.New(规则名称已存在)}// 路由路径的唯一性验证// ......// 不能将自身设为自己的上级 上级存在性检查// ......方法位置错误findByName根据规则名称查询和findByPath根据菜单路由路径查询方法AI 给放在了服务层可能是因为目前这两个方法仅用于 数据效验 时查数据。这里让它移动至仓储层一是因为职能规划二是这两个方法未来其他地方需要使用的可能性很大仓储里边有它们非常合理。完成移动只需要一句话然后已移至 internal/repository/admin/rule.go FindByName 和 FindByPath 已移至 internal/repository/admin/rule.go。 两个仓储方法均使用 GORM Generics API。 服务层改为调用 s.repo.FindByName(c, entity.Name)s.repo.FindByPath(c, entity.Path)删除服务层原有的 findByName、findByPath 私有方法。 服务层仍负责唯一性判断和业务错误消息。 已格式化并通过仓储层、服务层相关包测试。菜单规则管理定制功能菜单规则作为权限系统的基础此功能当然不是一个CRUD就能搞定的接下来基于已有CRUD进行一系列定制控制器层自定义扩展数据和列表数据适配器菜单规则管理的菜单列表需要组装为可以展开折叠的树状表格数据使用tree.Build即可选择器如远程下拉读取菜单列表时需要组装为另外一种树状列表在title字段上渲染树状分支符号使用tree.Render即可只读取当前登录管理员拥有权限的菜单规则使用handler.WithExtension自定义扩展数据传递当前登录管理员的数据到服务层供后续使用// NewAuthAdminRuleHandler 创建菜单和权限规则管理控制器实例funcNewAuthAdminRuleHandler(svc*svcAuth.AuthAdminRuleService)*AuthAdminRuleHandler{returnAuthAdminRuleHandler{Handler:handler.NewHandler(svc,handler.WithAdapter(handler.Adapter{// 定义控制器层的数据适配器就不需要重写控制器层的 List 方法了List:func(data any,opts service.Options)(any,error){rules,ok:data.([]model.AdminRule)if!ok{returndata,nil}ruleData:make([]map[string]any,len(rules))fori:rangerules{ruleData[i]rules[i].ToMap()}ifopts.Selector{returntree.Render(ruleData,id,pid,title),nil}else{returntree.Build(ruleData,id,pid,children),nil}},}),handler.WithExtension(func(c*gin.Context)any{returnsvcAuth.AuthAdminRuleExtension{AdminSession:middleware.GetAdmin(c),}}),),svc:svc,}}服务层List 方法读取自定义扩展中的管理员数据并使用Permission包读取当前登录管理员拥有权限的菜单规则。// List 覆写通用查询全部记录方法: 根据管理员权限过滤func(s*AuthAdminRuleService)List(c*gin.Context,opts service.Options)([]model.AdminRule,error){// 从控制器传来的 管理员信息 扩展数据extension,ok:opts.Extension.(*AuthAdminRuleExtension)if!ok||extension.AdminSessionnil{returnnil,errors.New(参数错误缺少 AdminSession 扩展数据)}perm:permission.New()// 是否超级管理员super,err:perm.IsSuperAdmin(c.Request.Context(),extension.AdminSession.ID)iferr!nil{returnnil,err}// 树状表格无翻页设定无限 limitopts.Limit999999// 来自选择器则只查 dir、menu不查 nodeifopts.Selector{opts.Wheresappend(opts.Wheres,service.WhereGroup{Wheres:[]service.Where{{Field:type,Operator:IN,Value:[]string{dir,menu},}},})}// 非超管读取当前管理员拥有的权限规则 IDsif!super{ruleIDs,err:perm.GetRuleIds(c.Request.Context(),extension.AdminSession.ID,nil)iferr!nil{returnnil,err}iflen(ruleIDs)0{returnnil,nil}// 添加 IN IDs 的 where 以确保只读取到当前管理员拥有的权限规则opts.Wheresappend(opts.Wheres,service.WhereGroup{Wheres:[]service.Where{{Field:id,Operator:IN,Value:ruleIDs,}},})}rules,err:s.repo.List(c,s.BuildRepoOpts(opts))returnrules,err}Count 方法做类似以上的改造只读取拥有权限的菜单规则的数量前端受益于我们的table组件和useTableManager.ts表格管家按部就班的写好列定义及表单即可自动完成对服务端数状表格的兼容没啥特殊的不再赘述。