feat(subscribe): add traffic limit rules and user traffic stats

- Add subscribe traffic_limit schema and migration\n- Support traffic_limit in admin create/update and list/details\n- Apply traffic_limit when building server user list speed limits\n- Add public user traffic stats API
This commit is contained in:
EUForest
2026-03-14 12:41:52 +08:00
parent 17163486f6
commit 06a2425474
34 changed files with 974 additions and 153 deletions
@@ -2,6 +2,7 @@ package group
import (
"context"
"errors"
"time"
"github.com/perfect-panel/server/internal/model/group"
@@ -25,17 +26,51 @@ func NewCreateNodeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *C
}
func (l *CreateNodeGroupLogic) CreateNodeGroup(req *types.CreateNodeGroupRequest) error {
// 验证:系统中只能有一个过期节点组
if req.IsExpiredGroup != nil && *req.IsExpiredGroup {
var count int64
err := l.svcCtx.DB.Model(&group.NodeGroup{}).
Where("is_expired_group = ?", true).
Count(&count).Error
if err != nil {
logger.Errorf("failed to check expired group count: %v", err)
return err
}
if count > 0 {
return errors.New("system already has an expired node group, cannot create multiple")
}
}
// 创建节点组
nodeGroup := &group.NodeGroup{
Name: req.Name,
Description: req.Description,
Sort: req.Sort,
ForCalculation: req.ForCalculation,
MinTrafficGB: req.MinTrafficGB,
MaxTrafficGB: req.MaxTrafficGB,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Name: req.Name,
Description: req.Description,
Sort: req.Sort,
ForCalculation: req.ForCalculation,
IsExpiredGroup: req.IsExpiredGroup,
MaxTrafficGBExpired: req.MaxTrafficGBExpired,
MinTrafficGB: req.MinTrafficGB,
MaxTrafficGB: req.MaxTrafficGB,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// 设置过期节点组的默认值
if req.IsExpiredGroup != nil && *req.IsExpiredGroup {
// 过期节点组不参与分组计算
falseValue := false
nodeGroup.ForCalculation = &falseValue
if req.ExpiredDaysLimit != nil {
nodeGroup.ExpiredDaysLimit = *req.ExpiredDaysLimit
} else {
nodeGroup.ExpiredDaysLimit = 7 // 默认7天
}
if req.SpeedLimit != nil {
nodeGroup.SpeedLimit = *req.SpeedLimit
}
}
if err := l.svcCtx.DB.Create(nodeGroup).Error; err != nil {
logger.Errorf("failed to create node group: %v", err)
return err
@@ -6,6 +6,7 @@ import (
"fmt"
"github.com/perfect-panel/server/internal/model/group"
"github.com/perfect-panel/server/internal/model/node"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
@@ -37,9 +38,9 @@ func (l *DeleteNodeGroupLogic) DeleteNodeGroup(req *types.DeleteNodeGroupRequest
return err
}
// 检查是否有关联节点
// 检查是否有关联节点(使用JSON_CONTAINS查询node_group_ids数组)
var nodeCount int64
if err := l.svcCtx.DB.Table("nodes").Where("node_group_id = ?", nodeGroup.Id).Count(&nodeCount).Error; err != nil {
if err := l.svcCtx.DB.Model(&node.Node{}).Where("JSON_CONTAINS(node_group_ids, ?)", fmt.Sprintf("[%d]", nodeGroup.Id)).Count(&nodeCount).Error; err != nil {
logger.Errorf("failed to count nodes in group: %v", err)
return err
}
@@ -7,6 +7,7 @@ import (
"fmt"
"github.com/perfect-panel/server/internal/model/group"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
@@ -77,7 +78,7 @@ func (l *ExportGroupResultLogic) ExportGroupResult(req *types.ExportGroupResultR
NodeGroupId int64 `json:"node_group_id"`
}
var userSubscribes []UserNodeGroupInfo
if err := l.svcCtx.DB.Table("user_subscribe").
if err := l.svcCtx.DB.Model(&user.Subscribe{}).
Select("DISTINCT user_id as id, node_group_id").
Where("node_group_id > ?", 0).
Find(&userSubscribes).Error; err != nil {
@@ -2,8 +2,10 @@ package group
import (
"context"
"fmt"
"github.com/perfect-panel/server/internal/model/group"
"github.com/perfect-panel/server/internal/model/node"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
@@ -46,9 +48,9 @@ func (l *GetNodeGroupListLogic) GetNodeGroupList(req *types.GetNodeGroupListRequ
// 转换为响应格式
var list []types.NodeGroup
for _, ng := range nodeGroups {
// 统计该组的节点数
// 统计该组的节点数JSON数组查询)
var nodeCount int64
l.svcCtx.DB.Table("nodes").Where("node_group_id = ?", ng.Id).Count(&nodeCount)
l.svcCtx.DB.Model(&node.Node{}).Where("JSON_CONTAINS(node_group_ids, ?)", fmt.Sprintf("[%d]", ng.Id)).Count(&nodeCount)
// 处理指针类型的字段
var forCalculation bool
@@ -58,25 +60,37 @@ func (l *GetNodeGroupListLogic) GetNodeGroupList(req *types.GetNodeGroupListRequ
forCalculation = true // 默认值
}
var minTrafficGB, maxTrafficGB int64
var isExpiredGroup bool
if ng.IsExpiredGroup != nil {
isExpiredGroup = *ng.IsExpiredGroup
}
var minTrafficGB, maxTrafficGB, maxTrafficGBExpired int64
if ng.MinTrafficGB != nil {
minTrafficGB = *ng.MinTrafficGB
}
if ng.MaxTrafficGB != nil {
maxTrafficGB = *ng.MaxTrafficGB
}
if ng.MaxTrafficGBExpired != nil {
maxTrafficGBExpired = *ng.MaxTrafficGBExpired
}
list = append(list, types.NodeGroup{
Id: ng.Id,
Name: ng.Name,
Description: ng.Description,
Sort: ng.Sort,
ForCalculation: forCalculation,
MinTrafficGB: minTrafficGB,
MaxTrafficGB: maxTrafficGB,
NodeCount: nodeCount,
CreatedAt: ng.CreatedAt.Unix(),
UpdatedAt: ng.UpdatedAt.Unix(),
Id: ng.Id,
Name: ng.Name,
Description: ng.Description,
Sort: ng.Sort,
ForCalculation: forCalculation,
IsExpiredGroup: isExpiredGroup,
ExpiredDaysLimit: ng.ExpiredDaysLimit,
MaxTrafficGBExpired: maxTrafficGBExpired,
SpeedLimit: ng.SpeedLimit,
MinTrafficGB: minTrafficGB,
MaxTrafficGB: maxTrafficGB,
NodeCount: nodeCount,
CreatedAt: ng.CreatedAt.Unix(),
UpdatedAt: ng.UpdatedAt.Unix(),
})
}
@@ -28,14 +28,14 @@ func NewGetSubscribeGroupMappingLogic(ctx context.Context, svcCtx *svc.ServiceCo
func (l *GetSubscribeGroupMappingLogic) GetSubscribeGroupMapping(req *types.GetSubscribeGroupMappingRequest) (resp *types.GetSubscribeGroupMappingResponse, err error) {
// 1. 查询所有订阅套餐
var subscribes []subscribe.Subscribe
if err := l.svcCtx.DB.Table("subscribe").Find(&subscribes).Error; err != nil {
if err := l.svcCtx.DB.Model(&subscribe.Subscribe{}).Find(&subscribes).Error; err != nil {
l.Errorw("[GetSubscribeGroupMapping] failed to query subscribes", logger.Field("error", err.Error()))
return nil, err
}
// 2. 查询所有节点组
var nodeGroups []group.NodeGroup
if err := l.svcCtx.DB.Table("node_group").Find(&nodeGroups).Error; err != nil {
if err := l.svcCtx.DB.Model(&group.NodeGroup{}).Find(&nodeGroups).Error; err != nil {
l.Errorw("[GetSubscribeGroupMapping] failed to query node groups", logger.Field("error", err.Error()))
return nil, err
}
@@ -6,7 +6,10 @@ import (
"fmt"
"strings"
"github.com/perfect-panel/server/internal/model/group"
"github.com/perfect-panel/server/internal/model/node"
"github.com/perfect-panel/server/internal/model/subscribe"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
@@ -38,7 +41,7 @@ func (l *PreviewUserNodesLogic) PreviewUserNodes(req *types.PreviewUserNodesRequ
NodeGroupId int64 // 用户订阅的 node_group_id(单个ID
}
var userSubscribes []UserSubscribe
err = l.svcCtx.DB.Table("user_subscribe").
err = l.svcCtx.DB.Model(&user.Subscribe{}).
Select("id, user_id, subscribe_id, node_group_id").
Where("user_id = ? AND status IN ?", req.UserId, []int8{0, 1}).
Find(&userSubscribes).Error
@@ -74,7 +77,7 @@ func (l *PreviewUserNodesLogic) PreviewUserNodes(req *types.PreviewUserNodesRequ
NodeTags string // 节点标签
}
var subscribeInfos []SubscribeInfo
err = l.svcCtx.DB.Table("subscribe").
err = l.svcCtx.DB.Model(&subscribe.Subscribe{}).
Select("id, node_group_id, node_group_ids, nodes, node_tags").
Where("id IN ?", subscribeIds).
Find(&subscribeInfos).Error
@@ -149,15 +152,23 @@ func (l *PreviewUserNodesLogic) PreviewUserNodes(req *types.PreviewUserNodesRequ
logger.Infof("[PreviewUserNodes] collected direct node_ids: %v", allDirectNodeIds)
// 4. 判断分组功能是否启用
var groupEnabled string
l.svcCtx.DB.Table("system").
type SystemConfig struct {
Value string
}
var config SystemConfig
l.svcCtx.DB.Model(&struct {
Category string `gorm:"column:category"`
Key string `gorm:"column:key"`
Value string `gorm:"column:value"`
}{}).
Table("system").
Where("`category` = ? AND `key` = ?", "group", "enabled").
Select("value").
Scan(&groupEnabled)
Scan(&config)
logger.Infof("[PreviewUserNodes] groupEnabled: %v", groupEnabled)
logger.Infof("[PreviewUserNodes] groupEnabled: %v", config.Value)
isGroupEnabled := groupEnabled == "true" || groupEnabled == "1"
isGroupEnabled := config.Value == "true" || config.Value == "1"
var filteredNodes []node.Node
@@ -177,7 +188,7 @@ func (l *PreviewUserNodesLogic) PreviewUserNodes(req *types.PreviewUserNodesRequ
// 5. 查询所有启用的节点(只有当有节点组时才查询)
if len(allNodeGroupIds) > 0 {
var dbNodes []node.Node
err = l.svcCtx.DB.Table("nodes").
err = l.svcCtx.DB.Model(&node.Node{}).
Where("enabled = ?", true).
Find(&dbNodes).Error
if err != nil {
@@ -238,7 +249,7 @@ func (l *PreviewUserNodesLogic) PreviewUserNodes(req *types.PreviewUserNodesRequ
// 8. 查询所有启用的节点(只有当有 tags 时才查询)
if len(allTags) > 0 {
var dbNodes []node.Node
err = l.svcCtx.DB.Table("nodes").
err = l.svcCtx.DB.Model(&node.Node{}).
Where("enabled = ?", true).
Find(&dbNodes).Error
if err != nil {
@@ -370,7 +381,7 @@ func (l *PreviewUserNodesLogic) PreviewUserNodes(req *types.PreviewUserNodesRequ
Name string
}
var nodeGroupInfos []NodeGroupInfo
err = l.svcCtx.DB.Table("node_group").
err = l.svcCtx.DB.Model(&group.NodeGroup{}).
Select("id, name").
Where("id IN ?", allGroupIds).
Find(&nodeGroupInfos).Error
@@ -508,7 +519,7 @@ func (l *PreviewUserNodesLogic) PreviewUserNodes(req *types.PreviewUserNodesRequ
if len(allDirectNodeIds) > 0 {
// 查询直接分配的节点详情
var directNodes []node.Node
err = l.svcCtx.DB.Table("nodes").
err = l.svcCtx.DB.Model(&node.Node{}).
Where("id IN ? AND enabled = ?", allDirectNodeIds, true).
Find(&directNodes).Error
if err != nil {
@@ -3,9 +3,13 @@ package group
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/perfect-panel/server/internal/model/group"
"github.com/perfect-panel/server/internal/model/node"
"github.com/perfect-panel/server/internal/model/subscribe"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
@@ -131,7 +135,7 @@ func (l *RecalculateGroupLogic) getUserEmail(tx *gorm.DB, userId int64) string {
}
var authMethod UserAuthMethod
if err := tx.Table("user_auth_methods").
if err := tx.Model(&user.AuthMethods{}).
Select("auth_identifier").
Where("user_id = ? AND (auth_type = ? OR auth_type = ?)", userId, "email", "6").
First(&authMethod).Error; err != nil {
@@ -152,7 +156,7 @@ func (l *RecalculateGroupLogic) executeAverageGrouping(tx *gorm.DB, historyId in
}
var userSubscribes []UserSubscribeInfo
if err := tx.Table("user_subscribe").
if err := tx.Model(&user.Subscribe{}).
Select("id, user_id, subscribe_id").
Where("group_locked = ? AND status IN (0, 1)", 0). // 只查询未锁定且有效的用户订阅
Scan(&userSubscribes).Error; err != nil {
@@ -168,7 +172,7 @@ func (l *RecalculateGroupLogic) executeAverageGrouping(tx *gorm.DB, historyId in
// 1.5 查询所有参与计算的节点组ID
var calculationNodeGroups []group.NodeGroup
if err := tx.Table("node_group").
if err := tx.Model(&group.NodeGroup{}).
Select("id").
Where("for_calculation = ?", true).
Scan(&calculationNodeGroups).Error; err != nil {
@@ -195,7 +199,7 @@ func (l *RecalculateGroupLogic) executeAverageGrouping(tx *gorm.DB, historyId in
NodeGroupIds string `json:"node_group_ids"` // JSON string
}
var subscribeInfos []SubscribeInfo
if err := tx.Table("subscribe").
if err := tx.Model(&subscribe.Subscribe{}).
Select("id, node_group_ids").
Where("id IN ?", subscribeIds).
Find(&subscribeInfos).Error; err != nil {
@@ -261,10 +265,10 @@ func (l *RecalculateGroupLogic) executeAverageGrouping(tx *gorm.DB, historyId in
}
}
// 如果没有节点组ID跳过
// 如果没有节点组ID,跳过
if len(nodeGroupIds) == 0 {
l.Debugf("no valid node_group_ids for subscribe_id=%d, setting to 0", subInfo.Id)
if err := tx.Table("user_subscribe").
if err := tx.Model(&user.Subscribe{}).
Where("id = ?", us.Id).
Update("node_group_id", 0).Error; err != nil {
l.Errorw("failed to update user_subscribe node_group_id",
@@ -290,7 +294,7 @@ func (l *RecalculateGroupLogic) executeAverageGrouping(tx *gorm.DB, historyId in
}
// 更新 user_subscribe 的 node_group_id 字段(单个ID
if err := tx.Table("user_subscribe").
if err := tx.Model(&user.Subscribe{}).
Where("id = ?", us.Id).
Update("node_group_id", selectedNodeGroupId).Error; err != nil {
l.Errorw("failed to update user_subscribe node_group_id",
@@ -329,8 +333,8 @@ func (l *RecalculateGroupLogic) executeAverageGrouping(tx *gorm.DB, historyId in
// 统计该节点组的节点数
var nodeCount int64 = 0
if nodeGroupId > 0 {
if err := tx.Table("nodes").
Where("JSON_CONTAINS(node_group_ids, ?)", nodeGroupId).
if err := tx.Model(&node.Node{}).
Where("JSON_CONTAINS(node_group_ids, ?)", fmt.Sprintf("[%d]", nodeGroupId)).
Count(&nodeCount).Error; err != nil {
l.Errorw("failed to count nodes",
logger.Field("node_group_id", nodeGroupId),
@@ -383,7 +387,7 @@ func (l *RecalculateGroupLogic) executeSubscribeGrouping(tx *gorm.DB, historyId
}
var userSubscribes []UserSubscribeInfo
if err := tx.Table("user_subscribe").
if err := tx.Model(&user.Subscribe{}).
Select("id, user_id, subscribe_id").
Where("group_locked = ? AND status IN (0, 1)", 0).
Scan(&userSubscribes).Error; err != nil {
@@ -400,7 +404,7 @@ func (l *RecalculateGroupLogic) executeSubscribeGrouping(tx *gorm.DB, historyId
// 1.5 查询所有参与计算的节点组ID
var calculationNodeGroups []group.NodeGroup
if err := tx.Table("node_group").
if err := tx.Model(&group.NodeGroup{}).
Select("id").
Where("for_calculation = ?", true).
Scan(&calculationNodeGroups).Error; err != nil {
@@ -427,7 +431,7 @@ func (l *RecalculateGroupLogic) executeSubscribeGrouping(tx *gorm.DB, historyId
NodeGroupIds string `json:"node_group_ids"` // JSON string
}
var subscribeInfos []SubscribeInfo
if err := tx.Table("subscribe").
if err := tx.Model(&subscribe.Subscribe{}).
Select("id, node_group_ids").
Where("id IN ?", subscribeIds).
Find(&subscribeInfos).Error; err != nil {
@@ -501,7 +505,7 @@ func (l *RecalculateGroupLogic) executeSubscribeGrouping(tx *gorm.DB, historyId
us.Id, us.SubscribeId, selectedNodeGroupId, len(nodeGroupIds))
// 更新 user_subscribe 的 node_group_id 字段
if err := tx.Table("user_subscribe").
if err := tx.Model(&user.Subscribe{}).
Where("id = ?", us.Id).
Update("node_group_id", selectedNodeGroupId).Error; err != nil {
l.Errorw("failed to update user_subscribe node_group_id",
@@ -548,7 +552,7 @@ func (l *RecalculateGroupLogic) executeSubscribeGrouping(tx *gorm.DB, historyId
expiredAffectedCount := 0
for _, eu := range expiredUserSubscribes {
// 更新 user_subscribe 表的 node_group_id 字段到 0
if err := tx.Table("user_subscribe").
if err := tx.Model(&user.Subscribe{}).
Where("id = ?", eu.Id).
Update("node_group_id", 0).Error; err != nil {
l.Errorw("failed to update expired user subscribe node_group_id",
@@ -573,8 +577,8 @@ func (l *RecalculateGroupLogic) executeSubscribeGrouping(tx *gorm.DB, historyId
// 统计该节点组的节点数
var nodeCount int64 = 0
if nodeGroupId > 0 {
if err := tx.Table("nodes").
Where("JSON_CONTAINS(node_group_ids, ?)", nodeGroupId).
if err := tx.Model(&node.Node{}).
Where("JSON_CONTAINS(node_group_ids, ?)", fmt.Sprintf("[%d]", nodeGroupId)).
Count(&nodeCount).Error; err != nil {
l.Errorw("failed to count nodes",
logger.Field("node_group_id", nodeGroupId),
@@ -652,7 +656,7 @@ func (l *RecalculateGroupLogic) executeTrafficGrouping(tx *gorm.DB, historyId in
}
var userSubscribes []UserSubscribeInfo
if err := tx.Table("user_subscribe").
if err := tx.Model(&user.Subscribe{}).
Select("id, user_id, upload, download, (upload + download) as used_traffic").
Where("group_locked = ? AND status IN (0, 1)", 0). // 只查询有效且未锁定的用户订阅
Scan(&userSubscribes).Error; err != nil {
@@ -694,7 +698,7 @@ func (l *RecalculateGroupLogic) executeTrafficGrouping(tx *gorm.DB, historyId in
// 如果没有匹配到任何范围,targetNodeGroupId 保持为 0(不分配节点组)
// 更新 user_subscribe 的 node_group_id 字段
if err := tx.Table("user_subscribe").
if err := tx.Model(&user.Subscribe{}).
Where("id = ?", us.Id).
Update("node_group_id", targetNodeGroupId).Error; err != nil {
l.Errorw("failed to update user subscribe node_group_id",
@@ -6,6 +6,7 @@ import (
"time"
"github.com/perfect-panel/server/internal/model/group"
"github.com/perfect-panel/server/internal/model/subscribe"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
@@ -37,6 +38,34 @@ func (l *UpdateNodeGroupLogic) UpdateNodeGroup(req *types.UpdateNodeGroupRequest
return err
}
// 验证:系统中只能有一个过期节点组
if req.IsExpiredGroup != nil && *req.IsExpiredGroup {
var count int64
err := l.svcCtx.DB.Model(&group.NodeGroup{}).
Where("is_expired_group = ? AND id != ?", true, req.Id).
Count(&count).Error
if err != nil {
logger.Errorf("failed to check expired group count: %v", err)
return err
}
if count > 0 {
return errors.New("system already has an expired node group, cannot create multiple")
}
// 验证:被订阅商品设置为默认节点组的不能设置为过期节点组
var subscribeCount int64
err = l.svcCtx.DB.Model(&subscribe.Subscribe{}).
Where("node_group_id = ?", req.Id).
Count(&subscribeCount).Error
if err != nil {
logger.Errorf("failed to check subscribe usage: %v", err)
return err
}
if subscribeCount > 0 {
return errors.New("this node group is used as default node group in subscription products, cannot set as expired group")
}
}
// 构建更新数据
updates := map[string]interface{}{
"updated_at": time.Now(),
@@ -53,6 +82,22 @@ func (l *UpdateNodeGroupLogic) UpdateNodeGroup(req *types.UpdateNodeGroupRequest
if req.ForCalculation != nil {
updates["for_calculation"] = *req.ForCalculation
}
if req.IsExpiredGroup != nil {
updates["is_expired_group"] = *req.IsExpiredGroup
// 过期节点组不参与分组计算
if *req.IsExpiredGroup {
updates["for_calculation"] = false
}
}
if req.ExpiredDaysLimit != nil {
updates["expired_days_limit"] = *req.ExpiredDaysLimit
}
if req.MaxTrafficGBExpired != nil {
updates["max_traffic_gb_expired"] = *req.MaxTrafficGBExpired
}
if req.SpeedLimit != nil {
updates["speed_limit"] = *req.SpeedLimit
}
// 获取新的流量区间值
newMinTraffic := nodeGroup.MinTrafficGB
@@ -34,6 +34,12 @@ func (l *CreateSubscribeLogic) CreateSubscribe(req *types.CreateSubscribeRequest
val, _ := json.Marshal(req.Discount)
discount = string(val)
}
trafficLimit := ""
if len(req.TrafficLimit) > 0 {
val, _ := json.Marshal(req.TrafficLimit)
trafficLimit = string(val)
}
sub := &subscribe.Subscribe{
Id: 0,
Name: req.Name,
@@ -52,6 +58,7 @@ func (l *CreateSubscribeLogic) CreateSubscribe(req *types.CreateSubscribeRequest
NodeTags: tool.StringSliceToString(req.NodeTags),
NodeGroupIds: subscribe.JSONInt64Slice(req.NodeGroupIds),
NodeGroupId: req.NodeGroupId,
TrafficLimit: trafficLimit,
Show: req.Show,
Sell: req.Sell,
Sort: 0,
@@ -42,6 +42,12 @@ func (l *GetSubscribeDetailsLogic) GetSubscribeDetails(req *types.GetSubscribeDe
l.Logger.Error("[GetSubscribeDetailsLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("discount", sub.Discount))
}
}
if sub.TrafficLimit != "" {
err = json.Unmarshal([]byte(sub.TrafficLimit), &resp.TrafficLimit)
if err != nil {
l.Logger.Error("[GetSubscribeDetailsLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("traffic_limit", sub.TrafficLimit))
}
}
resp.Nodes = tool.StringToInt64Slice(sub.Nodes)
resp.NodeTags = strings.Split(sub.NodeTags, ",")
return resp, nil
@@ -62,6 +62,12 @@ func (l *GetSubscribeListLogic) GetSubscribeList(req *types.GetSubscribeListRequ
l.Logger.Error("[GetSubscribeListLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("discount", item.Discount))
}
}
if item.TrafficLimit != "" {
err = json.Unmarshal([]byte(item.TrafficLimit), &sub.TrafficLimit)
if err != nil {
l.Logger.Error("[GetSubscribeListLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("traffic_limit", item.TrafficLimit))
}
}
sub.Nodes = tool.StringToInt64Slice(item.Nodes)
sub.NodeTags = strings.Split(item.NodeTags, ",")
// Handle NodeGroupIds - convert from JSONInt64Slice to []int64
@@ -42,6 +42,12 @@ func (l *UpdateSubscribeLogic) UpdateSubscribe(req *types.UpdateSubscribeRequest
val, _ := json.Marshal(req.Discount)
discount = string(val)
}
trafficLimit := ""
if len(req.TrafficLimit) > 0 {
val, _ := json.Marshal(req.TrafficLimit)
trafficLimit = string(val)
}
sub := &subscribe.Subscribe{
Id: req.Id,
Name: req.Name,
@@ -60,6 +66,7 @@ func (l *UpdateSubscribeLogic) UpdateSubscribe(req *types.UpdateSubscribeRequest
NodeTags: tool.StringSliceToString(req.NodeTags),
NodeGroupIds: subscribe.JSONInt64Slice(req.NodeGroupIds),
NodeGroupId: req.NodeGroupId,
TrafficLimit: trafficLimit,
Show: req.Show,
Sell: req.Sell,
Sort: req.Sort,
@@ -5,6 +5,7 @@ import (
"strings"
"time"
"github.com/perfect-panel/server/internal/model/group"
"github.com/perfect-panel/server/internal/model/node"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
@@ -88,7 +89,7 @@ func (l *QueryUserSubscribeNodeListLogic) QueryUserSubscribeNodeList() (resp *ty
func (l *QueryUserSubscribeNodeListLogic) getServers(userSub *user.Subscribe) (userSubscribeNodes []*types.UserSubscribeNodeInfo, err error) {
userSubscribeNodes = make([]*types.UserSubscribeNodeInfo, 0)
if l.isSubscriptionExpired(userSub) {
return l.createExpiredServers(), nil
return l.createExpiredServers(userSub), nil
}
// Check if group management is enabled
@@ -312,8 +313,98 @@ func (l *QueryUserSubscribeNodeListLogic) isSubscriptionExpired(userSub *user.Su
return userSub.ExpireTime.Unix() < time.Now().Unix() && userSub.ExpireTime.Unix() != 0
}
func (l *QueryUserSubscribeNodeListLogic) createExpiredServers() []*types.UserSubscribeNodeInfo {
return nil
func (l *QueryUserSubscribeNodeListLogic) createExpiredServers(userSub *user.Subscribe) []*types.UserSubscribeNodeInfo {
// 1. 查询过期节点组
var expiredGroup group.NodeGroup
err := l.svcCtx.DB.Where("is_expired_group = ?", true).First(&expiredGroup).Error
if err != nil {
l.Debugw("no expired node group configured", logger.Field("error", err))
return nil
}
// 2. 检查用户是否在过期天数限制内
expiredDays := int(time.Since(userSub.ExpireTime).Hours() / 24)
if expiredDays > expiredGroup.ExpiredDaysLimit {
l.Debugf("user subscription expired %d days, exceeds limit %d days", expiredDays, expiredGroup.ExpiredDaysLimit)
return nil
}
// 3. 检查用户已使用流量是否超过限制(仅使用过期期间的流量)
if expiredGroup.MaxTrafficGBExpired != nil && *expiredGroup.MaxTrafficGBExpired > 0 {
usedTrafficGB := (userSub.ExpiredDownload + userSub.ExpiredUpload) / (1024 * 1024 * 1024)
if usedTrafficGB >= *expiredGroup.MaxTrafficGBExpired {
l.Debugf("user expired traffic %d GB, exceeds expired group limit %d GB", usedTrafficGB, *expiredGroup.MaxTrafficGBExpired)
return nil
}
}
// 4. 查询过期节点组的节点
enable := true
_, nodes, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{
Page: 0,
Size: 1000,
NodeGroupIds: []int64{expiredGroup.Id},
Enabled: &enable,
})
if err != nil {
l.Errorw("failed to query expired group nodes", logger.Field("error", err))
return nil
}
if len(nodes) == 0 {
l.Debug("no nodes found in expired group")
return nil
}
// 5. 查询服务器信息
var serverMapIds = make(map[int64]*node.Server)
for _, n := range nodes {
serverMapIds[n.ServerId] = nil
}
var serverIds []int64
for k := range serverMapIds {
serverIds = append(serverIds, k)
}
servers, err := l.svcCtx.NodeModel.QueryServerList(l.ctx, serverIds)
if err != nil {
l.Errorw("failed to query servers", logger.Field("error", err))
return nil
}
for _, s := range servers {
serverMapIds[s.Id] = s
}
// 6. 构建节点列表
userSubscribeNodes := make([]*types.UserSubscribeNodeInfo, 0, len(nodes))
for _, n := range nodes {
server := serverMapIds[n.ServerId]
if server == nil {
continue
}
userSubscribeNode := &types.UserSubscribeNodeInfo{
Id: n.Id,
Name: n.Name,
Uuid: userSub.UUID,
Protocol: n.Protocol,
Protocols: server.Protocols,
Port: n.Port,
Address: n.Address,
Tags: strings.Split(n.Tags, ","),
Country: server.Country,
City: server.City,
Latitude: server.Latitude,
Longitude: server.Longitude,
LongitudeCenter: server.LongitudeCenter,
LatitudeCenter: server.LatitudeCenter,
CreatedAt: n.CreatedAt.Unix(),
}
userSubscribeNodes = append(userSubscribeNodes, userSubscribeNode)
}
l.Infof("returned %d nodes from expired group for user %d (expired %d days)", len(userSubscribeNodes), userSub.UserId, expiredDays)
return userSubscribeNodes
}
func (l *QueryUserSubscribeNodeListLogic) getFirstHostLine() string {
@@ -0,0 +1,138 @@
package user
import (
"context"
"strconv"
"time"
"gorm.io/gorm"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type GetUserTrafficStatsLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Get User Traffic Statistics
func NewGetUserTrafficStatsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserTrafficStatsLogic {
return &GetUserTrafficStatsLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetUserTrafficStatsLogic) GetUserTrafficStats(req *types.GetUserTrafficStatsRequest) (resp *types.GetUserTrafficStatsResponse, err error) {
// 获取当前用户
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
logger.Error("current user is not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
// 将字符串 ID 转换为 int64
userSubscribeId, err := strconv.ParseInt(req.UserSubscribeId, 10, 64)
if err != nil {
l.Errorw("[GetUserTrafficStats] Invalid User Subscribe ID:",
logger.Field("user_subscribe_id", req.UserSubscribeId),
logger.Field("err", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid subscription ID")
}
// 验证订阅归属权 - 直接查询 user_subscribe 表
var userSubscribe struct {
Id int64
UserId int64
}
err = l.svcCtx.DB.WithContext(l.ctx).
Table("user_subscribe").
Select("id, user_id").
Where("id = ?", userSubscribeId).
First(&userSubscribe).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
l.Errorw("[GetUserTrafficStats] User Subscribe Not Found:",
logger.Field("user_subscribe_id", userSubscribeId),
logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Subscription not found")
}
l.Errorw("[GetUserTrafficStats] Query User Subscribe Error:", logger.Field("err", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Subscribe Error")
}
if userSubscribe.UserId != u.Id {
l.Errorw("[GetUserTrafficStats] User Subscribe Access Denied:",
logger.Field("user_subscribe_id", userSubscribeId),
logger.Field("subscribe_user_id", userSubscribe.UserId),
logger.Field("current_user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
// 计算时间范围
now := time.Now()
startDate := now.AddDate(0, 0, -req.Days+1)
startDate = time.Date(startDate.Year(), startDate.Month(), startDate.Day(), 0, 0, 0, 0, time.Local)
// 初始化响应
resp = &types.GetUserTrafficStatsResponse{
List: make([]types.DailyTrafficStats, 0, req.Days),
TotalUpload: 0,
TotalDownload: 0,
TotalTraffic: 0,
}
// 按天查询流量数据
for i := 0; i < req.Days; i++ {
currentDate := startDate.AddDate(0, 0, i)
dayStart := time.Date(currentDate.Year(), currentDate.Month(), currentDate.Day(), 0, 0, 0, 0, time.Local)
dayEnd := dayStart.Add(24 * time.Hour).Add(-time.Nanosecond)
// 查询当天流量
var dailyTraffic struct {
Upload int64
Download int64
}
// 直接使用 model 的查询方法
err := l.svcCtx.DB.WithContext(l.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 BETWEEN ? AND ?",
u.Id, userSubscribeId, dayStart, dayEnd).
Scan(&dailyTraffic).Error
if err != nil {
l.Errorw("[GetUserTrafficStats] Query Daily Traffic Error:",
logger.Field("date", currentDate.Format("2006-01-02")),
logger.Field("err", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query Traffic Error")
}
// 添加到结果列表
total := dailyTraffic.Upload + dailyTraffic.Download
resp.List = append(resp.List, types.DailyTrafficStats{
Date: currentDate.Format("2006-01-02"),
Upload: dailyTraffic.Upload,
Download: dailyTraffic.Download,
Total: total,
})
// 累加总计
resp.TotalUpload += dailyTraffic.Upload
resp.TotalDownload += dailyTraffic.Download
}
resp.TotalTraffic = resp.TotalUpload + resp.TotalDownload
return resp, nil
}
@@ -3,6 +3,7 @@ package user
import (
"context"
"encoding/json"
"strconv"
"time"
"github.com/perfect-panel/server/pkg/constant"
@@ -52,6 +53,9 @@ func (l *QueryUserSubscribeLogic) QueryUserSubscribe() (resp *types.QueryUserSub
var sub types.UserSubscribe
tool.DeepCopy(&sub, item)
// 填充 IdStr 字段,避免前端精度丢失
sub.IdStr = strconv.FormatInt(item.Id, 10)
// 解析Discount字段 避免在续订时只能续订一个月
if item.Subscribe != nil && item.Subscribe.Discount != "" {
var discounts []types.SubscribeDiscount
+196 -2
View File
@@ -4,10 +4,13 @@ import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/model/group"
"github.com/perfect-panel/server/internal/model/node"
"github.com/perfect-panel/server/internal/model/subscribe"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
@@ -133,7 +136,7 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR
return nil, err
}
}
if len(subs) == 0 {
return &types.GetServerUserListResponse{
Users: []types.ServerUser{
@@ -151,14 +154,33 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR
return nil, err
}
for _, datum := range data {
if !l.shouldIncludeServerUser(datum, nodeGroupIds) {
continue
}
// 计算该用户的实际限速值(考虑按量限速规则)
effectiveSpeedLimit := l.calculateEffectiveSpeedLimit(sub, datum)
users = append(users, types.ServerUser{
Id: datum.Id,
UUID: datum.UUID,
SpeedLimit: sub.SpeedLimit,
SpeedLimit: effectiveSpeedLimit,
DeviceLimit: sub.DeviceLimit,
})
}
}
// 处理过期订阅用户:如果当前节点属于过期节点组,添加符合条件的过期用户
if len(nodeGroupIds) > 0 {
expiredUsers, expiredSpeedLimit := l.getExpiredUsers(nodeGroupIds)
for i := range expiredUsers {
if expiredSpeedLimit > 0 {
expiredUsers[i].SpeedLimit = expiredSpeedLimit
}
}
users = append(users, expiredUsers...)
}
if len(users) == 0 {
users = append(users, types.ServerUser{
Id: 1,
@@ -181,3 +203,175 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR
}
return resp, nil
}
func (l *GetServerUserListLogic) shouldIncludeServerUser(userSub *user.Subscribe, serverNodeGroupIds []int64) bool {
if userSub == nil {
return false
}
if userSub.ExpireTime.Unix() == 0 || userSub.ExpireTime.After(time.Now()) {
return true
}
return l.canUseExpiredNodeGroup(userSub, serverNodeGroupIds)
}
func (l *GetServerUserListLogic) getExpiredUsers(serverNodeGroupIds []int64) ([]types.ServerUser, int64) {
var expiredGroup group.NodeGroup
if err := l.svcCtx.DB.Where("is_expired_group = ?", true).First(&expiredGroup).Error; err != nil {
return nil, 0
}
if !tool.Contains(serverNodeGroupIds, expiredGroup.Id) {
return nil, 0
}
var expiredSubs []*user.Subscribe
if err := l.svcCtx.DB.Where("status = ?", 3).Find(&expiredSubs).Error; err != nil {
l.Errorw("query expired subscriptions failed", logger.Field("error", err.Error()))
return nil, 0
}
users := make([]types.ServerUser, 0)
seen := make(map[int64]bool)
for _, userSub := range expiredSubs {
if !l.checkExpiredUserEligibility(userSub, &expiredGroup) {
continue
}
if seen[userSub.Id] {
continue
}
seen[userSub.Id] = true
users = append(users, types.ServerUser{
Id: userSub.Id,
UUID: userSub.UUID,
})
}
return users, int64(expiredGroup.SpeedLimit)
}
func (l *GetServerUserListLogic) checkExpiredUserEligibility(userSub *user.Subscribe, expiredGroup *group.NodeGroup) bool {
expiredDays := int(time.Since(userSub.ExpireTime).Hours() / 24)
if expiredDays > expiredGroup.ExpiredDaysLimit {
return false
}
if expiredGroup.MaxTrafficGBExpired != nil && *expiredGroup.MaxTrafficGBExpired > 0 {
usedTrafficGB := (userSub.ExpiredDownload + userSub.ExpiredUpload) / (1024 * 1024 * 1024)
if usedTrafficGB >= *expiredGroup.MaxTrafficGBExpired {
return false
}
}
return true
}
func (l *GetServerUserListLogic) canUseExpiredNodeGroup(userSub *user.Subscribe, serverNodeGroupIds []int64) bool {
var expiredGroup group.NodeGroup
if err := l.svcCtx.DB.Where("is_expired_group = ?", true).First(&expiredGroup).Error; err != nil {
return false
}
if !tool.Contains(serverNodeGroupIds, expiredGroup.Id) {
return false
}
expiredDays := int(time.Since(userSub.ExpireTime).Hours() / 24)
if expiredDays > expiredGroup.ExpiredDaysLimit {
return false
}
if expiredGroup.MaxTrafficGBExpired != nil && *expiredGroup.MaxTrafficGBExpired > 0 {
usedTrafficGB := (userSub.ExpiredDownload + userSub.ExpiredUpload) / (1024 * 1024 * 1024)
if usedTrafficGB >= *expiredGroup.MaxTrafficGBExpired {
return false
}
}
return true
}
// 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,应用该限速
if rule.SpeedLimit > 0 {
// 如果基础限速为0(无限速)或规则限速更严格,使用规则限速
if baseSpeedLimit == 0 || rule.SpeedLimit < baseSpeedLimit {
return rule.SpeedLimit
}
}
}
}
return baseSpeedLimit
}
@@ -8,6 +8,7 @@ import (
"github.com/perfect-panel/server/adapter"
"github.com/perfect-panel/server/internal/model/client"
"github.com/perfect-panel/server/internal/model/group"
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/model/node"
"github.com/perfect-panel/server/internal/report"
@@ -206,6 +207,19 @@ func (l *SubscribeLogic) logSubscribeActivity(subscribeStatus bool, userSub *use
func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*node.Node, error) {
if l.isSubscriptionExpired(userSub) {
// 尝试获取过期节点组的节点
expiredNodes, err := l.getExpiredGroupNodes(userSub)
if err != nil {
l.Errorw("[Generate Subscribe]get expired group nodes error", logger.Field("error", err.Error()))
return l.createExpiredServers(), nil
}
// 如果有符合条件的过期节点组节点,返回它们
if len(expiredNodes) > 0 {
l.Debugf("[Generate Subscribe]user %d can use expired node group, nodes count: %d", userSub.UserId, len(expiredNodes))
return expiredNodes, nil
}
// 否则返回假的过期节点
l.Debugf("[Generate Subscribe]user %d cannot use expired node group, return fake expired nodes", userSub.UserId)
return l.createExpiredServers(), nil
}
@@ -422,3 +436,52 @@ func (l *SubscribeLogic) isGroupEnabled() bool {
}
return value == "true" || value == "1"
}
// getExpiredGroupNodes 获取过期节点组的节点
func (l *SubscribeLogic) getExpiredGroupNodes(userSub *user.Subscribe) ([]*node.Node, error) {
// 1. 查询过期节点组
var expiredGroup group.NodeGroup
err := l.svc.DB.Where("is_expired_group = ?", true).First(&expiredGroup).Error
if err != nil {
l.Debugw("[SubscribeLogic]no expired node group configured", logger.Field("error", err.Error()))
return nil, err
}
// 2. 检查用户是否在过期天数限制内
expiredDays := int(time.Since(userSub.ExpireTime).Hours() / 24)
if expiredDays > expiredGroup.ExpiredDaysLimit {
l.Debugf("[SubscribeLogic]user %d subscription expired %d days, exceeds limit %d days", userSub.UserId, expiredDays, expiredGroup.ExpiredDaysLimit)
return nil, nil
}
// 3. 检查用户已使用流量是否超过限制(仅使用过期期间的流量)
if expiredGroup.MaxTrafficGBExpired != nil && *expiredGroup.MaxTrafficGBExpired > 0 {
usedTrafficGB := (userSub.ExpiredDownload + userSub.ExpiredUpload) / (1024 * 1024 * 1024)
if usedTrafficGB >= *expiredGroup.MaxTrafficGBExpired {
l.Debugf("[SubscribeLogic]user %d expired traffic %d GB, exceeds expired group limit %d GB", userSub.UserId, usedTrafficGB, *expiredGroup.MaxTrafficGBExpired)
return nil, nil
}
}
// 4. 查询过期节点组的节点
enable := true
_, nodes, err := l.svc.NodeModel.FilterNodeList(l.ctx.Request.Context(), &node.FilterNodeParams{
Page: 0,
Size: 1000,
NodeGroupIds: []int64{expiredGroup.Id},
Enabled: &enable,
Preload: true,
})
if err != nil {
l.Errorw("[SubscribeLogic]failed to query expired group nodes", logger.Field("error", err.Error()))
return nil, err
}
if len(nodes) == 0 {
l.Debug("[SubscribeLogic]no nodes found in expired group")
return nil, nil
}
l.Infof("[SubscribeLogic]returned %d nodes from expired group for user %d (expired %d days)", len(nodes), userSub.UserId, expiredDays)
return nodes, nil
}