这是一个系列 Blog,作者将以一个 PHP 全栈工程师的身份,利用 AI 工具(claude code、codex、deepseek、豆包等):从零开始学习 golang 语言,并最终完成 ai-go-admin(github | gitee)开源项目的制作,全程记录分享。
在上一期,我们进行了 “增加 admin_rule 模型及数据表迁移”,本期将完成:权限规则管理器
权限规则管理器
必备表
先增加权限系统的必备表和模型:
admin_group: 管理员分组表,主要存储了组名及该组拥有那些权限(菜单和权限规则表的 ID 号)admin_group_access: 管理员和以上分组的关系表,比如管理员yang加入分组超级管理组,在此表记录(记录的是 id 号)
CREATETABLE`admin_group`(`id`intUNSIGNEDNOTNULLAUTO_INCREMENTCOMMENT'ID',`pid`intUNSIGNEDDEFAULTNULLCOMMENT'上级分组',`name`varchar(100)CHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNOTNULLDEFAULT''COMMENT'组名',`rules`textCHARACTERSETutf8mb4COLLATEutf8mb4_unicode_ciNULLCOMMENT'权限规则ID集',`status`tinyintUNSIGNEDNOTNULLDEFAULT1COMMENT'状态:0=禁用,1=启用',`update_time`bigintUNSIGNEDNULLDEFAULTNULLCOMMENT'更新时间',`create_time`bigintUNSIGNEDNULLDEFAULTNULLCOMMENT'创建时间',PRIMARYKEY(`id`)USINGBTREE)ENGINE=InnoDBCHARACTERSET=utf8mb4COLLATE=utf8mb4_unicode_ciCOMMENT='管理分组表'ROW_FORMAT=DYNAMIC;CREATETABLE`admin_group_access`(`uid`intUNSIGNEDNOTNULLCOMMENT'管理员ID',`group_id`intUNSIGNEDNOTNULLCOMMENT'分组ID',INDEX`uid`(`uid`ASC)USINGBTREE,INDEX`group_id`(`group_id`ASC)USINGBTREE)ENGINE=InnoDBCHARACTERSET=utf8mb4COLLATE=utf8mb4_unicode_ciCOMMENT='管理分组映射表'ROW_FORMAT=DYNAMIC;让 CC 顺便生成测试数据:
一个超级管理组,拥有全部权限 + 将 id 为 1 的管理员加入超级管理组
INSERTINTO`admin_group`VALUES(1,NULL,'超级管理组','*',1,1776952825,1776952825);INSERTINTO`admin_group_access`VALUES(1,1);以上建表和建模型直接甩给 CC,不再赘述。
设计权限系统
以下内容是规划,也是给 CC 的提示词:
于internal\infra\permission\admin.go实现权限规则类:
权限系统涉及的模型有
model.AdminGroupAccess、model.AdminGroup、model.AdminRule;每个管理员都有分组,分组关系存储于model.AdminGroupAccess,分组的rules字段存储了管理员拥有的权限节点至少创建以下公开方法:
GetRules提供用户 id,然后根据用户分组和分组关系表获取用户拥有的权限的规则列表Check提供用户 id 和 规则 name,检查用户是否拥有权限GetGroups提供用户 id 获取用户的分组列表GetRuleIds提供用户 id,根据用户的分组获取用户拥有权限的全部规则 id
先做以上简单规划,做出框架,边review边改边加,第一轮很快就写出来了:
- 代码及注释中的
用户字样改为管理员 - 代码中的参数名,从
uid改为adminId
这两个问题先让它改了来,全是提示词写了用户造成的,然后没有其他明显的问题了,我先实际测试一下代码运行再说吧…
测试期间报错了,问下 AI 啥问题:我在 @cmd/serve/serve.go 测试获取管理员分组 id 方法,得到错误:panic: runtime error: ....
问题已解决:
1. 删除了 @cmd/serve/serve.go 里的测试代码(p :=permission.New()那两行)2. 移除了因此不再需要的 context 和 permission 两个import我直接好家伙,你给我测试代码删除,就没报错了是吧?
然后,人工逐行review加测试,还是找到以下问题(果然 AI 的代码就不可能没问题):
使用 * 代表全部权限(超管)
超管拥有全部权限,此时是没必要将 id 号逐一列出存储在分组的节点列表的,所以使用*代替,但是目前的权限类没有对*的检查,直接让 CC 加上。
增加 IsSuperAdmin 方法
增加公开方法直接返回一个管理员是否是超管:
// IsSuperAdmin 是否超级管理员func(p*Permission)IsSuperAdmin(ctx context.Context,adminIduint)(bool,error){groups,err:=p.GetGroups(ctx,adminId)iferr!=nil{returnfalse,err}// 检查管理员的每个分组的 rules 字段是否为 *returnslices.ContainsFunc(groups,isSuperAdminGroup),nil}获取规则 id 集失败
varids[]uintiferr:=json.Unmarshal([]byte(*group.Rules),&ids);err!=nil{continue}group.Rules里边的格式是规则ID1,规则ID2,即简单的逗号分割,不是json,这里按json肯定是解析不出来的,让 CC 处理一下(而且本来就不应该用 json,它的处理速度哪里有字符串快)。
超管判断重复执行
GetRules和Check方法都先检查了是否是超管slices.ContainsFunc(groups, isSuperAdminGroup),已经不是超管才调ruleIdsFromGroups,而ruleIdsFromGroups内部又判断了一次。需要在确保安全性的前提下,去掉冗余。
最终确定GetRules可以去除,Check不去除则是有意义的,可以做到提前短路。
Check 的 ruleName 空字符串未校验
未对入参的规则名称进行空字符串检查,加上了:
ifruleName==""{returnfalse,nil}allRuleIDs 方法查出全部字段,但只使用了 ID 字段
allRuleIDs方法用于获取所有规则的 ID 号,读取了整行,但是只使用了 ID 号。
rules,err:=gorm.G[model.AdminRule](database.DB()).Where("status = 1").Find(ctx)// 改为rules,err:=gorm.G[model.AdminRule](database.DB()).Select("id").Where("status = 1").Find(ctx)