From 3d1a31a19f2454de954ba7914217e795d9687cd3 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Fri, 12 Jun 2026 22:39:15 -0700 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=BB=B4=E5=BA=A6=E9=99=90=E9=80=9F=E5=9C=A8=E8=BF=87=E6=9C=9F?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E7=BB=84=E5=88=86=E6=94=AF=E7=94=9F=E6=95=88?= =?UTF-8?q?=20+=20=E7=BB=9F=E4=B8=80=20speed=5Flimit=20=E5=8D=95=E4=BD=8D?= =?UTF-8?q?=E4=B8=BA=20Mbps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getServerUserListLogic.getExpiredUsers 之前完全忽略 user_subscribe.speed_limit, 现在带出用户级覆盖并与过期节点组 speed_limit 取更严(mergeSpeedLimit:0 视为无限制) - node_group.SpeedLimit 注释从 "KB/s" 修正为 "Mbps"(旧注释是笔误,实际下发节点的 ServerUser.SpeedLimit 字段语义就是 Mbps,节点端 ppanel-node 按 *1e6/8 换算为 Byte/s) - apis/node/node.api 给 ServerUser.SpeedLimit 加 Mbps 单位注释 - 新增 TestMergeSpeedLimit 表驱动测试覆盖 8 种边界 主链路(活跃用户、套餐 traffic_limit 阶梯)行为不变,已在生产 (server_id=52) 验证 147 个限速用户下发正确,与 DB 完全对应。 --- apis/node/node.api | 2 ++ .../logic/server/getServerUserListLogic.go | 32 +++++++++++++++---- .../server/getServerUserListLogic_test.go | 29 +++++++++++++++++ internal/model/group/node_group.go | 5 ++- 4 files changed, 61 insertions(+), 7 deletions(-) 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