修复个人中心问题

This commit is contained in:
2026-09-29 18:25:09 +08:00
parent 0dc5c2f075
commit ca4220f92c
44 changed files with 1793 additions and 310 deletions
+155
View File
@@ -0,0 +1,155 @@
package middleware
import (
"errors"
"log"
"net/http"
"sort"
"strings"
"sync"
"server/pkg/jwtutil"
"server/services"
beego "github.com/beego/beego/v2/server/web"
"github.com/beego/beego/v2/server/web/context"
)
// 模块套餐鉴权(PlanAuth)
//
// 把「接口前缀」映射到「功能模块编码」(yz_system_modules.code),
// 当前租户生效套餐(含已开通的单品订单)未包含该模块时拒绝访问。
//
// 与 JWT 鉴权一致支持三档模式(app.conf: plan_enforce):
// - off :完全不启用
// - warn:观察模式,只记录「本会被拦截」的请求(默认,便于上线前核对映射)
// - on :真正拦截,未开通返回 403
//
// 映射表(app.conf: plan_module_paths)示例:
// plan_module_paths = /backend/crm:crm,/backend/erp:erp,/backend/oa:oa
// 按最长前缀匹配;未出现在映射表中的接口不做模块校验(默认放行),
// 因此可以按模块逐个灰度开启,不会误伤用户/角色/文件/日志等通用接口。
const planMappingSeparator = ":"
var (
errPlanNoToken = errors.New("未提供认证信息")
errPlanBadToken = errors.New("认证信息格式错误")
planOnce sync.Once
planMode = AuthModeWarn
planRoutes []planRoute
)
// planRoute 接口前缀 → 模块编码
type planRoute struct {
prefix string
module string
}
func planInit() {
planOnce.Do(func() {
if v, err := beego.AppConfig.String("plan_enforce"); err == nil {
switch strings.ToLower(strings.TrimSpace(v)) {
case AuthModeOff, AuthModeWarn, AuthModeOn:
planMode = strings.ToLower(strings.TrimSpace(v))
}
}
raw, _ := beego.AppConfig.String("plan_module_paths")
for _, item := range strings.Split(raw, ",") {
item = strings.TrimSpace(item)
if item == "" {
continue
}
parts := strings.SplitN(item, planMappingSeparator, 2)
if len(parts) != 2 {
log.Printf("[plan] 忽略无效映射(应为 前缀:模块编码): %s", item)
continue
}
prefix := strings.TrimSpace(parts[0])
module := services.NormalizeModuleCode(parts[1])
if prefix == "" || module == "" {
log.Printf("[plan] 忽略无效映射(前缀或模块编码为空): %s", item)
continue
}
planRoutes = append(planRoutes, planRoute{prefix: prefix, module: module})
}
// 最长前缀优先:/backend/crm/bidding 必须先于 /backend/crm 命中
sort.Slice(planRoutes, func(i, j int) bool {
return len(planRoutes[i].prefix) > len(planRoutes[j].prefix)
})
log.Printf("[plan] 模块套餐鉴权模式=%s,接口映射=%d 条", planMode, len(planRoutes))
})
}
// planModuleOf 返回路径归属的模块编码;未配置归属时返回空字符串
func planModuleOf(path string) string {
for _, r := range planRoutes {
if strings.HasPrefix(path, r.prefix) {
return r.module
}
}
return ""
}
// PlanAuthMiddleware 模块套餐鉴权中间件
func PlanAuthMiddleware() beego.FilterFunc {
return func(ctx *context.Context) {
planInit()
if planMode == AuthModeOff || len(planRoutes) == 0 {
return
}
if ctx.Input.Method() == http.MethodOptions {
return
}
path := cleanPath(ctx.Request.RequestURI)
if !isProtected(path) || isWhitelisted(path) {
return
}
module := planModuleOf(path)
if module == "" {
return
}
claims, err := planClaims(ctx)
if err != nil {
// 未登录 / 令牌无效:交给 JWTAuthMiddleware 与各控制器按原逻辑处理
return
}
if claims.TenantId <= 0 {
return
}
if services.IsModuleEnabled(uint64(claims.TenantId), module) {
return
}
reason := "模块未开通: " + module
if planMode == AuthModeWarn {
warnLog(path, reason)
return
}
ctx.Output.SetStatus(http.StatusForbidden)
_ = ctx.Output.JSON(map[string]interface{}{
"code": 403,
"msg": "该功能未开通,请先购买对应套餐",
}, false, false)
}
}
// planClaims 解析 Authorization 头中的 JWT(仅用于本中间件的模块归属判断)
func planClaims(ctx *context.Context) (*jwtutil.Claims, error) {
authHeader := ctx.Request.Header.Get("Authorization")
if authHeader == "" {
return nil, errPlanNoToken
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return nil, errPlanBadToken
}
return jwtutil.ParseToken(parts[1])
}