diff --git a/apis/node/node.api b/apis/node/node.api index 9313aca..e028a37 100644 --- a/apis/node/node.api +++ b/apis/node/node.api @@ -64,6 +64,8 @@ type ( ServerUser { Id int64 `json:"id"` UUID string `json:"uuid"` + // SpeedLimit 单位为 Mbps,0 表示不限速。 + // 节点端 (V2bX/XrayR 等) 按 Mbps 解释该值,服务端透传不做单位换算。 SpeedLimit int64 `json:"speed_limit"` DeviceLimit int64 `json:"device_limit"` } diff --git a/internal/logic/server/getServerUserListLogic.go b/internal/logic/server/getServerUserListLogic.go index 80bc085..625595a 100644 --- a/internal/logic/server/getServerUserListLogic.go +++ b/internal/logic/server/getServerUserListLogic.go @@ -258,13 +258,13 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR } } - // 处理过期订阅用户:如果当前节点属于过期节点组,添加符合条件的过期用户 + // 处理过期订阅用户:如果当前节点属于过期节点组,添加符合条件的过期用户。 + // 用户级 speed_limit (user_subscribe.speed_limit) 与过期节点组 speed_limit + // 取更严格的一个 — 0 视为"无限制",正值优先于 0。 if len(nodeGroupIds) > 0 { expiredUsers, expiredSpeedLimit := l.getExpiredUsers(nodeGroupIds) for i := range expiredUsers { - if expiredSpeedLimit > 0 { - expiredUsers[i].SpeedLimit = expiredSpeedLimit - } + expiredUsers[i].SpeedLimit = mergeSpeedLimit(expiredUsers[i].SpeedLimit, expiredSpeedLimit) } users = append(users, expiredUsers...) } @@ -369,14 +369,34 @@ func (l *GetServerUserListLogic) getExpiredUsers(serverNodeGroupIds []int64) ([] } seen[userSub.Id] = true users = append(users, types.ServerUser{ - Id: userSub.Id, - UUID: userSub.UUID, + Id: userSub.Id, + UUID: userSub.UUID, + SpeedLimit: userSub.SpeedLimit, }) } return users, int64(expiredGroup.SpeedLimit) } +// mergeSpeedLimit 返回两个速度限制(Mbps)中更严格的一个。 +// 0 视为"无限制",因此会被任意正值覆盖;都为 0 时返回 0。 +// 用于用户级 speed_limit 与节点组级 speed_limit 的合并: +// - both 0 → 0 (不限速) +// - 仅一个 > 0 → 取该值 +// - both > 0 → 取较小者(更严格) +func mergeSpeedLimit(a, b int64) int64 { + if a <= 0 { + return b + } + if b <= 0 { + return a + } + if a < b { + return a + } + return b +} + func (l *GetServerUserListLogic) checkExpiredUserEligibility(userSub *user.Subscribe, expiredGroup *group.NodeGroup) bool { expiredDays := int(time.Since(userSub.ExpireTime).Hours() / 24) if expiredDays > expiredGroup.ExpiredDaysLimit { diff --git a/internal/logic/server/getServerUserListLogic_test.go b/internal/logic/server/getServerUserListLogic_test.go index 2744b3f..47a1d79 100644 --- a/internal/logic/server/getServerUserListLogic_test.go +++ b/internal/logic/server/getServerUserListLogic_test.go @@ -338,3 +338,32 @@ func httptestNewRequest() *http.Request { req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/v1/server/user", nil) return req } + +// TestMergeSpeedLimit 验证用户级 speed_limit 与过期节点组 speed_limit 合并规则: +// 0 = 不限制 (loses),正值优先;都为正取较小者(更严格)。 +// 这是 user-dimension 限速在过期节点组分支下能生效的关键。 +func TestMergeSpeedLimit(t *testing.T) { + cases := []struct { + name string + a int64 + b int64 + want int64 + }{ + {"both zero stays zero", 0, 0, 0}, + {"a positive b zero takes a", 30, 0, 30}, + {"a zero b positive takes b", 0, 50, 50}, + {"a negative treated as zero takes b", -1, 50, 50}, + {"b negative treated as zero takes a", 50, -1, 50}, + {"both positive takes smaller (a