From 82eff47f386abd1dd94b5ad7521d8796f4560277 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Wed, 27 May 2026 08:07:16 -0700 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD(#69):=20=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=BA=A7=E9=99=90=E9=80=9F=E8=A6=86=E7=9B=96=20?= =?UTF-8?q?=E2=80=94=20=E6=95=B0=E6=8D=AE=E5=B1=82=20+=20=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E9=80=BB=E8=BE=91=20+=20=E7=AE=A1=E7=90=86=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 migration 02153: user_subscribe 表添加 speed_limit、traffic_limit 列(幂等) - model 层 Subscribe/SubscribeDetails 新增用户级限速字段(*string 类型支持 nil 回退) - server 用户列表和管理员详情接口支持用户级限速覆盖优先级计算 - 更新管理员订阅更新接口支持写入/清除用户级限速 Co-authored-by: multica-agent --- .../02153_user_subscribe_speed_limit.down.sql | 38 ++++++++++++++++-- .../02153_user_subscribe_speed_limit.up.sql | 34 ++++++++-------- .../admin/user/getUserSubscribeByIdLogic.go | 20 +++++++++- .../admin/user/updateUserSubscribeLogic.go | 2 +- .../logic/server/getServerUserListLogic.go | 14 ++++++- internal/model/user/model.go | 40 ++++++++++--------- internal/model/user/user.go | 4 +- 7 files changed, 105 insertions(+), 47 deletions(-) diff --git a/initialize/migrate/database/02153_user_subscribe_speed_limit.down.sql b/initialize/migrate/database/02153_user_subscribe_speed_limit.down.sql index 579a942..d46dfea 100644 --- a/initialize/migrate/database/02153_user_subscribe_speed_limit.down.sql +++ b/initialize/migrate/database/02153_user_subscribe_speed_limit.down.sql @@ -1,5 +1,35 @@ --- Purpose: Rollback user-level speed limit overrides from user_subscribe +SET @traffic_limit_exists = ( + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'user_subscribe' + AND COLUMN_NAME = 'traffic_limit' +); -ALTER TABLE `user_subscribe` - DROP COLUMN IF EXISTS `traffic_limit`, - DROP COLUMN IF EXISTS `speed_limit`; +SET @traffic_limit_sql = IF( + @traffic_limit_exists = 1, + 'ALTER TABLE `user_subscribe` DROP COLUMN `traffic_limit`', + 'SELECT 1' +); + +PREPARE traffic_limit_stmt FROM @traffic_limit_sql; +EXECUTE traffic_limit_stmt; +DEALLOCATE PREPARE traffic_limit_stmt; + +SET @speed_limit_exists = ( + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'user_subscribe' + AND COLUMN_NAME = 'speed_limit' +); + +SET @speed_limit_sql = IF( + @speed_limit_exists = 1, + 'ALTER TABLE `user_subscribe` DROP COLUMN `speed_limit`', + 'SELECT 1' +); + +PREPARE speed_limit_stmt FROM @speed_limit_sql; +EXECUTE speed_limit_stmt; +DEALLOCATE PREPARE speed_limit_stmt; diff --git a/initialize/migrate/database/02153_user_subscribe_speed_limit.up.sql b/initialize/migrate/database/02153_user_subscribe_speed_limit.up.sql index 001c1fc..f7b4673 100644 --- a/initialize/migrate/database/02153_user_subscribe_speed_limit.up.sql +++ b/initialize/migrate/database/02153_user_subscribe_speed_limit.up.sql @@ -1,6 +1,4 @@ --- Purpose: Add user-level speed limit overrides to user_subscribe - -SET @column_exists = ( +SET @speed_limit_exists = ( SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() @@ -8,17 +6,17 @@ SET @column_exists = ( AND COLUMN_NAME = 'speed_limit' ); -SET @sql = IF( - @column_exists = 0, - 'ALTER TABLE `user_subscribe` ADD COLUMN `speed_limit` int NOT NULL DEFAULT 0 COMMENT ''User-level speed limit override (Mbps, 0=use plan default)'' AFTER `upload`', - 'SELECT ''Column speed_limit already exists in user_subscribe table''' +SET @speed_limit_sql = IF( + @speed_limit_exists = 0, + 'ALTER TABLE `user_subscribe` ADD COLUMN `speed_limit` BIGINT NOT NULL DEFAULT 0 COMMENT ''User-level speed limit override (Mbps), 0 uses plan-level'' AFTER `upload`', + 'SELECT 1' ); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; +PREPARE speed_limit_stmt FROM @speed_limit_sql; +EXECUTE speed_limit_stmt; +DEALLOCATE PREPARE speed_limit_stmt; -SET @column_exists = ( +SET @traffic_limit_exists = ( SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() @@ -26,12 +24,12 @@ SET @column_exists = ( AND COLUMN_NAME = 'traffic_limit' ); -SET @sql = IF( - @column_exists = 0, - 'ALTER TABLE `user_subscribe` ADD COLUMN `traffic_limit` text DEFAULT NULL COMMENT ''User-level traffic limit rules override (JSON, NULL=use plan default)'' AFTER `speed_limit`', - 'SELECT ''Column traffic_limit already exists in user_subscribe table''' +SET @traffic_limit_sql = IF( + @traffic_limit_exists = 0, + 'ALTER TABLE `user_subscribe` ADD COLUMN `traffic_limit` TEXT DEFAULT NULL COMMENT ''User-level traffic limit override (JSON), NULL uses plan-level'' AFTER `speed_limit`', + 'SELECT 1' ); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; +PREPARE traffic_limit_stmt FROM @traffic_limit_sql; +EXECUTE traffic_limit_stmt; +DEALLOCATE PREPARE traffic_limit_stmt; diff --git a/internal/logic/admin/user/getUserSubscribeByIdLogic.go b/internal/logic/admin/user/getUserSubscribeByIdLogic.go index 5ef44e7..dcc59fb 100644 --- a/internal/logic/admin/user/getUserSubscribeByIdLogic.go +++ b/internal/logic/admin/user/getUserSubscribeByIdLogic.go @@ -2,6 +2,7 @@ package user import ( "context" + "encoding/json" "github.com/perfect-panel/server/internal/model/group" "github.com/perfect-panel/server/internal/svc" @@ -36,6 +37,13 @@ func (l *GetUserSubscribeByIdLogic) GetUserSubscribeById(req *types.GetUserSubsc } var subscribeDetails types.UserSubscribeDetail tool.DeepCopy(&subscribeDetails, sub) + subscribeDetails.SpeedLimit = sub.SpeedLimit + if sub.TrafficLimit != nil && *sub.TrafficLimit != "" { + _ = json.Unmarshal([]byte(*sub.TrafficLimit), &subscribeDetails.TrafficLimit) + } + if sub.Subscribe != nil { + subscribeDetails.PlanSpeedLimit = sub.Subscribe.SpeedLimit + } // 填充分组名 if sub.NodeGroupId > 0 { @@ -47,7 +55,17 @@ func (l *GetUserSubscribeByIdLogic) GetUserSubscribeById(req *types.GetUserSubsc // 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) + baseSpeed := sub.Subscribe.SpeedLimit + if sub.SpeedLimit > 0 { + baseSpeed = sub.SpeedLimit + } + + trafficLimit := sub.Subscribe.TrafficLimit + if sub.TrafficLimit != nil && *sub.TrafficLimit != "" { + trafficLimit = *sub.TrafficLimit + } + + result := speedlimit.Calculate(l.ctx, l.svcCtx.DB, sub.UserId, sub.Id, baseSpeed, trafficLimit) subscribeDetails.EffectiveSpeed = result.EffectiveSpeed subscribeDetails.IsThrottled = result.IsThrottled subscribeDetails.ThrottleRule = result.ThrottleRule diff --git a/internal/logic/admin/user/updateUserSubscribeLogic.go b/internal/logic/admin/user/updateUserSubscribeLogic.go index ca03c22..cb131fd 100644 --- a/internal/logic/admin/user/updateUserSubscribeLogic.go +++ b/internal/logic/admin/user/updateUserSubscribeLogic.go @@ -45,7 +45,7 @@ func (l *UpdateUserSubscribeLogic) UpdateUserSubscribe(req *types.UpdateUserSubs } trafficLimit := userSub.TrafficLimit if req.TrafficLimit != nil { - trafficLimit = *req.TrafficLimit + trafficLimit = req.TrafficLimit } err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &user.Subscribe{ diff --git a/internal/logic/server/getServerUserListLogic.go b/internal/logic/server/getServerUserListLogic.go index b8cf825..8b51dd8 100644 --- a/internal/logic/server/getServerUserListLogic.go +++ b/internal/logic/server/getServerUserListLogic.go @@ -307,14 +307,24 @@ func (l *GetServerUserListLogic) canUseExpiredNodeGroup(userSub *user.Subscribe, // calculateEffectiveSpeedLimit 计算用户的实际限速值(考虑按量限速规则) func (l *GetServerUserListLogic) calculateEffectiveSpeedLimit(sub *subscribe.Subscribe, userSub *user.Subscribe) int64 { + baseSpeed := sub.SpeedLimit + if userSub.SpeedLimit > 0 { + baseSpeed = userSub.SpeedLimit + } + + trafficLimit := sub.TrafficLimit + if userSub.TrafficLimit != nil && *userSub.TrafficLimit != "" { + trafficLimit = *userSub.TrafficLimit + } + result := speedlimit.CalculateWithCache( l.ctx.Request.Context(), l.svcCtx.Redis, l.svcCtx.DB, userSub.UserId, userSub.Id, - sub.SpeedLimit, - sub.TrafficLimit, + baseSpeed, + trafficLimit, 30*time.Second, ) return result.EffectiveSpeed diff --git a/internal/model/user/model.go b/internal/model/user/model.go index ef9c5e8..8156f42 100644 --- a/internal/model/user/model.go +++ b/internal/model/user/model.go @@ -23,25 +23,27 @@ const ( ) type SubscribeDetails struct { - Id int64 `gorm:"primarykey"` - UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` - User *User `gorm:"foreignKey:UserId;references:Id"` - OrderId int64 `gorm:"index:idx_order_id;not null;comment:Order ID"` - SubscribeId int64 `gorm:"index:idx_subscribe_id;not null;comment:Subscription ID"` - Subscribe *subscribe.Subscribe `gorm:"foreignKey:SubscribeId;references:Id"` - NodeGroupId int64 `gorm:"index:idx_node_group_id;not null;default:0;comment:Node Group ID (single ID)"` - StartTime time.Time `gorm:"default:CURRENT_TIMESTAMP(3);not null;comment:Subscription Start Time"` - ExpireTime time.Time `gorm:"default:NULL;comment:Subscription Expire Time"` - FinishedAt *time.Time `gorm:"default:NULL;comment:Finished Time"` - Traffic int64 `gorm:"default:0;comment:Traffic"` - Download int64 `gorm:"default:0;comment:Download Traffic"` - Upload int64 `gorm:"default:0;comment:Upload Traffic"` - Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"` - UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"` - Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired; 4: Cancelled"` - Note string `gorm:"type:varchar(500);default:'';comment:User note for subscription"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` - UpdatedAt time.Time `gorm:"comment:Update Time"` + Id int64 `gorm:"primarykey"` + UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` + User *User `gorm:"foreignKey:UserId;references:Id"` + OrderId int64 `gorm:"index:idx_order_id;not null;comment:Order ID"` + SubscribeId int64 `gorm:"index:idx_subscribe_id;not null;comment:Subscription ID"` + Subscribe *subscribe.Subscribe `gorm:"foreignKey:SubscribeId;references:Id"` + NodeGroupId int64 `gorm:"index:idx_node_group_id;not null;default:0;comment:Node Group ID (single ID)"` + StartTime time.Time `gorm:"default:CURRENT_TIMESTAMP(3);not null;comment:Subscription Start Time"` + ExpireTime time.Time `gorm:"default:NULL;comment:Subscription Expire Time"` + FinishedAt *time.Time `gorm:"default:NULL;comment:Finished Time"` + Traffic int64 `gorm:"default:0;comment:Traffic"` + Download int64 `gorm:"default:0;comment:Download Traffic"` + Upload int64 `gorm:"default:0;comment:Upload Traffic"` + SpeedLimit int64 `gorm:"default:0;comment:User-level speed limit override (Mbps), 0 uses plan-level"` + TrafficLimit *string `gorm:"type:text;default:null;comment:User-level traffic limit override (JSON), NULL uses plan-level"` + Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"` + UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"` + Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired; 4: Cancelled"` + Note string `gorm:"type:varchar(500);default:'';comment:User note for subscription"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` } type SubscribeLogFilterParams struct { diff --git a/internal/model/user/user.go b/internal/model/user/user.go index bea2c6f..7c3b791 100644 --- a/internal/model/user/user.go +++ b/internal/model/user/user.go @@ -101,10 +101,10 @@ type Subscribe struct { Traffic int64 `gorm:"default:0;comment:Traffic"` Download int64 `gorm:"default:0;comment:Download Traffic"` Upload int64 `gorm:"default:0;comment:Upload Traffic"` - SpeedLimit int64 `gorm:"default:0;comment:User-level speed limit override (Mbps, 0=use plan default)"` - TrafficLimit string `gorm:"type:text;default:null;comment:User-level traffic limit rules override (JSON)"` ExpiredDownload int64 `gorm:"default:0;comment:Expired period download traffic (bytes)"` ExpiredUpload int64 `gorm:"default:0;comment:Expired period upload traffic (bytes)"` + SpeedLimit int64 `gorm:"default:0;comment:User-level speed limit override (Mbps), 0 uses plan-level"` + TrafficLimit *string `gorm:"type:text;default:null;comment:User-level traffic limit override (JSON), NULL uses plan-level"` Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"` UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"` Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted 5: stopped"`