208 lines
6.5 KiB
Go
208 lines
6.5 KiB
Go
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()
|
||
}
|