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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user