feat: 用户订阅显示节点分组名及限速起止时间
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m28s

- UserSubscribe/UserSubscribeDetail 新增 node_group_id/node_group_name 字段
- 管理员查询用户订阅列表批量填充分组名
- 管理员查询单个订阅详情填充分组名
- ThrottleResult/UserSubscribeDetail 新增 throttle_start/throttle_end 字段
- 限速时返回限速窗口起止时间(秒级 Unix 时间戳)

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
shanshanzhong 2026-03-29 10:07:17 -07:00
parent 1dbc3a81e9
commit a9205cc3fc
4 changed files with 49 additions and 5 deletions

View File

@ -3,6 +3,7 @@ package user
import ( import (
"context" "context"
"github.com/perfect-panel/server/internal/model/group"
"github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/logger"
@ -36,12 +37,22 @@ func (l *GetUserSubscribeByIdLogic) GetUserSubscribeById(req *types.GetUserSubsc
var subscribeDetails types.UserSubscribeDetail var subscribeDetails types.UserSubscribeDetail
tool.DeepCopy(&subscribeDetails, sub) tool.DeepCopy(&subscribeDetails, sub)
// 填充分组名
if sub.NodeGroupId > 0 {
var ng group.NodeGroup
if err := l.svcCtx.DB.WithContext(l.ctx).First(&ng, sub.NodeGroupId).Error; err == nil {
subscribeDetails.NodeGroupName = ng.Name
}
}
// 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) result := speedlimit.Calculate(l.ctx, l.svcCtx.DB, sub.UserId, sub.Id, sub.Subscribe.SpeedLimit, sub.Subscribe.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
subscribeDetails.ThrottleStart = result.ThrottleStart
subscribeDetails.ThrottleEnd = result.ThrottleEnd
} }
return &subscribeDetails, nil return &subscribeDetails, nil

View File

@ -3,6 +3,7 @@ package user
import ( import (
"context" "context"
"github.com/perfect-panel/server/internal/model/group"
"github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/logger"
@ -38,10 +39,33 @@ func (l *GetUserSubscribeLogic) GetUserSubscribe(req *types.GetUserSubscribeList
Total: int64(len(data)), Total: int64(len(data)),
} }
// 收集所有 node_group_id批量查分组名
groupIdSet := make(map[int64]struct{})
for _, item := range data {
if item.NodeGroupId > 0 {
groupIdSet[item.NodeGroupId] = struct{}{}
}
}
groupNames := make(map[int64]string)
if len(groupIdSet) > 0 {
ids := make([]int64, 0, len(groupIdSet))
for id := range groupIdSet {
ids = append(ids, id)
}
var groups []group.NodeGroup
if err := l.svcCtx.DB.WithContext(l.ctx).Where("id IN ?", ids).Find(&groups).Error; err == nil {
for _, g := range groups {
groupNames[g.Id] = g.Name
}
}
}
for _, item := range data { for _, item := range data {
var sub types.UserSubscribe var sub types.UserSubscribe
tool.DeepCopy(&sub, item) tool.DeepCopy(&sub, item)
sub.Short, _ = tool.FixedUniqueString(item.Token, 8, "") sub.Short, _ = tool.FixedUniqueString(item.Token, 8, "")
sub.NodeGroupId = item.NodeGroupId
sub.NodeGroupName = groupNames[item.NodeGroupId]
resp.List = append(resp.List, sub) resp.List = append(resp.List, sub)
} }
return return

View File

@ -3259,6 +3259,8 @@ type UserSubscribe struct {
OrderId int64 `json:"order_id"` OrderId int64 `json:"order_id"`
SubscribeId int64 `json:"subscribe_id"` SubscribeId int64 `json:"subscribe_id"`
Subscribe Subscribe `json:"subscribe"` Subscribe Subscribe `json:"subscribe"`
NodeGroupId int64 `json:"node_group_id"`
NodeGroupName string `json:"node_group_name"`
StartTime int64 `json:"start_time"` StartTime int64 `json:"start_time"`
ExpireTime int64 `json:"expire_time"` ExpireTime int64 `json:"expire_time"`
FinishedAt int64 `json:"finished_at"` FinishedAt int64 `json:"finished_at"`
@ -3285,6 +3287,7 @@ type UserSubscribeDetail struct {
SubscribeId int64 `json:"subscribe_id"` SubscribeId int64 `json:"subscribe_id"`
Subscribe Subscribe `json:"subscribe"` Subscribe Subscribe `json:"subscribe"`
NodeGroupId int64 `json:"node_group_id"` NodeGroupId int64 `json:"node_group_id"`
NodeGroupName string `json:"node_group_name"`
GroupLocked bool `json:"group_locked"` GroupLocked bool `json:"group_locked"`
StartTime int64 `json:"start_time"` StartTime int64 `json:"start_time"`
ExpireTime int64 `json:"expire_time"` ExpireTime int64 `json:"expire_time"`
@ -3297,6 +3300,8 @@ type UserSubscribeDetail struct {
EffectiveSpeed int64 `json:"effective_speed"` EffectiveSpeed int64 `json:"effective_speed"`
IsThrottled bool `json:"is_throttled"` IsThrottled bool `json:"is_throttled"`
ThrottleRule string `json:"throttle_rule,omitempty"` ThrottleRule string `json:"throttle_rule,omitempty"`
ThrottleStart int64 `json:"throttle_start,omitempty"`
ThrottleEnd int64 `json:"throttle_end,omitempty"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"`
} }

View File

@ -24,6 +24,8 @@ type ThrottleResult struct {
IsThrottled bool `json:"is_throttled"` // Whether the user is currently throttled 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) 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) UsedTrafficGB float64 `json:"used_traffic_gb"` // Traffic used in the matched rule's window (GB)
ThrottleStart int64 `json:"throttle_start"` // Window start Unix timestamp (seconds), 0 if not throttled
ThrottleEnd int64 `json:"throttle_end"` // Window end Unix timestamp (seconds), 0 if not throttled
} }
// Calculate computes the effective speed limit for a user subscription, // Calculate computes the effective speed limit for a user subscription,
@ -89,6 +91,8 @@ func Calculate(ctx context.Context, db *gorm.DB, userId, subscribeId, baseSpeedL
result.EffectiveSpeed = rule.SpeedLimit result.EffectiveSpeed = rule.SpeedLimit
result.IsThrottled = true result.IsThrottled = true
result.UsedTrafficGB = usedGB result.UsedTrafficGB = usedGB
result.ThrottleStart = startTime.Unix()
result.ThrottleEnd = now.Unix()
statLabel := "小时" statLabel := "小时"
if rule.StatType == "day" { if rule.StatType == "day" {