新功能(#69): 用户级限速覆盖 — 数据层 + 核心逻辑 + 管理接口
- 新增 migration 02153: user_subscribe 表添加 speed_limit、traffic_limit 列(幂等) - model 层 Subscribe/SubscribeDetails 新增用户级限速字段(*string 类型支持 nil 回退) - server 用户列表和管理员详情接口支持用户级限速覆盖优先级计算 - 更新管理员订阅更新接口支持写入/清除用户级限速 Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -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`
|
SET @traffic_limit_sql = IF(
|
||||||
DROP COLUMN IF EXISTS `traffic_limit`,
|
@traffic_limit_exists = 1,
|
||||||
DROP COLUMN IF EXISTS `speed_limit`;
|
'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;
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
-- Purpose: Add user-level speed limit overrides to user_subscribe
|
SET @speed_limit_exists = (
|
||||||
|
|
||||||
SET @column_exists = (
|
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
@@ -8,17 +6,17 @@ SET @column_exists = (
|
|||||||
AND COLUMN_NAME = 'speed_limit'
|
AND COLUMN_NAME = 'speed_limit'
|
||||||
);
|
);
|
||||||
|
|
||||||
SET @sql = IF(
|
SET @speed_limit_sql = IF(
|
||||||
@column_exists = 0,
|
@speed_limit_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`',
|
'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 ''Column speed_limit already exists in user_subscribe table'''
|
'SELECT 1'
|
||||||
);
|
);
|
||||||
|
|
||||||
PREPARE stmt FROM @sql;
|
PREPARE speed_limit_stmt FROM @speed_limit_sql;
|
||||||
EXECUTE stmt;
|
EXECUTE speed_limit_stmt;
|
||||||
DEALLOCATE PREPARE stmt;
|
DEALLOCATE PREPARE speed_limit_stmt;
|
||||||
|
|
||||||
SET @column_exists = (
|
SET @traffic_limit_exists = (
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
@@ -26,12 +24,12 @@ SET @column_exists = (
|
|||||||
AND COLUMN_NAME = 'traffic_limit'
|
AND COLUMN_NAME = 'traffic_limit'
|
||||||
);
|
);
|
||||||
|
|
||||||
SET @sql = IF(
|
SET @traffic_limit_sql = IF(
|
||||||
@column_exists = 0,
|
@traffic_limit_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`',
|
'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 ''Column traffic_limit already exists in user_subscribe table'''
|
'SELECT 1'
|
||||||
);
|
);
|
||||||
|
|
||||||
PREPARE stmt FROM @sql;
|
PREPARE traffic_limit_stmt FROM @traffic_limit_sql;
|
||||||
EXECUTE stmt;
|
EXECUTE traffic_limit_stmt;
|
||||||
DEALLOCATE PREPARE stmt;
|
DEALLOCATE PREPARE traffic_limit_stmt;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package user
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/group"
|
"github.com/perfect-panel/server/internal/model/group"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
@@ -36,6 +37,13 @@ func (l *GetUserSubscribeByIdLogic) GetUserSubscribeById(req *types.GetUserSubsc
|
|||||||
}
|
}
|
||||||
var subscribeDetails types.UserSubscribeDetail
|
var subscribeDetails types.UserSubscribeDetail
|
||||||
tool.DeepCopy(&subscribeDetails, sub)
|
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 {
|
if sub.NodeGroupId > 0 {
|
||||||
@@ -47,7 +55,17 @@ func (l *GetUserSubscribeByIdLogic) GetUserSubscribeById(req *types.GetUserSubsc
|
|||||||
|
|
||||||
// Calculate speed limit status
|
// Calculate speed limit status
|
||||||
if sub.Subscribe != nil && sub.Status == 1 {
|
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.EffectiveSpeed = result.EffectiveSpeed
|
||||||
subscribeDetails.IsThrottled = result.IsThrottled
|
subscribeDetails.IsThrottled = result.IsThrottled
|
||||||
subscribeDetails.ThrottleRule = result.ThrottleRule
|
subscribeDetails.ThrottleRule = result.ThrottleRule
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func (l *UpdateUserSubscribeLogic) UpdateUserSubscribe(req *types.UpdateUserSubs
|
|||||||
}
|
}
|
||||||
trafficLimit := userSub.TrafficLimit
|
trafficLimit := userSub.TrafficLimit
|
||||||
if req.TrafficLimit != nil {
|
if req.TrafficLimit != nil {
|
||||||
trafficLimit = *req.TrafficLimit
|
trafficLimit = req.TrafficLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &user.Subscribe{
|
err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &user.Subscribe{
|
||||||
|
|||||||
@@ -307,14 +307,24 @@ func (l *GetServerUserListLogic) canUseExpiredNodeGroup(userSub *user.Subscribe,
|
|||||||
|
|
||||||
// calculateEffectiveSpeedLimit 计算用户的实际限速值(考虑按量限速规则)
|
// calculateEffectiveSpeedLimit 计算用户的实际限速值(考虑按量限速规则)
|
||||||
func (l *GetServerUserListLogic) calculateEffectiveSpeedLimit(sub *subscribe.Subscribe, userSub *user.Subscribe) int64 {
|
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(
|
result := speedlimit.CalculateWithCache(
|
||||||
l.ctx.Request.Context(),
|
l.ctx.Request.Context(),
|
||||||
l.svcCtx.Redis,
|
l.svcCtx.Redis,
|
||||||
l.svcCtx.DB,
|
l.svcCtx.DB,
|
||||||
userSub.UserId,
|
userSub.UserId,
|
||||||
userSub.Id,
|
userSub.Id,
|
||||||
sub.SpeedLimit,
|
baseSpeed,
|
||||||
sub.TrafficLimit,
|
trafficLimit,
|
||||||
30*time.Second,
|
30*time.Second,
|
||||||
)
|
)
|
||||||
return result.EffectiveSpeed
|
return result.EffectiveSpeed
|
||||||
|
|||||||
@@ -23,25 +23,27 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SubscribeDetails struct {
|
type SubscribeDetails struct {
|
||||||
Id int64 `gorm:"primarykey"`
|
Id int64 `gorm:"primarykey"`
|
||||||
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
|
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
|
||||||
User *User `gorm:"foreignKey:UserId;references:Id"`
|
User *User `gorm:"foreignKey:UserId;references:Id"`
|
||||||
OrderId int64 `gorm:"index:idx_order_id;not null;comment:Order 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"`
|
SubscribeId int64 `gorm:"index:idx_subscribe_id;not null;comment:Subscription ID"`
|
||||||
Subscribe *subscribe.Subscribe `gorm:"foreignKey:SubscribeId;references: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)"`
|
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"`
|
StartTime time.Time `gorm:"default:CURRENT_TIMESTAMP(3);not null;comment:Subscription Start Time"`
|
||||||
ExpireTime time.Time `gorm:"default:NULL;comment:Subscription Expire Time"`
|
ExpireTime time.Time `gorm:"default:NULL;comment:Subscription Expire Time"`
|
||||||
FinishedAt *time.Time `gorm:"default:NULL;comment:Finished Time"`
|
FinishedAt *time.Time `gorm:"default:NULL;comment:Finished Time"`
|
||||||
Traffic int64 `gorm:"default:0;comment:Traffic"`
|
Traffic int64 `gorm:"default:0;comment:Traffic"`
|
||||||
Download int64 `gorm:"default:0;comment:Download Traffic"`
|
Download int64 `gorm:"default:0;comment:Download Traffic"`
|
||||||
Upload int64 `gorm:"default:0;comment:Upload Traffic"`
|
Upload int64 `gorm:"default:0;comment:Upload Traffic"`
|
||||||
Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"`
|
SpeedLimit int64 `gorm:"default:0;comment:User-level speed limit override (Mbps), 0 uses plan-level"`
|
||||||
UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"`
|
TrafficLimit *string `gorm:"type:text;default:null;comment:User-level traffic limit override (JSON), NULL uses plan-level"`
|
||||||
Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired; 4: Cancelled"`
|
Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"`
|
||||||
Note string `gorm:"type:varchar(500);default:'';comment:User note for subscription"`
|
UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"`
|
||||||
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired; 4: Cancelled"`
|
||||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
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 {
|
type SubscribeLogFilterParams struct {
|
||||||
|
|||||||
@@ -101,10 +101,10 @@ type Subscribe struct {
|
|||||||
Traffic int64 `gorm:"default:0;comment:Traffic"`
|
Traffic int64 `gorm:"default:0;comment:Traffic"`
|
||||||
Download int64 `gorm:"default:0;comment:Download Traffic"`
|
Download int64 `gorm:"default:0;comment:Download Traffic"`
|
||||||
Upload int64 `gorm:"default:0;comment:Upload 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)"`
|
ExpiredDownload int64 `gorm:"default:0;comment:Expired period download traffic (bytes)"`
|
||||||
ExpiredUpload int64 `gorm:"default:0;comment:Expired period upload 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"`
|
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"`
|
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"`
|
Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted 5: stopped"`
|
||||||
|
|||||||
Reference in New Issue
Block a user