Files
qitongxue-api/internal/logic/admin_perm.go
2026-09-29 10:57:01 +08:00

208 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}