feat(cli): 添加命令行敏感操作门禁和数据库管理功能
- 添加 migrate、seed、reset-all 命令行子命令 - 实现管理员密码门禁机制保护敏感操作 - 添加数据库表结构迁移和种子数据初始化功能 - 实现清空并重建全部表的 reset-all 功能 - 重构种子数据只包含系统必需基础数据,移除演示数据 - 添加 RBAC 权限角色初始化的命令行支持 - 配置命令行密码验证和操作日志记录功能
This commit is contained in:
@@ -115,140 +115,146 @@ var seedRoleMeta = []struct {
|
||||
{"inspector", "质检员", "质量检验相关"},
|
||||
}
|
||||
|
||||
// seedRbacHandler POST /api/rbac/seed 幂等初始化权限与角色(已存在按 code 跳过)
|
||||
func seedRbacHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return requireAdmin(func(w http.ResponseWriter, r *http.Request) {
|
||||
createdP, createdR := 0, 0
|
||||
for _, p := range seedPermissions {
|
||||
exist, _ := ctx.EntClient.Permission.Query().
|
||||
Where(permission.CodeEQ(p.Code)).Exist(ctx0())
|
||||
if exist {
|
||||
// 已存在:幂等补齐 parent_code(新增字段不影响已建角色权限)
|
||||
_, _ = ctx.EntClient.Permission.Update().
|
||||
Where(permission.CodeEQ(p.Code)).
|
||||
SetParentCode(p.Parent).
|
||||
Save(ctx0())
|
||||
continue
|
||||
}
|
||||
_, err := ctx.EntClient.Permission.Create().
|
||||
SetCode(p.Code).SetName(p.Name).SetType(p.Type).
|
||||
// SeedRBAC 幂等初始化权限与角色(已存在按 code 跳过/补齐),返回(新增权限数, 新增角色数, 绑定历史用户数)。
|
||||
// 接收最小 ServiceContext(命令行 reset-all/seed 仅需 EntClient + EventLog),与 HTTP seedRbacHandler 共用同一逻辑,
|
||||
// 保证两条路径灌入完全一致的权限模型。operator 为操作人(HTTP 传登录名,命令行传 "system")。
|
||||
func SeedRBAC(ctx *svc.ServiceContext, operator string) (createdP, createdR, bound int) {
|
||||
for _, p := range seedPermissions {
|
||||
exist, _ := ctx.EntClient.Permission.Query().
|
||||
Where(permission.CodeEQ(p.Code)).Exist(ctx0())
|
||||
if exist {
|
||||
// 已存在:幂等补齐 parent_code(新增字段不影响已建角色权限)
|
||||
_, _ = ctx.EntClient.Permission.Update().
|
||||
Where(permission.CodeEQ(p.Code)).
|
||||
SetParentCode(p.Parent).
|
||||
SetPath(p.Path).SetIcon(p.Icon).SetSort(p.Sort).
|
||||
Save(ctx0())
|
||||
if err == nil {
|
||||
createdP++
|
||||
}
|
||||
continue
|
||||
}
|
||||
codes := seedRoleCodes()
|
||||
bound := 0
|
||||
for _, m := range seedRoleMeta {
|
||||
rl, err := ctx.EntClient.Role.Query().
|
||||
Where(role.CodeEQ(m.Code)).Only(ctx0())
|
||||
if err != nil {
|
||||
// 内置角色不存在则创建
|
||||
rl, err = ctx.EntClient.Role.Create().
|
||||
SetCode(m.Code).SetName(m.Name).SetRemark(m.Remark).
|
||||
SetPermissionCodes(codes[m.Code]).
|
||||
Save(ctx0())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
createdR++
|
||||
}
|
||||
// 把「role 字符串 == 该角色 code」的历史用户绑定到 role_id(幂等,仅绑定 role_id 为空者)
|
||||
users, _ := ctx.EntClient.User.Query().
|
||||
Where(user.RoleEQ(m.Code), user.RoleIDIsNil()).All(ctx0())
|
||||
for _, u := range users {
|
||||
if _, err := ctx.EntClient.User.UpdateOneID(u.ID).SetRoleID(rl.ID).Save(ctx0()); err == nil {
|
||||
bound++
|
||||
}
|
||||
}
|
||||
_, err := ctx.EntClient.Permission.Create().
|
||||
SetCode(p.Code).SetName(p.Name).SetType(p.Type).
|
||||
SetParentCode(p.Parent).
|
||||
SetPath(p.Path).SetIcon(p.Icon).SetSort(p.Sort).
|
||||
Save(ctx0())
|
||||
if err == nil {
|
||||
createdP++
|
||||
}
|
||||
// 迁移历史角色:把“可查看的菜单”自动补上对应导出码(精细化:原全局 *:export 等价于“可见菜单均可导出”)
|
||||
// 同时清理可能残留的废弃 *:export。已有角色 seed 不会覆盖,故在此补齐导出能力。
|
||||
menuExport := map[string]string{}
|
||||
for _, p := range seedPermissions {
|
||||
if p.Type == "BUTTON" && p.Parent != "" && p.Name == "导出" {
|
||||
menuExport[p.Parent] = p.Code
|
||||
}
|
||||
}
|
||||
allRoles, _ := ctx.EntClient.Role.Query().All(ctx0())
|
||||
for _, rl := range allRoles {
|
||||
codes := rl.PermissionCodes
|
||||
changed := false
|
||||
set := map[string]bool{}
|
||||
for _, c := range codes {
|
||||
if c == "*:export" {
|
||||
changed = true // 移除废弃全局码
|
||||
continue
|
||||
}
|
||||
set[c] = true
|
||||
}
|
||||
for c := range set {
|
||||
if strings.HasSuffix(c, ":view") {
|
||||
// c 本身就是菜单权限码(如 inbound:view),与 menuExport 的 key 一致
|
||||
if exp, ok := menuExport[c]; ok && !set[exp] {
|
||||
set[exp] = true
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
newCodes := make([]string, 0, len(set))
|
||||
for c := range set {
|
||||
newCodes = append(newCodes, c)
|
||||
}
|
||||
_, _ = ctx.EntClient.Role.UpdateOneID(rl.ID).SetPermissionCodes(newCodes).Save(ctx0())
|
||||
}
|
||||
// 新增菜单权限的自动补授:seed 新增了 MENU 权限码(如新上线「退库确认」),
|
||||
// 但内置角色已存在、不会被上面覆盖 → 这里把内置角色(非 admin)按 seedRoleCodes
|
||||
// 的期望集合做「并集补授」,保证新菜单对既有角色立即可见。仅对内置三角色生效,
|
||||
// 不触碰自建角色(避免影响管理员的自定义授权)。
|
||||
builtin := map[string]bool{"admin": true, "operator": true, "inspector": true}
|
||||
for _, m := range seedRoleMeta {
|
||||
if m.Code == "admin" || !builtin[m.Code] {
|
||||
continue
|
||||
}
|
||||
rl, err := ctx.EntClient.Role.Query().Where(role.CodeEQ(m.Code)).Only(ctx0())
|
||||
}
|
||||
codes := seedRoleCodes()
|
||||
for _, m := range seedRoleMeta {
|
||||
rl, err := ctx.EntClient.Role.Query().
|
||||
Where(role.CodeEQ(m.Code)).Only(ctx0())
|
||||
if err != nil {
|
||||
// 内置角色不存在则创建
|
||||
rl, err = ctx.EntClient.Role.Create().
|
||||
SetCode(m.Code).SetName(m.Name).SetRemark(m.Remark).
|
||||
SetPermissionCodes(codes[m.Code]).
|
||||
Save(ctx0())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
want := codes[m.Code]
|
||||
set := map[string]bool{}
|
||||
for _, c := range rl.PermissionCodes {
|
||||
set[c] = true
|
||||
createdR++
|
||||
}
|
||||
// 把「role 字符串 == 该角色 code」的历史用户绑定到 role_id(幂等,仅绑定 role_id 为空者)
|
||||
users, _ := ctx.EntClient.User.Query().
|
||||
Where(user.RoleEQ(m.Code), user.RoleIDIsNil()).All(ctx0())
|
||||
for _, u := range users {
|
||||
if _, err := ctx.EntClient.User.UpdateOneID(u.ID).SetRoleID(rl.ID).Save(ctx0()); err == nil {
|
||||
bound++
|
||||
}
|
||||
changed := false
|
||||
for _, c := range want {
|
||||
if !set[c] {
|
||||
set[c] = true
|
||||
}
|
||||
}
|
||||
// 迁移历史角色:把“可查看的菜单”自动补上对应导出码(精细化:原全局 *:export 等价于“可见菜单均可导出”)
|
||||
// 同时清理可能残留的废弃 *:export。已有角色 seed 不会覆盖,故在此补齐导出能力。
|
||||
menuExport := map[string]string{}
|
||||
for _, p := range seedPermissions {
|
||||
if p.Type == "BUTTON" && p.Parent != "" && p.Name == "导出" {
|
||||
menuExport[p.Parent] = p.Code
|
||||
}
|
||||
}
|
||||
allRoles, _ := ctx.EntClient.Role.Query().All(ctx0())
|
||||
for _, rl := range allRoles {
|
||||
codes := rl.PermissionCodes
|
||||
changed := false
|
||||
set := map[string]bool{}
|
||||
for _, c := range codes {
|
||||
if c == "*:export" {
|
||||
changed = true // 移除废弃全局码
|
||||
continue
|
||||
}
|
||||
set[c] = true
|
||||
}
|
||||
for c := range set {
|
||||
if strings.HasSuffix(c, ":view") {
|
||||
// c 本身就是菜单权限码(如 inbound:view),与 menuExport 的 key 一致
|
||||
if exp, ok := menuExport[c]; ok && !set[exp] {
|
||||
set[exp] = true
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
merged := make([]string, 0, len(set))
|
||||
for c := range set {
|
||||
merged = append(merged, c)
|
||||
}
|
||||
_, _ = ctx.EntClient.Role.UpdateOneID(rl.ID).SetPermissionCodes(merged).Save(ctx0())
|
||||
}
|
||||
// 强制管理员角色始终拥有全部权限(不允许被缩减),与“admin 拥有所有权限”一致
|
||||
if adminRole, aerr := ctx.EntClient.Role.Query().Where(role.CodeEQ("admin")).Only(ctx0()); aerr == nil {
|
||||
_ = ctx.EntClient.Role.UpdateOneID(adminRole.ID).SetPermissionCodes(codes["admin"]).Exec(ctx0())
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
// 清理已废弃的全局 *:export 权限行(迁移后无角色引用,幂等)
|
||||
_, _ = ctx.EntClient.Permission.Delete().Where(permission.CodeEQ("*:export")).Exec(ctx0())
|
||||
newCodes := make([]string, 0, len(set))
|
||||
for c := range set {
|
||||
newCodes = append(newCodes, c)
|
||||
}
|
||||
_, _ = ctx.EntClient.Role.UpdateOneID(rl.ID).SetPermissionCodes(newCodes).Save(ctx0())
|
||||
}
|
||||
// 新增菜单权限的自动补授:seed 新增了 MENU 权限码(如新上线「退库确认」),
|
||||
// 但内置角色已存在、不会被上面覆盖 → 这里把内置角色(非 admin)按 seedRoleCodes
|
||||
// 的期望集合做「并集补授」,保证新菜单对既有角色立即可见。仅对内置三角色生效,
|
||||
// 不触碰自建角色(避免影响管理员的自定义授权)。
|
||||
builtin := map[string]bool{"admin": true, "operator": true, "inspector": true}
|
||||
for _, m := range seedRoleMeta {
|
||||
if m.Code == "admin" || !builtin[m.Code] {
|
||||
continue
|
||||
}
|
||||
rl, err := ctx.EntClient.Role.Query().Where(role.CodeEQ(m.Code)).Only(ctx0())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
want := codes[m.Code]
|
||||
set := map[string]bool{}
|
||||
for _, c := range rl.PermissionCodes {
|
||||
set[c] = true
|
||||
}
|
||||
changed := false
|
||||
for _, c := range want {
|
||||
if !set[c] {
|
||||
set[c] = true
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
merged := make([]string, 0, len(set))
|
||||
for c := range set {
|
||||
merged = append(merged, c)
|
||||
}
|
||||
_, _ = ctx.EntClient.Role.UpdateOneID(rl.ID).SetPermissionCodes(merged).Save(ctx0())
|
||||
}
|
||||
// 强制管理员角色始终拥有全部权限(不允许被缩减),与“admin 拥有所有权限”一致
|
||||
if adminRole, aerr := ctx.EntClient.Role.Query().Where(role.CodeEQ("admin")).Only(ctx0()); aerr == nil {
|
||||
_ = ctx.EntClient.Role.UpdateOneID(adminRole.ID).SetPermissionCodes(codes["admin"]).Exec(ctx0())
|
||||
}
|
||||
// 清理已废弃的全局 *:export 权限行(迁移后无角色引用,幂等)
|
||||
_, _ = ctx.EntClient.Permission.Delete().Where(permission.CodeEQ("*:export")).Exec(ctx0())
|
||||
|
||||
// 幂等 seed 只在发生真实变更(新增权限/角色/绑定历史用户)时记录日志,避免每次页面加载刷屏
|
||||
if createdP > 0 || createdR > 0 || bound > 0 {
|
||||
ctx.EventLog.Write(ctx0(), "rbac.seed", r.Header.Get("X-Username"), "rbac", "",
|
||||
"RBAC 种子初始化", map[string]any{"permissions": createdP, "roles": createdR, "bound": bound})
|
||||
}
|
||||
logx.Infof("rbac seed: 新增权限 %d 条、角色 %d 个、绑定历史用户 %d 个", createdP, createdR, bound)
|
||||
// 幂等 seed 只在发生真实变更(新增权限/角色/绑定历史用户)时记录日志,避免每次页面加载刷屏
|
||||
if createdP > 0 || createdR > 0 || bound > 0 {
|
||||
ctx.EventLog.Write(ctx0(), "rbac.seed", operator, "rbac", "",
|
||||
"RBAC 种子初始化", map[string]any{"permissions": createdP, "roles": createdR, "bound": bound})
|
||||
}
|
||||
logx.Infof("rbac seed: 新增权限 %d 条、角色 %d 个、绑定历史用户 %d 个", createdP, createdR, bound)
|
||||
return
|
||||
}
|
||||
|
||||
// seedRbacHandler POST /api/rbac/seed 幂等初始化权限与角色(复用 SeedRBAC,与命令行 reset-all/seed 同源)
|
||||
func seedRbacHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return requireAdmin(func(w http.ResponseWriter, r *http.Request) {
|
||||
createdP, createdR, bound := SeedRBAC(ctx, r.Header.Get("X-Username"))
|
||||
ok(w, map[string]any{"createdPermissions": createdP, "createdRoles": createdR, "boundUsers": bound})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user