package handler import ( "net/http" "strings" "bj_power_wms/ent" "bj_power_wms/ent/permission" "bj_power_wms/ent/role" "bj_power_wms/ent/user" "bj_power_wms/internal/svc" "github.com/zeromicro/go-zero/core/logx" ) // 种子权限定义:MENU=菜单权限(对应侧栏,path 为路由);BUTTON=按钮级权限(页面内操作) type seedPerm struct { Code string Name string Type string Path string Icon string Sort int // Parent 按钮所属菜单编码(BUTTON 填其所属 MENU code;导出等按钮已按菜单拆分,不再用全局 *) Parent string } var seedPermissions = []seedPerm{ // ===== 菜单权限 ===== {"dashboard:view", "工作台", "MENU", "/", "HomeFilled", 10, ""}, {"inbound:view", "入库管理", "MENU", "/inbound", "Download", 20, ""}, {"outbound:view", "出库管理", "MENU", "/outbound", "Upload", 30, ""}, {"inventory:view", "库存查询", "MENU", "/inventory", "Search", 40, ""}, {"inspection:view", "质量检验", "MENU", "/inspection", "CircleCheck", 50, ""}, {"stocktake:view", "库存盘点", "MENU", "/stocktake", "List", 60, ""}, {"semi:view", "半成品/成品", "MENU", "/semi", "Box", 70, ""}, {"ledger:view", "备料台账", "MENU", "/ledger", "Tickets", 80, ""}, {"zone:view", "区域维护", "MENU", "/zone", "Location", 90, ""}, {"material:view", "物料档案", "MENU", "/material", "Goods", 100, ""}, {"user:manage", "账号管理", "MENU", "/users", "EditPen", 110, ""}, {"role:manage", "角色管理", "MENU", "/roles", "Key", 120, ""}, // ===== 按钮权限 ===== {"inbound:create", "批量入库", "BUTTON", "", "", 200, "inbound:view"}, {"inbound:import", "Excel导入", "BUTTON", "", "", 201, "inbound:view"}, {"inbound:export", "导出", "BUTTON", "", "", 202, "inbound:view"}, {"outbound:create", "发起出库", "BUTTON", "", "", 210, "outbound:view"}, {"outbound:export", "导出", "BUTTON", "", "", 211, "outbound:view"}, {"inspection:create", "录入检验", "BUTTON", "", "", 220, "inspection:view"}, {"inspection:export", "导出", "BUTTON", "", "", 221, "inspection:view"}, {"inventory:export", "导出", "BUTTON", "", "", 204, "inventory:view"}, {"stocktake:start", "发起盘点", "BUTTON", "", "", 230, "stocktake:view"}, {"stocktake:writeback", "差异写回", "BUTTON", "", "", 231, "stocktake:view"}, {"material:create", "新增物料", "BUTTON", "", "", 240, "material:view"}, {"material:edit", "编辑物料", "BUTTON", "", "", 241, "material:view"}, {"material:delete", "删除物料", "BUTTON", "", "", 242, "material:view"}, {"material:import", "导入", "BUTTON", "", "", 243, "material:view"}, {"material:export", "导出", "BUTTON", "", "", 244, "material:view"}, {"zone:create", "新增区域", "BUTTON", "", "", 250, "zone:view"}, {"zone:edit", "编辑区域", "BUTTON", "", "", 251, "zone:view"}, {"zone:delete", "删除区域", "BUTTON", "", "", 252, "zone:view"}, {"user:create", "新增账号", "BUTTON", "", "", 260, "user:manage"}, {"user:edit", "编辑账号", "BUTTON", "", "", 261, "user:manage"}, {"user:delete", "删除账号", "BUTTON", "", "", 262, "user:manage"}, {"role:create", "新增角色", "BUTTON", "", "", 270, "role:manage"}, {"role:edit", "编辑角色", "BUTTON", "", "", 271, "role:manage"}, {"role:delete", "删除角色", "BUTTON", "", "", 272, "role:manage"}, // 注:导出权限已按菜单拆分(inbound/outbound/inspection/inventory:export), // 不再使用全局 *:export,以更精细地控制“谁能在哪个菜单导出”。 } // 三角色预置权限(对齐原硬编码 rolePermissions,并补充按钮权限) // admin 全量;operator 除账号/角色管理外全部;inspector 仅质检相关 func seedRoleCodes() map[string][]string { all := make([]string, 0, len(seedPermissions)) for _, p := range seedPermissions { all = append(all, p.Code) } oper := make([]string, 0, len(seedPermissions)) for _, p := range seedPermissions { if p.Code == "user:manage" || p.Code == "role:manage" || p.Code == "user:create" || p.Code == "user:edit" || p.Code == "user:delete" || p.Code == "role:create" || p.Code == "role:edit" || p.Code == "role:delete" { continue } oper = append(oper, p.Code) } insp := []string{ "dashboard:view", "inventory:view", "inspection:view", "inspection:create", "stocktake:view", "ledger:view", "zone:view", "material:view", "inventory:export", "inspection:export", "material:export", } return map[string][]string{"admin": all, "operator": oper, "inspector": insp} } var seedRoleMeta = []struct { Code string Name string Remark string }{ {"admin", "管理员", "系统管理员,拥有全部权限"}, {"operator", "库房保管员", "入库/出库/库存/盘点等日常作业"}, {"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). SetParentCode(p.Parent). SetPath(p.Path).SetIcon(p.Icon).SetSort(p.Sort). Save(ctx0()) if err == nil { createdP++ } } 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++ } } } // 迁移历史角色:把“可查看的菜单”自动补上对应导出码(精细化:原全局 *: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()) } // 强制管理员角色始终拥有全部权限(不允许被缩减),与“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()) logx.Infof("rbac seed: 新增权限 %d 条、角色 %d 个、绑定历史用户 %d 个", createdP, createdR, bound) ok(w, map[string]any{"createdPermissions": createdP, "createdRoles": createdR, "boundUsers": bound}) }) } // listRolesHandler GET /api/roles 角色列表(含权限码) func listRolesHandler(ctx *svc.ServiceContext) http.HandlerFunc { return requirePerm(ctx, "role:manage", func(w http.ResponseWriter, r *http.Request) { list, err := ctx.EntClient.Role.Query().Order(ent.Asc("id")).All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ok(w, map[string]any{"list": list}) }) } // createRoleHandler POST /api/roles 新增角色 func createRoleHandler(ctx *svc.ServiceContext) http.HandlerFunc { return requirePerm(ctx, "role:create", func(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` Code string `json:"code"` Remark string `json:"remark"` PermissionCodes []string `json:"permissionCodes"` } if err := parseJSON(r, &req); err != nil || req.Code == "" || req.Name == "" { fail(w, http.StatusBadRequest, "角色名称与编码必填") return } exist, _ := ctx.EntClient.Role.Query().Where(role.CodeEQ(req.Code)).Exist(ctx0()) if exist { fail(w, http.StatusConflict, "角色编码已存在: "+req.Code) return } codes := req.PermissionCodes if codes == nil { codes = []string{} } rl, err := ctx.EntClient.Role.Create(). SetName(req.Name).SetCode(req.Code). SetNillableRemark(strPtr(req.Remark)). SetPermissionCodes(codes).Save(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ok(w, rl) }) } // updateRoleHandler POST /api/roles/update 编辑角色(含权限勾选) func updateRoleHandler(ctx *svc.ServiceContext) http.HandlerFunc { return requirePerm(ctx, "role:edit", func(w http.ResponseWriter, r *http.Request) { var req struct { ID int `json:"id"` Name string `json:"name"` Remark string `json:"remark"` PermissionCodes []string `json:"permissionCodes"` } if err := parseJSON(r, &req); err != nil || req.ID <= 0 { fail(w, http.StatusBadRequest, "参数错误") return } // 内置管理员角色不允许修改其权限(admin 拥有所有权限,且不可被缩减) target, err := ctx.EntClient.Role.Get(ctx0(), req.ID) if err != nil { fail(w, http.StatusNotFound, "角色不存在") return } if target.Code == "admin" { fail(w, http.StatusBadRequest, "内置管理员角色不允许修改其权限") return } // 角色编码创建后不可修改(前端亦置灰),此处只改名称/备注/权限 upd := ctx.EntClient.Role.UpdateOneID(req.ID) if req.Name != "" { upd = upd.SetName(req.Name) } if req.Remark != "" { upd = upd.SetRemark(req.Remark) } if req.PermissionCodes != nil { upd = upd.SetPermissionCodes(req.PermissionCodes) } rl, err := upd.Save(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ok(w, rl) }) } // deleteRoleHandler POST /api/roles/delete 删除角色(内置三角色禁止删除) func deleteRoleHandler(ctx *svc.ServiceContext) http.HandlerFunc { return requirePerm(ctx, "role:delete", func(w http.ResponseWriter, r *http.Request) { var req struct { ID int `json:"id"` } if err := parseJSON(r, &req); err != nil || req.ID <= 0 { fail(w, http.StatusBadRequest, "参数错误") return } target, err := ctx.EntClient.Role.Get(ctx0(), req.ID) if err != nil { fail(w, http.StatusNotFound, "角色不存在") return } if target.Code == "admin" || target.Code == "operator" || target.Code == "inspector" { fail(w, http.StatusBadRequest, "内置角色不可删除: "+target.Code) return } if err := ctx.EntClient.Role.DeleteOneID(req.ID).Exec(ctx0()); err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ok(w, nil) }) } // listPermissionsHandler GET /api/permissions 权限列表(菜单+按钮,按 sort 排序) func listPermissionsHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { list, err := ctx.EntClient.Permission.Query(). Order(ent.Asc("sort"), ent.Asc("id")).All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ok(w, map[string]any{"list": list}) } }