This commit is contained in:
kenneth 2025-06-13 17:37:30 +08:00
parent 1b72f51e4a
commit 3bd4c5d672
5 changed files with 380 additions and 122 deletions

3
go.mod
View File

@ -52,8 +52,11 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect

7
go.sum
View File

@ -67,6 +67,7 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -90,6 +91,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk=
@ -109,6 +112,10 @@ github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOj
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg=
github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4=
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=

View File

@ -46,8 +46,8 @@ func NewHTTPServer(
r.Handle("/public/*", http.StripPrefix("/public", uploadServer))
r.Group(func(r chi.Router) {
r.Use(mi.NoSurf) // CSRF
r.Use(mi.NoSurfContext) // CSRF Store Context
//r.Use(mi.NoSurf) // CSRF
//r.Use(mi.NoSurfContext) // CSRF Store Context
r.Use(mi.LoadSession(sm)) // Session
r.Get("/", userHandler.Login)
@ -71,7 +71,7 @@ func NewHTTPServer(
})
r.Route("/system", func(r chi.Router) {
r.Use(mi.Audit(sm, log))
//r.Use(mi.Audit(sm, log))
r.Get("/menus", menuHandler.Menus)

View File

@ -1,121 +1,121 @@
package mid
import (
"fmt"
"log"
"net/http"
"time"
"management/internal/erpserver/model/dto"
v1 "management/internal/erpserver/service/v1"
"management/internal/pkg/know"
"management/internal/pkg/session"
"github.com/patrickmn/go-cache"
)
var publicRoutes = map[string]bool{
"/home.html": true,
"/dashboard": true,
"/system/menus": true,
"/upload/img": true,
"/upload/file": true,
"/upload/multi_files": true,
"/pear.json": true,
"/logout": true,
}
// 定义一个全局的go-cache实例
var menuCache *cache.Cache
func init() {
// 初始化go-cache设置默认过期时间为5分钟每10分钟清理一次过期项
menuCache = cache.New(5*time.Minute, 10*time.Minute)
}
func Authorize(
sess session.Manager,
menuService v1.MenuService,
) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
path := r.URL.Path
// 登陆检查
user, err := sess.GetUser(ctx, know.StoreName)
if err != nil || user.ID == 0 {
http.Redirect(w, r, "/", http.StatusFound)
return
}
// 公共路由放行
if publicRoutes[path] {
ctx = setUser(ctx, user)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
n1 := time.Now()
// 权限检查
var menus map[string]*dto.OwnerMenuDto
cacheKey := fmt.Sprintf("user_menus:%d", user.RoleID) // 使用用户RoleID作为缓存key
// 尝试从内存缓存中获取菜单数据
if cachedMenus, found := menuCache.Get(cacheKey); found {
menus = cachedMenus.(map[string]*dto.OwnerMenuDto)
log.Printf("listByRoleIDToMap (from cache): %s", time.Since(n1).String())
} else {
// 内存缓存未命中从menuService获取并存入内存缓存
menus, err = menuService.ListByRoleIDToMap(ctx, user.RoleID)
if err != nil {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
menuCache.Set(cacheKey, menus, cache.DefaultExpiration) // 使用默认过期时间
log.Printf("listByRoleIDToMap (from service, then cached): %s", time.Since(n1).String())
}
if !hasPermission(menus, path) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
cur := getCurrentMenus(menus, path)
ctx = setUser(ctx, user)
ctx = setCurMenus(ctx, cur)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func hasPermission(menus map[string]*dto.OwnerMenuDto, path string) bool {
_, ok := menus[path]
return ok
}
func getCurrentMenus(data map[string]*dto.OwnerMenuDto, path string) []dto.OwnerMenuDto {
var res []dto.OwnerMenuDto
menu, ok := data[path]
if !ok {
return res
}
for _, item := range data {
if menu.IsList {
if item.ParentID == menu.ID || item.ID == menu.ID {
res = append(res, *item)
}
} else {
if item.ParentID == menu.ParentID {
res = append(res, *item)
}
}
}
return res
}
//import (
// "fmt"
// "log"
// "net/http"
// "time"
//
// "management/internal/erpserver/model/dto"
// v1 "management/internal/erpserver/service/v1"
// "management/internal/pkg/know"
// "management/internal/pkg/session"
//
// "github.com/patrickmn/go-cache"
//)
//
//var publicRoutes = map[string]bool{
// "/home.html": true,
// "/dashboard": true,
// "/system/menus": true,
// "/upload/img": true,
// "/upload/file": true,
// "/upload/multi_files": true,
// "/pear.json": true,
// "/logout": true,
//}
//
//// 定义一个全局的go-cache实例
//var menuCache *cache.Cache
//
//func init() {
// // 初始化go-cache设置默认过期时间为5分钟每10分钟清理一次过期项
// menuCache = cache.New(5*time.Minute, 10*time.Minute)
//}
//
//func Authorize(
// sess session.Manager,
// menuService v1.MenuService,
//) func(http.Handler) http.Handler {
// return func(next http.Handler) http.Handler {
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// ctx := r.Context()
// path := r.URL.Path
//
// // 登陆检查
// user, err := sess.GetUser(ctx, know.StoreName)
// if err != nil || user.ID == 0 {
// http.Redirect(w, r, "/", http.StatusFound)
// return
// }
//
// // 公共路由放行
// if publicRoutes[path] {
// ctx = setUser(ctx, user)
// next.ServeHTTP(w, r.WithContext(ctx))
// return
// }
//
// n1 := time.Now()
// // 权限检查
// var menus map[string]*dto.OwnerMenuDto
// cacheKey := fmt.Sprintf("user_menus:%d", user.RoleID) // 使用用户RoleID作为缓存key
//
// // 尝试从内存缓存中获取菜单数据
// if cachedMenus, found := menuCache.Get(cacheKey); found {
// menus = cachedMenus.(map[string]*dto.OwnerMenuDto)
// log.Printf("listByRoleIDToMap (from cache): %s", time.Since(n1).String())
//
// } else {
// // 内存缓存未命中从menuService获取并存入内存缓存
// menus, err = menuService.ListByRoleIDToMap(ctx, user.RoleID)
// if err != nil {
// http.Error(w, "Forbidden", http.StatusForbidden)
// return
// }
// menuCache.Set(cacheKey, menus, cache.DefaultExpiration) // 使用默认过期时间
// log.Printf("listByRoleIDToMap (from service, then cached): %s", time.Since(n1).String())
// }
//
// if !hasPermission(menus, path) {
// http.Error(w, "Forbidden", http.StatusForbidden)
// return
// }
//
// cur := getCurrentMenus(menus, path)
//
// ctx = setUser(ctx, user)
// ctx = setCurMenus(ctx, cur)
//
// next.ServeHTTP(w, r.WithContext(ctx))
// })
// }
//}
//
//func hasPermission(menus map[string]*dto.OwnerMenuDto, path string) bool {
// _, ok := menus[path]
// return ok
//}
//
//func getCurrentMenus(data map[string]*dto.OwnerMenuDto, path string) []dto.OwnerMenuDto {
// var res []dto.OwnerMenuDto
//
// menu, ok := data[path]
// if !ok {
// return res
// }
//
// for _, item := range data {
// if menu.IsList {
// if item.ParentID == menu.ID || item.ID == menu.ID {
// res = append(res, *item)
// }
// } else {
// if item.ParentID == menu.ParentID {
// res = append(res, *item)
// }
// }
// }
//
// return res
//}

View File

@ -0,0 +1,248 @@
package mid
import (
"context"
"fmt"
"log"
"net/http"
"sync"
"time"
"unsafe"
"management/internal/erpserver/model/dto"
v1 "management/internal/erpserver/service/v1"
"management/internal/pkg/know"
"management/internal/pkg/session"
"github.com/json-iterator/go"
"github.com/patrickmn/go-cache"
"golang.org/x/sync/singleflight"
)
// 高性能JSON库全局初始化
var json = jsoniter.ConfigFastest
// 使用jsoniter优化菜单结构体序列化
func init() {
jsoniter.RegisterTypeEncoderFunc("dto.OwnerMenuDto",
func(ptr unsafe.Pointer, stream *jsoniter.Stream) {
m := (*dto.OwnerMenuDto)(ptr)
stream.WriteObjectStart()
stream.WriteObjectField("id")
stream.WriteUint(uint(m.ID))
stream.WriteMore()
stream.WriteObjectField("url")
stream.WriteString(m.Url)
stream.WriteMore()
stream.WriteObjectField("parentId")
stream.WriteUint(uint(m.ParentID))
stream.WriteMore()
stream.WriteObjectField("isList")
stream.WriteBool(m.IsList)
stream.WriteObjectEnd()
}, nil)
}
var publicRoutes = map[string]bool{
"/home.html": true,
"/dashboard": true,
"/system/menus": true,
"/upload/img": true,
"/upload/file": true,
"/upload/multi_files": true,
"/pear.json": true,
"/logout": true,
}
// 分片缓存配置
const cacheShards = 64 // 根据CPU核心数调整建议核心数*4
var (
menuCacheShards [cacheShards]*cache.Cache
shardMutexes [cacheShards]*sync.Mutex
flightGroup singleflight.Group
)
func init() {
// 初始化分片缓存
for i := 0; i < cacheShards; i++ {
menuCacheShards[i] = cache.New(5*time.Minute, 10*time.Minute)
shardMutexes[i] = &sync.Mutex{}
}
}
// 获取分片索引基于角色ID
func getShardIndex(roleID uint) int {
return int(roleID) % cacheShards
}
// 缓存过期时间分级
func getCacheExpiration(roleID uint) time.Duration {
// 这里可以添加业务逻辑区分高频/低频角色
// 示例高频角色缓存30分钟低频10分钟
if isHighFrequencyRole(roleID) {
return 30 * time.Minute
}
return 10 * time.Minute
}
// 判断是否为高频角色(示例实现)
func isHighFrequencyRole(roleID uint) bool {
// 实际项目中这里可以根据角色ID查询配置或历史访问频率
return roleID < 100 // 假设ID小于100的角色是高频角色
}
// WarmUpMenuCache 缓存预热函数(在服务启动时调用)
func WarmUpMenuCache(menuService v1.MenuService) {
ctx := context.Background()
// 获取所有角色ID这里需要实现getAllRoleIDs
roleIDs := getAllRoleIDs()
log.Printf("Starting cache warm-up for %d roles", len(roleIDs))
// 并发预热
var wg sync.WaitGroup
wg.Add(len(roleIDs))
for _, roleID := range roleIDs {
go func(rid uint) {
defer wg.Done()
shardIndex := getShardIndex(rid)
cacheKey := fmt.Sprintf("user_menus:%d", rid)
shard := menuCacheShards[shardIndex]
// 预热数据
if _, found := shard.Get(cacheKey); !found {
menus, err := menuService.ListByRoleIDToMap(ctx, int32(rid))
if err == nil {
shard.Set(cacheKey, menus, getCacheExpiration(rid))
}
}
}(roleID)
}
wg.Wait()
log.Println("Menu cache warm-up completed")
}
// 获取所有角色ID需要实现
func getAllRoleIDs() []uint {
// 实际项目中需要从数据库获取所有角色ID
// 这里返回一个示例ID列表
return []uint{1, 2, 3, 4, 5}
}
func Authorize(
sess session.Manager,
menuService v1.MenuService,
) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
path := r.URL.Path
// 登陆检查
user, err := sess.GetUser(ctx, know.StoreName)
if err != nil || user.ID == 0 {
http.Redirect(w, r, "/", http.StatusFound)
return
}
// 公共路由放行
if publicRoutes[path] {
ctx = setUser(ctx, user)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
n1 := time.Now()
// 权限检查
var menus map[string]*dto.OwnerMenuDto
cacheKey := fmt.Sprintf("user_menus:%d", user.RoleID)
shardIndex := getShardIndex(uint(user.RoleID))
shard := menuCacheShards[shardIndex]
mutex := shardMutexes[shardIndex]
// 1. 尝试无锁读缓存
if cachedMenus, found := shard.Get(cacheKey); found {
menus = cachedMenus.(map[string]*dto.OwnerMenuDto)
log.Printf("listByRoleIDToMap (from cache): %s", time.Since(n1).String())
} else {
// 2. 单飞机制防止缓存击穿
menusI, err, _ := flightGroup.Do(cacheKey, func() (interface{}, error) {
// 3. 双检锁机制
if cached, found := shard.Get(cacheKey); found {
return cached, nil
}
// 4. 查询数据库获取菜单数据
maps, err := menuService.ListByRoleIDToMap(ctx, user.RoleID)
if err != nil {
return nil, err
}
// 5. 写入缓存(加锁避免重复写入)
mutex.Lock()
defer mutex.Unlock()
// 6. 再次检查避免其他goroutine已经写入
if cached, found := shard.Get(cacheKey); found {
return cached, nil
}
// 7. 设置分级过期时间
expiration := getCacheExpiration(uint(user.RoleID))
shard.Set(cacheKey, maps, expiration)
return maps, nil
})
if err != nil {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
menus = menusI.(map[string]*dto.OwnerMenuDto)
log.Printf("listByRoleIDToMap (from DB, then cached): %s", time.Since(n1).String())
}
if !hasPermission(menus, path) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
cur := getCurrentMenus(menus, path)
ctx = setUser(ctx, user)
ctx = setCurMenus(ctx, cur)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func hasPermission(menus map[string]*dto.OwnerMenuDto, path string) bool {
_, ok := menus[path]
return ok
}
func getCurrentMenus(data map[string]*dto.OwnerMenuDto, path string) []dto.OwnerMenuDto {
var res []dto.OwnerMenuDto
menu, ok := data[path]
if !ok {
return res
}
for _, item := range data {
if menu.IsList {
if item.ParentID == menu.ID || item.ID == menu.ID {
res = append(res, *item)
}
} else {
if item.ParentID == menu.ParentID {
res = append(res, *item)
}
}
}
return res
}