package logic import ( "context" "net/http" "sort" "strings" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "tool-api/internal/consts" ) // ============================================================================ // 后台权限分级(T17) // // 模型:角色(超管/运营/只读)→ 权限点集合;中间件按「路由 → 权限点」放行。 // 权限点与角色的对应关系集中在本文件,避免散落到每个 handler。 // ============================================================================ // 权限点 const ( PermUserWrite = "user:write" PermToolWrite = "tool:write" PermModuleWrite = "module:write" PermLevelWrite = "level:write" PermFeedbackWrite = "feedback:write" PermQuotaWrite = "quota:write" PermEventWrite = "event:write" PermAdminManage = "admin:manage" PermAuditRead = "audit:read" // 订单域:查看 / 退款(退款为可逆性资金动作,单独权限点便于收口) PermOrderRead = "order:read" PermOrderWrite = "order:write" ) // rolePermissions 角色 → 权限点集合。 // // 超管:全部 // 运营:内容读写(用户/工具/模块/等级/反馈/额度/年会/订单),不含管理员管理与审计 // 只读:无写权限,仅可查看(含订单查看) var rolePermissions = map[string]map[string]bool{ consts.AdminRoleSuper: { PermUserWrite: true, PermToolWrite: true, PermModuleWrite: true, PermLevelWrite: true, PermFeedbackWrite: true, PermQuotaWrite: true, PermEventWrite: true, PermAdminManage: true, PermAuditRead: true, PermOrderRead: true, PermOrderWrite: true, }, consts.AdminRoleOperator: { PermUserWrite: true, PermToolWrite: true, PermModuleWrite: true, PermLevelWrite: true, PermFeedbackWrite: true, PermQuotaWrite: true, PermEventWrite: true, PermOrderRead: true, PermOrderWrite: true, }, consts.AdminRoleReadonly: { PermOrderRead: true, }, } // HasPermission 判断角色是否拥有权限点;perm 为空表示「仅需登录」。 // 未知角色(历史脏数据)→ 按最小权限处理(false),避免越权。 func HasPermission(role, perm string) bool { if perm == "" { return true } perms, ok := rolePermissions[role] if !ok { return false } return perms[perm] } // PermissionsOf 返回角色的权限点列表(前端据此隐藏入口),已排序。 func PermissionsOf(role string) []string { perms := rolePermissions[role] out := make([]string, 0, len(perms)) for p := range perms { out = append(out, p) } sort.Strings(out) return out } // roleNameOf 角色值 → 展示名 func roleNameOf(role string) string { switch role { case consts.AdminRoleSuper: return "超级管理员" case consts.AdminRoleOperator: return "运营" case consts.AdminRoleReadonly: return "只读" } return role } // permissionForRoute 路由 → 所需权限点。空串 = 仅需登录(只读接口)。 // 采用「方法 + 路径前缀」映射;/user/quota-period 是额度操作,需先于 /user/ 命中。 func permissionForRoute(method, path string) string { if method != http.MethodPost { if strings.HasPrefix(path, "/audit/") { return PermAuditRead } if strings.HasPrefix(path, "/admin/") { return PermAdminManage } if strings.HasPrefix(path, "/order/") { return PermOrderRead } return "" } switch { case path == "/user/quota-period": return PermQuotaWrite case path == "/audit/record": // 上报审计≠读取审计:任何登录的管理员都可记录自己的动作(如导出) return "" case strings.HasPrefix(path, "/user/"): return PermUserWrite case strings.HasPrefix(path, "/tools/"): return PermToolWrite case strings.HasPrefix(path, "/module/"): return PermModuleWrite case strings.HasPrefix(path, "/level/"): return PermLevelWrite case strings.HasPrefix(path, "/feedback/"): return PermFeedbackWrite case strings.HasPrefix(path, "/quota/"), strings.HasPrefix(path, "/plan/"): return PermQuotaWrite case strings.HasPrefix(path, "/promo/"), strings.HasPrefix(path, "/coupon/"): // 优惠码 / 优惠券属商业化配置,归入「额度写」权限(运营+超管可写;只读不可写) return PermQuotaWrite case strings.HasPrefix(path, "/event/"): return PermEventWrite case strings.HasPrefix(path, "/order/"): // 订单退款属可逆性资金动作,按「订单写」权限放行(运营+超管可写;只读不可写) return PermOrderWrite case strings.HasPrefix(path, "/admin/"): return PermAdminManage case strings.HasPrefix(path, "/audit/"): return PermAuditRead } return "" } func writeForbidden(r *ghttp.Request, message string) { r.Response.WriteJson(g.Map{"code": 403, "message": message, "result": nil}) r.Exit() } // AdminPerm 权限中间件(须挂在 AdminAuth 之后)。 // 从库中复核管理员最新状态与角色(使「停用/改角色」立即生效,无需重新登录), // 按「路由 → 权限点」放行;管理员信息写入上下文,供审计复用。 func AdminPerm(r *ghttp.Request) { adminId := r.GetCtxVar(consts.CtxAdminId).Int64() record, err := g.Model(consts.TableAdminUsers).Where("id", adminId).One() if err != nil { // 鉴权已由 AdminAuth 完成;DB 抖动不应把所有人挡在门外 → 放行并告警 g.Log().Warningf(r.Context(), "[adminperm] 读取管理员失败,放行: %v", err) r.Middleware.Next() return } if record.IsEmpty() { writeForbidden(r, "管理员不存在,请重新登录") return } // status 列可能因迁移未执行而缺失:缺失视为启用(fail-open),避免全站被锁 if v, ok := record["status"]; ok && v.Int() != consts.AdminStatusEnabled { writeForbidden(r, "账号已停用") return } role := record["role_value"].String() if !HasPermission(role, permissionForRoute(r.Method, r.URL.Path)) { writeForbidden(r, "当前角色无此操作权限") return } r.SetCtxVar(consts.CtxAdminRole, role) r.SetCtxVar(consts.CtxAdminAccount, record["account"].String()) r.SetCtxVar(consts.CtxAdminName, record["nick_name"].String()) r.Middleware.Next() } // ===== 审计上下文取值 ===== func CtxAdminRole(ctx context.Context) string { req := g.RequestFromCtx(ctx) if req == nil { return "" } return req.GetCtxVar(consts.CtxAdminRole).String() } func CtxAdminAccount(ctx context.Context) string { req := g.RequestFromCtx(ctx) if req == nil { return "" } return req.GetCtxVar(consts.CtxAdminAccount).String() } func CtxAdminName(ctx context.Context) string { req := g.RequestFromCtx(ctx) if req == nil { return "" } return req.GetCtxVar(consts.CtxAdminName).String() }