修复(#1): 服务器用户列表缓存按 protocol 隔离 + 兜底不写缓存

服务器用户列表缓存跨协议污染修复(HIF-1 / 详见 PR #5 四件套):

1. 缓存 key 加 protocol 维度(`server:user:{server_id}:{protocol}`),对齐 ServerConfig 已有约定
2. 显式枚举协议清除用户列表缓存(AllProtocols + ServerUserListCacheKeysForServer),不用 SCAN
3. 三个兜底分支不写缓存 + Errorw 日志(带 server_id + protocol 字段)
4. hysteria2 → hysteria 兼容归一化 + 6 个新单测

Closes HIF-1
This commit is contained in:
2026-06-03 05:37:03 -07:00
committed by GitHub
parent 8ff992e74c
commit 19d28a8f89
6 changed files with 220 additions and 14 deletions
@@ -37,7 +37,9 @@ func NewGetServerUserListLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *Ge
}
func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListRequest) (resp *types.GetServerUserListResponse, err error) {
cacheKey := fmt.Sprintf("%s%d", node.ServerUserListCacheKey, req.ServerId)
protocolRequest := normalizeServerUserListProtocol(req.Protocol)
cacheKey := fmt.Sprintf("%s%d:%s", node.ServerUserListCacheKey, req.ServerId, protocolRequest)
cache, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result()
if cache != "" {
etag := tool.GenerateETag([]byte(cache))
@@ -64,7 +66,7 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR
Page: 1,
Size: 1000,
ServerId: []int64{server.Id},
Protocol: req.Protocol,
Protocol: protocolRequest,
})
if err != nil {
l.Errorw("FilterNodeList error", logger.Field("error", err.Error()))
@@ -72,6 +74,10 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR
}
if len(nodes) == 0 {
l.Errorw("[ServerUserList] fallback: no nodes matched server+protocol, returning placeholder without cache",
logger.Field("server_id", req.ServerId),
logger.Field("protocol", req.Protocol),
)
return &types.GetServerUserListResponse{
Users: []types.ServerUser{
{
@@ -139,6 +145,10 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR
}
if len(subs) == 0 {
l.Errorw("[ServerUserList] fallback: no subscriptions matched node group/tags, returning placeholder without cache",
logger.Field("server_id", req.ServerId),
logger.Field("protocol", req.Protocol),
)
return &types.GetServerUserListResponse{
Users: []types.ServerUser{
{
@@ -183,10 +193,18 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR
}
if len(users) == 0 {
users = append(users, types.ServerUser{
Id: 1,
UUID: uuidx.NewUUID().String(),
})
l.Errorw("[ServerUserList] fallback: matched subs returned zero eligible users, returning placeholder without cache",
logger.Field("server_id", req.ServerId),
logger.Field("protocol", req.Protocol),
)
return &types.GetServerUserListResponse{
Users: []types.ServerUser{
{
Id: 1,
UUID: uuidx.NewUUID().String(),
},
},
}, nil
}
resp = &types.GetServerUserListResponse{
Users: users,
@@ -205,6 +223,17 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR
return resp, nil
}
// normalizeServerUserListProtocol 将客户端可能携带的 hysteria2 兼容字段映射回
// DB 中存储的规范名 "hysteria"。其它协议原样返回。
// 缓存 key 与 FilterNodeList 查询都必须用归一化后的值,
// 否则 hysteria2 永远查不到节点,永远走兜底。
func normalizeServerUserListProtocol(protocol string) string {
if protocol == Hysteria2 {
return Hysteria
}
return protocol
}
func (l *GetServerUserListLogic) serverUserListCacheTTL() time.Duration {
pullInterval := l.svcCtx.Config.Node.NodePullInterval
if pullInterval <= 0 {