From 2db2bc0860dcefdabaaf646b8c9588eb9501af54 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Sat, 28 Mar 2026 08:14:07 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=99=90=E9=80=9F=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=8F=AF=E8=A7=86=E5=8C=96=20-=20=E5=90=8E=E5=8F=B0=E8=AE=A2?= =?UTF-8?q?=E9=98=85=E8=AF=A6=E6=83=85=E5=B1=95=E7=A4=BA=E5=AE=9E=E9=99=85?= =?UTF-8?q?=E9=99=90=E9=80=9F=E4=B8=8E=E9=99=8D=E9=80=9F=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 pkg/speedlimit/calculator.go 公共限速计算函数 - UserSubscribeDetail 响应新增 effective_speed/is_throttled/throttle_rule 字段 - GetUserSubscribeById 接口查询时实时计算并返回限速状态 - 重构 getServerUserListLogic 的 calculateEffectiveSpeedLimit,改用共享函数 Co-Authored-By: claude-flow --- apis/admin/user.api | 39 ++++--- .../admin/user/getUserSubscribeByIdLogic.go | 10 ++ .../logic/server/getServerUserListLogic.go | 84 +------------- internal/types/types.go | 39 ++++--- pkg/speedlimit/calculator.go | 105 ++++++++++++++++++ 5 files changed, 160 insertions(+), 117 deletions(-) create mode 100644 pkg/speedlimit/calculator.go diff --git a/apis/admin/user.api b/apis/admin/user.api index e75149f..889a1cc 100644 --- a/apis/admin/user.api +++ b/apis/admin/user.api @@ -77,24 +77,27 @@ type ( IsAdmin bool `json:"is_admin"` } UserSubscribeDetail { - Id int64 `json:"id"` - UserId int64 `json:"user_id"` - User User `json:"user"` - OrderId int64 `json:"order_id"` - SubscribeId int64 `json:"subscribe_id"` - Subscribe Subscribe `json:"subscribe"` - NodeGroupId int64 `json:"node_group_id"` - GroupLocked bool `json:"group_locked"` - StartTime int64 `json:"start_time"` - ExpireTime int64 `json:"expire_time"` - ResetTime int64 `json:"reset_time"` - Traffic int64 `json:"traffic"` - Download int64 `json:"download"` - Upload int64 `json:"upload"` - Token string `json:"token"` - Status uint8 `json:"status"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + User User `json:"user"` + OrderId int64 `json:"order_id"` + SubscribeId int64 `json:"subscribe_id"` + Subscribe Subscribe `json:"subscribe"` + NodeGroupId int64 `json:"node_group_id"` + GroupLocked bool `json:"group_locked"` + StartTime int64 `json:"start_time"` + ExpireTime int64 `json:"expire_time"` + ResetTime int64 `json:"reset_time"` + Traffic int64 `json:"traffic"` + Download int64 `json:"download"` + Upload int64 `json:"upload"` + Token string `json:"token"` + Status uint8 `json:"status"` + EffectiveSpeed int64 `json:"effective_speed"` + IsThrottled bool `json:"is_throttled"` + ThrottleRule string `json:"throttle_rule,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` } BatchDeleteUserRequest { Ids []int64 `json:"ids" validate:"required"` diff --git a/internal/logic/admin/user/getUserSubscribeByIdLogic.go b/internal/logic/admin/user/getUserSubscribeByIdLogic.go index 2f9af88..1cc4984 100644 --- a/internal/logic/admin/user/getUserSubscribeByIdLogic.go +++ b/internal/logic/admin/user/getUserSubscribeByIdLogic.go @@ -6,6 +6,7 @@ import ( "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/speedlimit" "github.com/perfect-panel/server/pkg/tool" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" @@ -34,5 +35,14 @@ func (l *GetUserSubscribeByIdLogic) GetUserSubscribeById(req *types.GetUserSubsc } var subscribeDetails types.UserSubscribeDetail tool.DeepCopy(&subscribeDetails, sub) + + // Calculate speed limit status + if sub.Subscribe != nil && sub.Status == 1 { + result := speedlimit.Calculate(l.ctx, l.svcCtx.DB, sub.UserId, sub.Id, sub.Subscribe.SpeedLimit, sub.Subscribe.TrafficLimit) + subscribeDetails.EffectiveSpeed = result.EffectiveSpeed + subscribeDetails.IsThrottled = result.IsThrottled + subscribeDetails.ThrottleRule = result.ThrottleRule + } + return &subscribeDetails, nil } diff --git a/internal/logic/server/getServerUserListLogic.go b/internal/logic/server/getServerUserListLogic.go index f32c97a..817ea70 100644 --- a/internal/logic/server/getServerUserListLogic.go +++ b/internal/logic/server/getServerUserListLogic.go @@ -15,6 +15,7 @@ import ( "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/speedlimit" "github.com/perfect-panel/server/pkg/tool" "github.com/perfect-panel/server/pkg/uuidx" "github.com/perfect-panel/server/pkg/xerr" @@ -294,85 +295,6 @@ func (l *GetServerUserListLogic) canUseExpiredNodeGroup(userSub *user.Subscribe, // calculateEffectiveSpeedLimit 计算用户的实际限速值(考虑按量限速规则) func (l *GetServerUserListLogic) calculateEffectiveSpeedLimit(sub *subscribe.Subscribe, userSub *user.Subscribe) int64 { - baseSpeedLimit := sub.SpeedLimit - - // 解析 traffic_limit 规则 - if sub.TrafficLimit == "" { - return baseSpeedLimit - } - - var trafficLimitRules []types.TrafficLimit - if err := json.Unmarshal([]byte(sub.TrafficLimit), &trafficLimitRules); err != nil { - l.Errorw("[calculateEffectiveSpeedLimit] Failed to unmarshal traffic_limit", - logger.Field("error", err.Error()), - logger.Field("traffic_limit", sub.TrafficLimit)) - return baseSpeedLimit - } - - if len(trafficLimitRules) == 0 { - return baseSpeedLimit - } - - // 查询用户指定时段的流量使用情况 - now := time.Now() - for _, rule := range trafficLimitRules { - var startTime, endTime time.Time - - if rule.StatType == "hour" { - // 按小时统计:根据 StatValue 计算时间范围(往前推 N 小时) - if rule.StatValue <= 0 { - continue - } - // 从当前时间往前推 StatValue 小时 - startTime = now.Add(-time.Duration(rule.StatValue) * time.Hour) - endTime = now - } else if rule.StatType == "day" { - // 按天统计:根据 StatValue 计算时间范围(往前推 N 天) - if rule.StatValue <= 0 { - continue - } - // 从当前时间往前推 StatValue 天 - startTime = now.AddDate(0, 0, -int(rule.StatValue)) - endTime = now - } else { - continue - } - - // 查询该时段的流量使用 - var usedTraffic struct { - Upload int64 - Download int64 - } - err := l.svcCtx.DB.WithContext(l.ctx.Request.Context()). - Table("traffic_log"). - Select("COALESCE(SUM(upload), 0) as upload, COALESCE(SUM(download), 0) as download"). - Where("user_id = ? AND subscribe_id = ? AND timestamp >= ? AND timestamp < ?", - userSub.UserId, userSub.Id, startTime, endTime). - Scan(&usedTraffic).Error - - if err != nil { - l.Errorw("[calculateEffectiveSpeedLimit] Failed to query traffic usage", - logger.Field("error", err.Error()), - logger.Field("user_id", userSub.UserId), - logger.Field("subscribe_id", userSub.Id)) - continue - } - - // 计算已使用流量(GB) - usedGB := float64(usedTraffic.Upload+usedTraffic.Download) / (1024 * 1024 * 1024) - - // 如果已使用流量达到或超过阈值,应用限速 - if usedGB >= float64(rule.TrafficUsage) { - // 如果规则限速大于0,应用该限速 - // rule.SpeedLimit 单位与 baseSpeedLimit 相同(Mbps),直接比较 - if rule.SpeedLimit > 0 { - // 如果基础限速为0(无限速)或规则限速更严格,使用规则限速 - if baseSpeedLimit == 0 || rule.SpeedLimit < baseSpeedLimit { - return rule.SpeedLimit - } - } - } - } - - return baseSpeedLimit + result := speedlimit.Calculate(l.ctx.Request.Context(), l.svcCtx.DB, userSub.UserId, userSub.Id, sub.SpeedLimit, sub.TrafficLimit) + return result.EffectiveSpeed } diff --git a/internal/types/types.go b/internal/types/types.go index 0b8866f..7a96a47 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -3278,24 +3278,27 @@ type UserSubscribe struct { } type UserSubscribeDetail struct { - Id int64 `json:"id"` - UserId int64 `json:"user_id"` - User User `json:"user"` - OrderId int64 `json:"order_id"` - SubscribeId int64 `json:"subscribe_id"` - Subscribe Subscribe `json:"subscribe"` - NodeGroupId int64 `json:"node_group_id"` - GroupLocked bool `json:"group_locked"` - StartTime int64 `json:"start_time"` - ExpireTime int64 `json:"expire_time"` - ResetTime int64 `json:"reset_time"` - Traffic int64 `json:"traffic"` - Download int64 `json:"download"` - Upload int64 `json:"upload"` - Token string `json:"token"` - Status uint8 `json:"status"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + User User `json:"user"` + OrderId int64 `json:"order_id"` + SubscribeId int64 `json:"subscribe_id"` + Subscribe Subscribe `json:"subscribe"` + NodeGroupId int64 `json:"node_group_id"` + GroupLocked bool `json:"group_locked"` + StartTime int64 `json:"start_time"` + ExpireTime int64 `json:"expire_time"` + ResetTime int64 `json:"reset_time"` + Traffic int64 `json:"traffic"` + Download int64 `json:"download"` + Upload int64 `json:"upload"` + Token string `json:"token"` + Status uint8 `json:"status"` + EffectiveSpeed int64 `json:"effective_speed"` + IsThrottled bool `json:"is_throttled"` + ThrottleRule string `json:"throttle_rule,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` } type UserSubscribeInfo struct { diff --git a/pkg/speedlimit/calculator.go b/pkg/speedlimit/calculator.go new file mode 100644 index 0000000..78f485e --- /dev/null +++ b/pkg/speedlimit/calculator.go @@ -0,0 +1,105 @@ +package speedlimit + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "gorm.io/gorm" +) + +// TrafficLimitRule represents a dynamic speed throttling rule. +type TrafficLimitRule struct { + StatType string `json:"stat_type"` + StatValue int64 `json:"stat_value"` + TrafficUsage int64 `json:"traffic_usage"` + SpeedLimit int64 `json:"speed_limit"` +} + +// ThrottleResult contains the computed speed limit status for a user subscription. +type ThrottleResult struct { + BaseSpeed int64 `json:"base_speed"` // Plan base speed limit (Mbps, 0=unlimited) + EffectiveSpeed int64 `json:"effective_speed"` // Current effective speed limit (Mbps) + IsThrottled bool `json:"is_throttled"` // Whether the user is currently throttled + ThrottleRule string `json:"throttle_rule"` // Description of the matched rule (empty if not throttled) + UsedTrafficGB float64 `json:"used_traffic_gb"` // Traffic used in the matched rule's window (GB) +} + +// Calculate computes the effective speed limit for a user subscription, +// considering traffic-based throttling rules. +func Calculate(ctx context.Context, db *gorm.DB, userId, subscribeId, baseSpeedLimit int64, trafficLimitJSON string) *ThrottleResult { + result := &ThrottleResult{ + BaseSpeed: baseSpeedLimit, + EffectiveSpeed: baseSpeedLimit, + } + + if trafficLimitJSON == "" { + return result + } + + var rules []TrafficLimitRule + if err := json.Unmarshal([]byte(trafficLimitJSON), &rules); err != nil { + return result + } + + if len(rules) == 0 { + return result + } + + now := time.Now() + for _, rule := range rules { + var startTime time.Time + + switch rule.StatType { + case "hour": + if rule.StatValue <= 0 { + continue + } + startTime = now.Add(-time.Duration(rule.StatValue) * time.Hour) + case "day": + if rule.StatValue <= 0 { + continue + } + startTime = now.AddDate(0, 0, -int(rule.StatValue)) + default: + continue + } + + var usedTraffic struct { + Upload int64 + Download int64 + } + err := db.WithContext(ctx). + Table("traffic_log"). + Select("COALESCE(SUM(upload), 0) as upload, COALESCE(SUM(download), 0) as download"). + Where("user_id = ? AND subscribe_id = ? AND timestamp >= ? AND timestamp < ?", + userId, subscribeId, startTime, now). + Scan(&usedTraffic).Error + + if err != nil { + continue + } + + usedGB := float64(usedTraffic.Upload+usedTraffic.Download) / (1024 * 1024 * 1024) + + if usedGB >= float64(rule.TrafficUsage) { + if rule.SpeedLimit > 0 { + if result.EffectiveSpeed == 0 || rule.SpeedLimit < result.EffectiveSpeed { + result.EffectiveSpeed = rule.SpeedLimit + result.IsThrottled = true + result.UsedTrafficGB = usedGB + + statLabel := "小时" + if rule.StatType == "day" { + statLabel = "天" + } + result.ThrottleRule = fmt.Sprintf("%d%s内超%dGB,限速%dMbps", + rule.StatValue, statLabel, rule.TrafficUsage, rule.SpeedLimit) + } + } + } + } + + return result +}